Event loops: served on by default, the data relayed by the loops

loopmode is cb unless a configuration says otherwise, as -L2 had it
for a service: its clients on event loops, a connection's data relayed
by the loop itself where nothing but the sockets is between its sides.
A service whose clients cannot share a thread (threadperclient: PAM,
LDAP, FilePlugin, names resolved by the system, -ne) or listening for
connections back (-R) still gives each a thread, as do the standalone
services and builds without fibers. loopmode on and -L keep the loops
with the relay on the client's stack; loopmode off and -L0 a thread
for each client.

The man page tells of cb. tests/run.py --loopmode takes the mode
(cb, on, off) to give every configuration, with two loops; the Linux
workflow runs the suite once more with a thread for each client, which
is no longer what a configuration gets by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vladimir Dubrovin 2026-10-28 11:49:51 +03:00
parent 143158e97f
commit 023eb8f830
8 changed files with 33 additions and 18 deletions

View File

@ -30,6 +30,8 @@ jobs:
run: make -f Makefile.Linux MAILPROXY=true FTP=true run: make -f Makefile.Linux MAILPROXY=true FTP=true
- name: regression tests - name: regression tests
run: python3 tests/run.py run: python3 tests/run.py
- name: regression tests, a thread for each client
run: python3 tests/run.py --loopmode off
- name: mkdir - name: mkdir
run: mkdir ~/3proxy run: mkdir ~/3proxy
- name: make install - name: make install

View File

@ -254,9 +254,10 @@ authorization and to drop the per-destination log records only.
(for udppm) singlepacket, expect only one packet from both client and server (for udppm) singlepacket, expect only one packet from both client and server
.br .br
.B -L .B -L
(for TCP services) serve the clients on event loops (see \fBloopmode\fR): a (for TCP services) how the clients are served, whatever \fBloopmode\fR says
few threads each serve many clients, instead of a thread for each. (see there): \fB-L2\fR on event loops, which also relay their data (the
\fB-L0\fR gives each client a thread of its own whatever \fBloopmode\fR says. default), \fB-L\fR on event loops relaying the data as a thread would,
\fB-L0\fR a thread for each client.
.br .br
.B -u .B -u
Never ask for username/password Never ask for username/password
@ -1526,16 +1527,21 @@ many connections were given to a waiting one on its Running Services page.
.br .br
.BR loopmode .BR loopmode
\fIon\fR|\fIoff\fR \fIcb\fR|\fIon\fR|\fIoff\fR
.br .br
Whether the clients of the TCP services which follow (until the next Whether the clients of the TCP services which follow (until the next
\fBloopmode\fR) are served on event loops: a few threads (see \fBloops\fR) \fBloopmode\fR) are served on event loops: a few threads (see \fBloops\fR)
each wait for the sockets and timers of many clients and switch between each wait for the sockets and timers of many clients and switch between
them, each client running on a stack of its own as large as its thread's them, each client running on a stack of its own as large as its thread's
would be. A client costs a stack and no thread, and waking it costs no would be. A client costs a stack and no thread, and waking it costs no
switch of threads, so many connections take less memory and CPU. Off by switch of threads, so many connections take less memory and CPU.
default; the \fB-L\fR option of a service overrides it, \fB-L0\fR for a \fIcb\fR, the default, also has the loop relay a connection's data itself
thread for each client. once the request is handled, where nothing but the sockets is between the
sides (no filters, no limits but that of a body's length), without
switching to the client's stack for each read. \fIon\fR relays the data
on the client's stack, as a thread of its own would, and \fIoff\fR gives
each client a thread. The \fB-L\fR option of a service overrides it: \fB-L2\fR,
\fB-L\fR, \fB-L0\fR.
.br .br
Clients keep a thread each, whatever is asked, where something they use Clients keep a thread each, whatever is asked, where something they use
would hold up the others sharing their thread: a service with FilePlugin, would hold up the others sharing their thread: a service with FilePlugin,

View File

@ -429,7 +429,7 @@ static int h_loops(int argc, unsigned char **argv){
static int h_loopmode(int argc, unsigned char **argv){ static int h_loopmode(int argc, unsigned char **argv){
if(!strcmp((char *)argv[1], "on")) conf->loopmode = 1; if(!strcmp((char *)argv[1], "on")) conf->loopmode = 1;
else if(!strcmp((char *)argv[1], "off")) conf->loopmode = 0; else if(!strcmp((char *)argv[1], "off")) conf->loopmode = 0;
/* and their data relayed by the loop (a draft) */ /* and their data relayed by the loop: the default */
else if(!strcmp((char *)argv[1], "cb")) conf->loopmode = 2; else if(!strcmp((char *)argv[1], "cb")) conf->loopmode = 2;
else return 1; else return 1;
return 0; return 0;

View File

@ -22,6 +22,8 @@
.trafcounter = NULL, .trafcounter = NULL,
.services = NULL, .services = NULL,
.stacksize = 0, .stacksize = 0,
/* clients served on event loops, their data relayed by the loops */
.loopmode = 2,
.poolmax = DEFAULT_POOLMAX, .poolmax = DEFAULT_POOLMAX,
.pooltimeout = DEFAULT_POOLTIMEOUT, .pooltimeout = DEFAULT_POOLTIMEOUT,
.counterd = -1, .counterd = -1,

View File

@ -480,7 +480,7 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){
srv->stacksize = atoi(argv[i]+2); srv->stacksize = atoi(argv[i]+2);
break; break;
case 'L': case 'L':
/* -L2: and their data relayed by the loop (a draft) */ /* -L2: and their data relayed by the loop (the default) */
srv->loopmode = argv[i][2]? atoi(argv[i]+2) : 1; srv->loopmode = argv[i][2]? atoi(argv[i]+2) : 1;
break; break;
case 'a': case 'a':

View File

@ -7,9 +7,10 @@
*/ */
/* Clients served on event loops: a few threads, a CPU each by default /* Clients served on event loops: a few threads, a CPU each by default
(loops), each running a loop, and each client of a service given -L (or (loops), each running a loop, and each client of a service (unless given
loopmode on) served by a fiber of one of them, with the same code as a -L0 or loopmode off) served by a fiber of one of them, with the same code
client with a thread of its own: its waits in sockpoll() wait in the loop. as a client with a thread of its own: its waits in sockpoll() wait in the
loop.
A service whose clients cannot share a thread (threadperclient), one A service whose clients cannot share a thread (threadperclient), one
listening for connections back (-R), all of its clients waiting for the listening for connections back (-R), all of its clients waiting for the
same socket, which a loop waits for once, or a build or processor without same socket, which a loop waits for once, or a build or processor without
@ -426,13 +427,13 @@ DONE:
} }
/* ---- sockmap() as callbacks (-L2, a draft) ------------------------------- */ /* ---- sockmap() as callbacks (-L2, loopmode cb) --------------------------- */
/* The data of a connection mapped by a fiber of a loop with neither filters, /* The data of a connection mapped by a fiber of a loop with neither filters,
limits but that of a body's length, nor anything but the socket functions of the system between the limits but that of a body's length, nor anything but the socket functions of the system between the
sides, is relayed by the loop itself: the sockets call it back, on the sides, is relayed by the loop itself: the sockets call it back, on the
loop's own stack, and the fiber, parked meanwhile, is resumed only once loop's own stack, and the fiber, parked meanwhile, is resumed only once
the mapping is over. No splice() yet: data is copied. */ the mapping is over. */
/* rounds of a read and a write each way at most in a callback, before what /* rounds of a read and a write each way at most in a callback, before what
is still ready waits for the next turn of the loop */ is still ready waits for the next turn of the loop */

View File

@ -1009,7 +1009,8 @@ struct extparam {
int filtermaxsize; int filtermaxsize;
int poolmax, pooltimeout; /* threadpool: idle workers kept, -1 automatic */ int poolmax, pooltimeout; /* threadpool: idle workers kept, -1 automatic */
int loops; /* threads serving clients on event loops, 0: a CPU each */ int loops; /* threads serving clients on event loops, 0: a CPU each */
int loopmode; /* services from here on serve clients on loops */ int loopmode; /* services from here on serve clients on loops: 1, and
relay their data there: 2 */
int gracetraf, gracenum, gracedelay; int gracetraf, gracenum, gracedelay;
int maxseg; int maxseg;
unsigned char **archiver; unsigned char **archiver;

View File

@ -58,11 +58,14 @@ def main():
help="keep the temporary directory") help="keep the temporary directory")
parser.add_argument("-v", "--verbose", action="store_true", parser.add_argument("-v", "--verbose", action="store_true",
help="print every check, not just the failures") help="print every check, not just the failures")
parser.add_argument("--loopmode", action="store_true", parser.add_argument("--loopmode", choices=("cb", "on", "off"),
help="serve the clients of every service on event loops") help="serve the clients of every service so (cb, the "
"default, and on: on two event loops; off: a thread "
"each)")
args = parser.parse_args() args = parser.parse_args()
if args.loopmode: if args.loopmode:
os.environ["THREEPROXY_TESTCONFIG"] = "loops 2\nloopmode on\n" os.environ["THREEPROXY_TESTCONFIG"] = \
"loops 2\nloopmode %s\n" % args.loopmode
binary = args.binary or os.environ.get("BIN") or default_binary() binary = args.binary or os.environ.get("BIN") or default_binary()
binary = os.path.abspath(binary) binary = os.path.abspath(binary)