|
107 | 107 |
|
108 | 108 | -define(CONNECT_TIMEOUT, 8000). |
109 | 109 | -define(IDLE_TIMEOUT, infinity). |
| 110 | +%% Grace window for pooled hackney_conn in `closed` state, during which |
| 111 | +%% late-arriving calls race the pool DOWN cleanup and still get a proper |
| 112 | +%% error reply instead of exit:{normal, _}. See issue #836. |
| 113 | +-define(CLOSED_GRACE_MS, 50). |
110 | 114 |
|
111 | 115 | %% State data record |
112 | 116 | -record(conn_data, { |
@@ -895,6 +899,10 @@ connected({call, From}, {send_headers, Method, Path, Headers}, Data) -> |
895 | 899 | %% HTTP/2 owner messages from h2 library |
896 | 900 | connected(info, {h2, H2Conn, Event}, #conn_data{h2_conn = H2Conn} = Data) -> |
897 | 901 | handle_h2_event(Event, Data); |
| 902 | +%% h2_connection is linked via start_link; trap_exit surfaces its termination |
| 903 | +%% as an 'EXIT' signal. Convert to the same cleanup path as the monitor DOWN. |
| 904 | +connected(info, {'EXIT', H2Conn, Reason}, #conn_data{h2_conn = H2Conn} = Data) -> |
| 905 | + h2_on_closed(Reason, Data#conn_data{h2_conn = undefined, h2_mon = undefined}); |
898 | 906 | connected(info, {'DOWN', Mon, process, _Pid, Reason}, |
899 | 907 | #conn_data{h2_mon = Mon} = Data) -> |
900 | 908 | h2_on_closed(Reason, Data#conn_data{h2_conn = undefined, h2_mon = undefined}); |
@@ -1406,15 +1414,23 @@ closed(enter, _OldState, #conn_data{socket = Socket, transport = Transport, pool |
1406 | 1414 | undefined -> ok; |
1407 | 1415 | _ -> Transport:close(Socket) |
1408 | 1416 | end, |
1409 | | - %% For pooled connections, stop the process so pool can clean up |
1410 | | - %% For non-pooled connections, stay alive to allow reconnection |
| 1417 | + %% Pooled connections used to stop immediately here, but that made |
| 1418 | + %% late-arriving {call, From, {request, _}} messages from workers that |
| 1419 | + %% raced the pool checkout race a terminating gen_statem — which |
| 1420 | + %% surfaces as `exit:{normal, _}` in the caller (issue #836). Stay |
| 1421 | + %% alive briefly so those late calls get a proper `{error, {closed, _}}` |
| 1422 | + %% reply via handle_common's invalid_state fallback, then stop. |
1411 | 1423 | case PoolPid of |
1412 | 1424 | undefined -> |
1413 | 1425 | {keep_state, Data#conn_data{socket = undefined}}; |
1414 | 1426 | _ -> |
1415 | | - {stop, normal, Data#conn_data{socket = undefined}} |
| 1427 | + {keep_state, Data#conn_data{socket = undefined}, |
| 1428 | + [{state_timeout, ?CLOSED_GRACE_MS, closed_grace_expired}]} |
1416 | 1429 | end; |
1417 | 1430 |
|
| 1431 | +closed(state_timeout, closed_grace_expired, Data) -> |
| 1432 | + {stop, normal, Data}; |
| 1433 | + |
1418 | 1434 | closed({call, From}, connect, Data) -> |
1419 | 1435 | %% Allow reconnection from closed state |
1420 | 1436 | #conn_data{ |
@@ -1576,6 +1592,12 @@ handle_common(info, {select, _Resource, _Ref, ready_input}, |
1576 | 1592 | _ = hackney_h3:process(ConnRef), |
1577 | 1593 | keep_state_and_data; |
1578 | 1594 |
|
| 1595 | +%% With trap_exit = true, an EXIT signal from any linked process (other than |
| 1596 | +%% h2_conn, handled in connected/3) arrives here. Swallow it rather than |
| 1597 | +%% propagating — avoids tearing down the gen_statem on stray links. |
| 1598 | +handle_common(info, {'EXIT', _Pid, _Reason}, _State, _Data) -> |
| 1599 | + keep_state_and_data; |
| 1600 | + |
1579 | 1601 | handle_common(info, _Msg, _State, _Data) -> |
1580 | 1602 | keep_state_and_data. |
1581 | 1603 |
|
@@ -2303,13 +2325,18 @@ start_h2_connection(Socket, Data, From, Origin) -> |
2303 | 2325 | h2_mon = Mon, |
2304 | 2326 | h2_streams = #{} |
2305 | 2327 | }, |
| 2328 | + %% Cancel any pending idle_timeout armed by the |
| 2329 | + %% TCP-first connected(enter): HTTP/2 connections |
| 2330 | + %% multiplex and stay in `connected`, so the 2s |
| 2331 | + %% pool default would kill a busy conn (#836). |
| 2332 | + CancelIdle = {state_timeout, infinity, idle_timeout}, |
2306 | 2333 | case Origin of |
2307 | 2334 | first_connect -> |
2308 | 2335 | {next_state, connected, NewData, |
2309 | | - [{reply, From, ok}]}; |
| 2336 | + [CancelIdle, {reply, From, ok}]}; |
2310 | 2337 | after_upgrade -> |
2311 | 2338 | {keep_state, NewData, |
2312 | | - [{reply, From, ok}]} |
| 2339 | + [CancelIdle, {reply, From, ok}]} |
2313 | 2340 | end; |
2314 | 2341 | {error, WaitErr} -> |
2315 | 2342 | catch h2_connection:close(H2Conn), |
@@ -2364,17 +2391,26 @@ do_h2_send(From, Method, Path, Headers, Body, StreamState, Mode, Data) -> |
2364 | 2391 | B when is_binary(B) -> B; |
2365 | 2392 | L -> iolist_to_binary(L) |
2366 | 2393 | end, |
2367 | | - SendRes = case BodyBin of |
2368 | | - <<>> -> h2_connection:send_request_headers(H2Conn, H2Headers, true); |
2369 | | - _ -> |
2370 | | - case h2_connection:send_request_headers(H2Conn, H2Headers, false) of |
2371 | | - {ok, SId} -> |
2372 | | - case h2_connection:send_data(H2Conn, SId, BodyBin, true) of |
2373 | | - ok -> {ok, SId}; |
2374 | | - {error, _} = E1 -> E1 |
2375 | | - end; |
2376 | | - Err -> Err |
2377 | | - end |
| 2394 | + %% h2_connection can die between pool checkout and this call; gen_statem:call |
| 2395 | + %% on a dead pid raises exit:noproc. Catch that and normalise to an error |
| 2396 | + %% so the caller sees {error, {closed, _}} instead of a gen_statem:call |
| 2397 | + %% blowing up (issue #836). |
| 2398 | + SendRes = try |
| 2399 | + case BodyBin of |
| 2400 | + <<>> -> h2_connection:send_request_headers(H2Conn, H2Headers, true); |
| 2401 | + _ -> |
| 2402 | + case h2_connection:send_request_headers(H2Conn, H2Headers, false) of |
| 2403 | + {ok, SId} -> |
| 2404 | + case h2_connection:send_data(H2Conn, SId, BodyBin, true) of |
| 2405 | + ok -> {ok, SId}; |
| 2406 | + {error, _} = E1 -> E1 |
| 2407 | + end; |
| 2408 | + Err -> Err |
| 2409 | + end |
| 2410 | + end |
| 2411 | + catch |
| 2412 | + exit:{ExitReason, _} -> {error, {closed, ExitReason}}; |
| 2413 | + exit:ExitReason -> {error, {closed, ExitReason}} |
2378 | 2414 | end, |
2379 | 2415 | case SendRes of |
2380 | 2416 | {ok, StreamId} -> |
@@ -2544,6 +2580,9 @@ h2_on_closed(Reason, Data) -> |
2544 | 2580 | {Replies, Data1} = collect_h2_aborts({closed, Reason}, Data), |
2545 | 2581 | Stripped = Data1#conn_data{h2_conn = undefined, h2_mon = undefined, |
2546 | 2582 | socket = undefined}, |
| 2583 | + %% Transition to closed. For pooled conns, closed(enter,...) keeps the |
| 2584 | + %% process alive for ?CLOSED_GRACE_MS so calls from workers that raced |
| 2585 | + %% the pool checkout get a proper error reply (issue #836). |
2547 | 2586 | {next_state, closed, Stripped, Replies}. |
2548 | 2587 |
|
2549 | 2588 | collect_h2_aborts(Err, #conn_data{h2_streams = Streams} = Data) -> |
|
0 commit comments