Stop dead-end connections instead of parking them in closed (#918)#919
Open
lambadalambda wants to merge 1 commit into
Open
Stop dead-end connections instead of parking them in closed (#918)#919lambadalambda wants to merge 1 commit into
closed (#918)#919lambadalambda wants to merge 1 commit into
Conversation
…c#918) A body cut short by the peer closing mid-transfer leaves read_full_body/2 with `socket = undefined`, so `receiving` went straight to `closed` and never reached the reuse check added for benoitc#902. Nothing reaped it there: `closed(enter)` only arms the benoitc#836 grace timer for pooled connections, and a connection started under hackney_conn_sup has the supervisor as its `owner`, so the owner-DOWN clause never fires either. The process parked forever holding every refc binary it had read. Callers cannot clean up: a synchronous request returns the body directly rather than a handle, and the truncated read still reports {ok, Body}. Add finish_dead_request/3 for the completions that cannot continue the connection - truncated body, bodyless (204/304) response, and a failed body read - in both the body and stream_body clauses. A pooled conn still parks in `closed` so the grace window answers late calls; an unpooled one closes its socket and stops. Also make get_location/1 and set_location/2 noproc-safe. Now that a dead-end connection stops rather than parks, the set_location/2 call hackney:request/5 makes on the original connection after a redirect chain can hit a stopped process, and the bare gen_statem:call exit propagated out to the caller.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #918. Sibling of #902, on the path
finish_sync_request/3does not cover.Problem
When the peer closes mid-body,
read_full_body/2returns withsocket = undefined, soreceiving({call, From}, body, ...)went straight toclosedand never reached the reuse check added by #903:{ok, Body, #conn_data{socket = undefined} = NewData} -> {next_state, closed, NewData, [{reply, From, {ok, Body}}]};Nothing reaps it there.
closed(enter)only arms the #836 grace timer whenpool_pid =/= undefined, and a connection started throughhackney_conn_sup:start_conn/1has the supervisor as itsowner(start_link/1capturesself(), which under asimple_one_for_onesupervisor is the supervisor;connect_direct/4andstart_conn_with_socket_internal/5pass noowner, unlikehackney_pool:start_connection/6). So the owner-DOWNclause is dead code for these, and the process parks forever holding every refc binary it read.Callers cannot clean up: a synchronous request returns the body directly rather than a handle, and the truncated read still reports
{ok, Body}— a success.Real-world trigger: a reverse proxy aborting a large response mid-stream, fetched through a CONNECT proxy (tunnels are
no_reuseand never pooled). Measured one leaked process per fetch, ~8.6 MB each.Fix
finish_dead_request/3, for the completions that cannot continue the connection — truncated body, bodyless (204/304) response, and a failed body read — in both thebodyandstream_bodyclauses:A pooled connection still parks in
closed, keeping the #836 grace window so late calls get a proper reply. An unpooled one closes its socket and stops. The 204/304 path also benefits:read_full_body/2drops the socket handle without closing it, so parking leaked the fd as well.I deliberately key on
pool_pidalone rather thanconnection_reusable/1. Both branches here terminate — parking is "stop in 50 ms with a grace window", not reuse — so bringing reuse into it would only remove that grace from pooled non-reusable conns.finish_sync_request/3usesconnection_reusable/1because there parking really does mean reuse; the two functions answer different questions.Also included
get_location/1andset_location/2now usesafe_call/2.hackney:request/5callsset_location/2on the original connection after a redirect chain returns; with a dead-end connection now stopping rather than parking, that pid can be gone and the baregen_statem:callexitednoprocstraight out ofhackney:request/5. Reproduced withpool=false,follow_redirect=true, and a 302 carrying a lyingContent-Length:{'EXIT',{noproc,{gen_statem,call,[<0.587.0>,{set_location,...}]}}}{ok,200,[{<<"Content-Length">>,<<"2">>}],<<"hi">>}That exit is reachable on 4.7.2 today via the
Connection: closevariant, so this is worth having regardless.Scope
This fixes one entry point into
closed; it is not the whole class. Still leaking for an unpooled connection, and out of scope here:connected(info, {tcp_closed|ssl_closed|tcp_error|ssl_error, Socket}, ...)— an idle conn whose peer closes.{pool, false}request, which parks inconnectedwith nothing to stop it.The structural fix for all of them is probably to pass
owner => self()fromconnect_direct/4andstart_conn_with_socket_internal/5, ashackney_pool:start_connection/6already does, so the owner-DOWNclauses become live for direct connections. That is a wider behaviour change (it would stop a connection whose creating process exits, whichhackney:connect/4users may rely on not happening), so I left it out — happy to follow up if you want it.Tests
rebar3 eunit: all 1048 pass. Four new tests inhackney_conn_tests:body/1on an unpooled conn stops it, asserting exit reasonnormalstream_body/1closed(guards the grace window)get_location/1/set_location/2are noproc-safeThe first fails on stock 4.7.2 (
wait_downnever fires). End-to-end, the redirect probe above leaves 2 connection processes on stock and 1 with this patch — the remaining one being the separate{pool, false}case noted above.