From 8cc7d255afba6f8358fe0b312ced82ea50665549 Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Mon, 19 Oct 2026 16:22:17 +0300 Subject: [PATCH] connect back: a replaced -r service stops waiting at the reload A -r service waited up to 60 s for the go-ahead on the connection it made, without looking at a reload. One replaced by a reload went on waiting, holding a slot and a thread, and took a client if the far side's go-ahead came in that time; every reload left another such connection in the far side's queue. The wait is now a second at a time (60 s in all, as before): at a reload the service takes a go-ahead already there, its client going on in the service which replaced it, and otherwise closes the connection and stops. The far side (-R) sent its go-ahead to any connection it accepted, and a closed one cost it a client. A connection the -r side closed, at a reload or after its wait, is readable before any go-ahead, since that side sends nothing first: once past the ACL it is passed over, without counting as one of the tries for as many as the listen queue holds (1 + maxchild/16), so that connections made to be closed or to send something cannot keep a client's thread in the loop. Tests: reattach's far side passes over closed connections as -R does. Co-Authored-By: Claude Opus 5.5 --- src/mainclient.c | 20 ++++++++++++++++++++ src/maintcp.c | 18 ++++++++++++++++-- tests/cases/reattach.py | 9 ++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/mainclient.c b/src/mainclient.c index a23849c..e18f7de 100644 --- a/src/mainclient.c +++ b/src/mainclient.c @@ -41,6 +41,8 @@ static void serveparam(struct clientparam *param){ if(param->srv->cbsock != INVALID_SOCKET){ SASIZETYPE size = sizeof(param->sinsr); struct pollfd fds; + /* closed connections passed over free: the listen queue's length */ + int skips = 1 + (param->srv->maxchild>>4); fds.fd = param->srv->cbsock; fds.events = POLLIN; fds.revents = 0; @@ -73,6 +75,24 @@ static void serveparam(struct clientparam *param){ continue; } #endif + { + /* the side connecting back sends nothing before the go-ahead: + readable, the connection was closed there, by a service + which stopped waiting (a reload, its time out); the next + one is taken, and this one does not count as a try, for as + many as the queue holds: past that it does */ + struct pollfd rf; + + rf.fd = param->remsock; + rf.events = POLLIN; + rf.revents = 0; + if(param->srv->so._poll(param->sostate, &rf, 1, 0) == 1){ + param->srv->so._closesocket(param->sostate, param->remsock); + param->remsock = INVALID_SOCKET; + if(skips > 0){ skips--; i++; } + continue; + } + } if(socksend(param, param->remsock, (unsigned char *)"C", 1, CONNBACK_TO) != 1){ dolog(param, (unsigned char *)"Connect back sending command failed"); param->srv->so._closesocket(param->sostate, param->remsock); diff --git a/src/maintcp.c b/src/maintcp.c index a5bf1f9..336bc71 100644 --- a/src/maintcp.c +++ b/src/maintcp.c @@ -37,6 +37,9 @@ static int cblisten(struct srvstart *s){ #endif +/* How long, in seconds, a connection made back (-r) waits for its go-ahead */ +#define CBWAIT 60 + /* What getting the next client came to. */ enum { NEXT_CLIENT, NEXT_RETRY, NEXT_STOP }; @@ -49,7 +52,7 @@ static int tcpnext(struct srvstart *s, SOCKET *client, int *nlog){ SOCKET new_sock; SASIZETYPE size; unsigned char buf[256]; - int error = 0, opt = 1; + int error = 0, opt = 1, res, waited; #ifndef _WIN32 struct linger lg; #else @@ -81,7 +84,18 @@ static int tcpnext(struct srvstart *s, SOCKET *client, int *nlog){ return NEXT_RETRY; } - if(sockrecvfrom(NULL, new_sock, NULL, buf, 1, 60*1000) != 1 || *buf!='C') { + /* the far side's go-ahead, waited for a second at a time to notice + a reload: a service paused then takes no more connections and + closes this one, unless the go-ahead came already, and its client + goes on in the service which took its place */ + for(waited = 0; !(res = sockrecvfrom(NULL, new_sock, NULL, buf, 1, 1000)); ){ + if(srv->config->paused != srv->paused){ + res = sockrecvfrom(NULL, new_sock, NULL, buf, 1, 0); + break; + } + if(++waited >= CBWAIT) break; + } + if(res != 1 || *buf!='C') { so._closesocket(so.state, new_sock); usleep(SLEEPTIME); return NEXT_RETRY; diff --git a/tests/cases/reattach.py b/tests/cases/reattach.py index c340aa1..903a120 100644 --- a/tests/cases/reattach.py +++ b/tests/cases/reattach.py @@ -3,6 +3,7 @@ the rules of the newest service on its listener, taking one of its maxconn slots, and is dropped where that cannot be.""" import base64 +import select import socket import threading import time @@ -128,7 +129,13 @@ def run(t): """A request over a connection the -r service made: the peer is its client once it has sent C.""" try: - c, _ = peer.accept() + # as the far side of a connect-back service does: a connection + # closed by a service which stopped waiting is passed over + while True: + c, _ = peer.accept() + if not select.select([c], [], [], 0)[0]: + break + c.close() c.settimeout(30) c.sendall(b"C") auth = base64.b64encode(b"alice:one").decode()