A connection attached with attach-srv is removed from the stopping list when it is reversed, and the soft-stop GOAWAY is only sent on frontend connections, so a stopping process never tells a reverse HTTP peer to leave. The connection is not a job either, so the process keeps it open until its last client stream ends. The peer sees no reason to redial, while the new process has no connection to it: requests to that server get a 503 for as long as the old process lingers, which with long-running requests and no hard-stop-after can be minutes.
Once the backends are stopped in do_soft_stop_now(), release the idle connections of every rhttp server, and refuse a reversed connection back into the idle list of a stopped backend, so one still carrying streams is closed as soon as its last stream ends. Each closure prompts the peer to redial into the new process. Gating on the stopped backend rather than on "stopping" keeps reverse connections usable during a "grace" period, while this process still serves. This only helps connections that are idle at the reload. One that carries a stream is still kept until its streams end, and the peer is not told to replace it meanwhile, so a pool whose connections are all busy is not improved. That needs a GOAWAY on busy reverse connections. Measured with pool-purge-delay 3600s and one 8s or 20s request in flight across a reload: requests to the server are served again after the peer's reconnect delay (1.1-1.2s for a peer with a 1.0-1.5s backoff, 0.44-0.56s for one that redials after 0.5s), against the whole remaining life of the in-flight request without this patch. The in-flight request still completes. With 16 concurrent requests over 4 connections, nothing is idle and the result is unchanged. This partially addresses GitHub issue #3556. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- Notes: This is the partial fix described in GitHub issue #3556, which has the reproduction (oktalz/reverse-http behind its recommended configuration) and the measurements. It applies to master at 92f34653b. On that tree: one 8s request in flight unpatched: 503 on 70/71 polls until it ends patched: served again 1.1s after the reload 16 concurrent 8s requests patched: 503 on 69/70 polls (no idle connection) one 8s request, grace 3s patched: served again 4.4s after the reload The busy case is the one that matters for long-running work: every connection carries a stream, so nothing here is closed. Sending a GOAWAY on busy reverse connections at soft-stop would let the peer open replacements while those streams finish. I did not do that for idle ones because they can be taken over by another thread while the stopping list is per thread and unlocked. Busy ones are not taken over, so that may be workable, and I would welcome guidance on the right place for it. include/haproxy/server.h | 1 + src/proxy.c | 15 +++++++++++++++ src/server.c | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/include/haproxy/server.h b/include/haproxy/server.h index 23d0c0d72..b299cc1bf 100644 --- a/include/haproxy/server.h +++ b/include/haproxy/server.h @@ -112,6 +112,7 @@ int snr_resolution_error_cb(struct resolv_requester *requester, int error_code); struct server *snr_check_ip_callback(struct server *srv, void *ip, unsigned char *ip_family); struct task *srv_cleanup_idle_conns(struct task *task, void *ctx, unsigned int state); void srv_release_conn(struct server *srv, struct connection *conn); +void srv_cleanup_connections(struct server *srv); struct connection *srv_lookup_conn(struct ceb_root **tree, uint64_t hash); struct connection *srv_lookup_conn_next(struct ceb_root **tree, struct connection *conn); diff --git a/src/proxy.c b/src/proxy.c index 2601d8b29..39a923dd7 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -58,6 +58,7 @@ #include <haproxy/quic_tp.h> #include <haproxy/quic_tune.h> #include <haproxy/server-t.h> +#include <haproxy/server.h> #include <haproxy/signal.h> #include <haproxy/stats.h> #include <haproxy/stconn.h> @@ -4154,6 +4155,20 @@ static void do_soft_stop_now() HA_RWLOCK_WRUNLOCK(PROXY_LOCK, &p->lock); } + /* Reverse HTTP peers only redial once their idle connection to this + * stopping process is closed, and nothing else would close it before + * the process exits. Busy ones are closed on release instead (see + * srv_add_to_idle_list()). + */ + list_for_each_entry(p, &main_proxies, el) { + struct server *srv; + + list_for_each_entry(srv, &p->servers, el_px) { + if (srv->flags & SRV_F_RHTTP) + srv_cleanup_connections(srv); + } + } + /* signal zero is used to broadcast the "stopping" event */ signal_handler(0); } diff --git a/src/server.c b/src/server.c index 50c7771ce..6022eb049 100644 --- a/src/server.c +++ b/src/server.c @@ -61,7 +61,6 @@ static void srv_update_status(struct server *s, int type, int cause); static int srv_apply_lastaddr(struct server *srv, int *err_code); -static void srv_cleanup_connections(struct server *srv); /* extra keywords used as value for other arguments. They are used as * suggestions for mistyped words. @@ -8082,7 +8081,7 @@ static int srv_migrate_conns_to_remove(struct server *srv, int thr, int toremove /* cleanup connections for a given server * might be useful when going on forced maintenance or live changing ip/port */ -static void srv_cleanup_connections(struct server *srv) +void srv_cleanup_connections(struct server *srv) { struct sess_priv_conns *sess_conns; int did_remove; @@ -8221,6 +8220,7 @@ int srv_add_to_idle_list(struct server *srv, struct connection *conn, int is_saf */ if (!(conn->flags & CO_FL_PRIVATE) && srv && srv->pool_purge_delay > 0 && + !((conn->flags & CO_FL_REVERSED) && (srv->proxy->flags & PR_FL_STOPPED)) && !(srv->cur_admin & SRV_ADMF_MAINT) && ((srv->proxy->options & PR_O_REUSE_MASK) != PR_O_REUSE_NEVR) && ha_used_fds < global.tune.pool_high_count && -- 2.54.0 (Apple Git-157)

