From 70a941eb2b7ab47b669edabaa23370fe065219a4 Mon Sep 17 00:00:00 2001 From: Karthic Raghupathi Date: Wed, 24 Jun 2026 22:49:57 -0400 Subject: [PATCH 1/3] Size FastAGI listen backlog via kernel cap, not sysctl (#32) get_somaxconn() shelled out to `sysctl` and parsed OS-specific output to read the system somaxconn, branched on Linux vs Darwin, and raised NotImplementedError on every other platform -- so FastAGIServer could not even start on, e.g., Windows or BSD. Reading the value is unnecessary. listen() already caps the backlog to the live system maximum, so passing the largest value the call accepts yields exactly that maximum and still tracks an administrator's tuned-up somaxconn automatically. Set request_queue_size to INT_MAX (2**31 - 1) and let the kernel clamp it. INT_MAX rather than a fixed ceiling like 65535: on modern kernels somaxconn is a 32-bit value that can be tuned above 65535, so any fixed ceiling could undershoot; the kernel's own limit is the only safe cap. Verified that listen() clamps (not rejects) a large backlog on Linux and on macOS/BSD. Removes the platform/socket/subprocess imports (only get_somaxconn used them) and the Linux/macOS-only restriction from the README and AGENTS docs. The _LISTEN_BACKLOG constant carries the full rationale inline. Add tests: the server sets request_queue_size to INT_MAX (binding on an ephemeral port also proves listen() accepts it), and the sysctl helper is gone. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 2 +- CHANGELOG.md | 1 + README.md | 2 +- pystrix/agi/fastagi.py | 54 +++++++++++++++--------------------- tests/test_fastagi_server.py | 21 ++++++++++++++ 5 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 tests/test_fastagi_server.py diff --git a/AGENTS.md b/AGENTS.md index 864c6e3..f04e911 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ Each raw message parses into a generic `_Message`. The reader then looks up the ### FastAGI scaling -This fork's main feature is FastAGI throughput. `_ThreadedTCPServer` (`fastagi.py:52`) sizes `request_queue_size` from the system `SOMAXCONN` (read via `sysctl` on Linux and Darwin) instead of the small Python default, and sets `allow_reuse_address`. The listen backlog directly bounds how many simultaneous calls the server can accept under a surge. +This fork's main feature is FastAGI throughput. `_ThreadedTCPServer` (`fastagi.py`) sets `request_queue_size` to `_LISTEN_BACKLOG` (`INT_MAX`) instead of the small Python default, and sets `allow_reuse_address`. The listen backlog directly bounds how many simultaneous calls the server can accept under a surge. Rather than read the system limit, it passes the maximum and lets the kernel cap the backlog to the live `somaxconn` (`net.core.somaxconn` on Linux, `kern.ipc.somaxconn` on macOS/BSD). That tracks a tuned-up limit automatically, runs on every platform, and avoids the old `sysctl` subprocess that raised on non-Linux/macOS hosts. See the `_LISTEN_BACKLOG` comment for the full rationale. ## Conventions for extending the library diff --git a/CHANGELOG.md b/CHANGELOG.md index dc12401..ef5ec64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `Manager.monitor_connection` no longer crashes its monitoring thread when the connection drops. Pinging a downed connection raised `ManagerSocketError` (broken socket) or `ManagerError` (the liveness check inside `send_action` failing when the connection dropped just after the loop's own check) inside the thread, which dumped a traceback to stderr and killed the monitor. The monitor now catches both and stops cleanly, logging the reason at debug level when a logger is set. The method also returns the monitoring thread so callers can join it (#3). - `Manager.send_action` no longer raises a raw `AttributeError` when a concurrent `disconnect()` clears the connection between the liveness check and the send. It now drops the just-registered request and raises `ManagerSocketError` instead (#3). - The FastAGI server no longer prints an unhandled traceback to stderr when a client disconnects during the AGI environment handshake. A caller hanging up, Asterisk aborting the leg, or a bare TCP probe raised `AGISIGPIPEHangup` from the handler and printed a full traceback for a routine event. The handler now ends the request quietly. Errors raised by the script handler itself still propagate (#49). +- The FastAGI server sizes its listen backlog without shelling out to `sysctl`. It now requests the maximum backlog and lets the kernel cap it to the live system `somaxconn`, which still tracks a tuned-up limit automatically. This also lifts the platform restriction added in 1.3.0: the server previously raised `NotImplementedError` on any host that was neither Linux nor macOS, and now runs everywhere (#32). ## [1.3.0] - 2026-06-24 diff --git a/README.md b/README.md index 5c839c2..70b0775 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ server.register_script_handler(None, demo_handler) # default handler server.serve_forever() ``` -The FastAGI server sizes its listen backlog from the system `SOMAXCONN` value, so it absorbs large bursts of simultaneous calls. It reads that value with `sysctl`, so the server currently runs on Linux and macOS only. AMI and AGI have no such restriction. +The FastAGI server requests the largest listen backlog the socket layer accepts and lets the kernel cap it to the host's configured maximum (`net.core.somaxconn` on Linux, `kern.ipc.somaxconn` on macOS and the BSDs), so it absorbs large bursts of simultaneous calls and automatically tracks a tuned-up limit. This needs no configuration and runs on any platform. ### AMI — control and monitor the server diff --git a/pystrix/agi/fastagi.py b/pystrix/agi/fastagi.py index 8758c99..360b819 100644 --- a/pystrix/agi/fastagi.py +++ b/pystrix/agi/fastagi.py @@ -36,16 +36,33 @@ - Neil Tallim """ -import platform -import socket import socketserver -import subprocess import threading from urllib.parse import parse_qs from pystrix.agi.agi_core import * from pystrix.agi.agi_core import _AGI +# The listen backlog bounds how many connections may queue while the server is +# busy, which directly limits the call surge a FastAGI server can absorb. We +# want the largest backlog the kernel will allow on the host. +# +# We do NOT read that limit ourselves. The kernel already enforces it: listen() +# silently caps the backlog to the live system maximum -- net.core.somaxconn on +# Linux, kern.ipc.somaxconn on macOS and the BSDs -- so passing a value at or +# above that maximum yields exactly the system maximum. Passing the largest +# value the call accepts therefore tracks a tuned-up maximum automatically, with +# no per-call lookup. +# +# This is why we do not use socket.SOMAXCONN (a small compile-time constant, +# historically 128 and 4096 since Linux 5.4) and do not hardcode a ceiling like +# 65535: on modern kernels somaxconn is a 32-bit value that an administrator can +# tune above 65535, so any fixed ceiling could undershoot. INT_MAX cannot, since +# the kernel's own limit is then the only cap. It also replaces the previous +# approach of shelling out to `sysctl`, which ran a subprocess, parsed +# OS-specific output, and raised on any system that was neither Linux nor macOS. +_LISTEN_BACKLOG = 2**31 - 1 # INT_MAX; the kernel caps this to the live somaxconn + class _ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): """ @@ -53,35 +70,10 @@ class _ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): request. """ - @staticmethod - def get_somaxconn(): - """ - Returns the value of SOMAXCONN configured in the system. - """ - # determine the OS appropriate management informations base (MIB) - # name to determine SOMAXCONN - system = platform.system() - if "Linux" == system: - sysctl_mib_somaxconn = "net.core.somaxconn" - sysctl_output_delimiter = "=" - elif "Darwin" == system: - sysctl_mib_somaxconn = "kern.ipc.somaxconn" - sysctl_output_delimiter = ":" - else: - raise NotImplementedError( - "Determining SOMAXCONN is not implemented for {} system.".format(system) - ) - # run the cmd to determine the SOMAXCONN - cmd_result = subprocess.check_output(["sysctl", sysctl_mib_somaxconn]) - - # parse the output of the cmd to return the value of SOMAXCONN - return int(cmd_result.decode().split(sysctl_output_delimiter)[-1].strip()) - def __init__(self, *args, **kwargs): - # adjust request queue size to a saner value for modern systems - # further adjustments are automatically picked up for kernel - # settings on server start - self.request_queue_size = max(socket.SOMAXCONN, self.get_somaxconn()) + # Request the maximum backlog and let the kernel cap it to the live + # system somaxconn; see _LISTEN_BACKLOG for the full rationale. + self.request_queue_size = _LISTEN_BACKLOG self.allow_reuse_address = True super().__init__(*args, **kwargs) diff --git a/tests/test_fastagi_server.py b/tests/test_fastagi_server.py new file mode 100644 index 0000000..2d43611 --- /dev/null +++ b/tests/test_fastagi_server.py @@ -0,0 +1,21 @@ +"""Tests for FastAGIServer listen-backlog configuration.""" + +from pystrix.agi.fastagi import FastAGIServer, _ThreadedTCPServer + + +def test_server_requests_max_listen_backlog(): + # Regression for #32: the server asks for the largest backlog the socket + # call accepts and lets the kernel cap it to the live somaxconn. Binding on + # an ephemeral port also proves the platform's listen() accepts the value + # rather than rejecting it. + server = FastAGIServer(interface="127.0.0.1", port=0) + try: + assert server.request_queue_size == 2**31 - 1 + finally: + server.server_close() + + +def test_backlog_sizing_does_not_shell_out(): + # The sysctl-spawning, OS-branching helper is gone; backlog sizing no longer + # shells out or raises on platforms without sysctl (regression for #32). + assert not hasattr(_ThreadedTCPServer, "get_somaxconn") From deff8007322288721e1b573930b1100b2f3cb24f Mon Sep 17 00:00:00 2001 From: Karthic Raghupathi Date: Wed, 24 Jun 2026 22:55:16 -0400 Subject: [PATCH 2/3] Document INT_MAX ceiling and Windows path; tighten backlog tests (#32 review) Review panel convergence (all comment/test polish, no behavior change): - Extend the _LISTEN_BACKLOG comment with two facts the edge-case lane verified: the value must be exactly INT_MAX because CPython raises OverflowError for a listen() backlog above a signed 32-bit int (so a future "round up" would crash startup), and Windows accepts INT_MAX not via a somaxconn clamp but because Winsock's SOMAXCONN constant is itself 0x7fffffff, a sentinel meaning "use a maximum reasonable backlog". - Test: import and assert against _LISTEN_BACKLOG instead of re-hardcoding the literal, and assert it exceeds 65535 so the "no fixed ceiling" rationale is executable. - Test: also assert the module no longer imports subprocess/platform, a behavioral guard against reintroducing the shell-out that survives a rename of the old helper. Co-Authored-By: Claude Opus 4.8 --- pystrix/agi/fastagi.py | 10 ++++++++++ tests/test_fastagi_server.py | 15 +++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/pystrix/agi/fastagi.py b/pystrix/agi/fastagi.py index 360b819..c98e808 100644 --- a/pystrix/agi/fastagi.py +++ b/pystrix/agi/fastagi.py @@ -61,6 +61,16 @@ # the kernel's own limit is then the only cap. It also replaces the previous # approach of shelling out to `sysctl`, which ran a subprocess, parsed # OS-specific output, and raised on any system that was neither Linux nor macOS. +# +# The value must be exactly INT_MAX (2**31 - 1), not larger: CPython parses the +# listen() backlog into a C int and raises OverflowError above INT_MAX, which +# would crash server startup. Do not "round up" this constant. +# +# Windows works too, but by a different route: it does not clamp to a system +# somaxconn. Winsock's own SOMAXCONN constant is 0x7fffffff (= INT_MAX), and +# Winsock treats that exact value as a sentinel meaning "use a maximum +# reasonable backlog". So INT_MAX is the right value on every platform, though +# Windows honors it as that sentinel rather than as a tuned registry limit. _LISTEN_BACKLOG = 2**31 - 1 # INT_MAX; the kernel caps this to the live somaxconn diff --git a/tests/test_fastagi_server.py b/tests/test_fastagi_server.py index 2d43611..781af85 100644 --- a/tests/test_fastagi_server.py +++ b/tests/test_fastagi_server.py @@ -1,6 +1,7 @@ """Tests for FastAGIServer listen-backlog configuration.""" -from pystrix.agi.fastagi import FastAGIServer, _ThreadedTCPServer +from pystrix.agi import fastagi +from pystrix.agi.fastagi import _LISTEN_BACKLOG, FastAGIServer, _ThreadedTCPServer def test_server_requests_max_listen_backlog(): @@ -10,12 +11,18 @@ def test_server_requests_max_listen_backlog(): # rather than rejecting it. server = FastAGIServer(interface="127.0.0.1", port=0) try: - assert server.request_queue_size == 2**31 - 1 + assert server.request_queue_size == _LISTEN_BACKLOG + # A fixed ceiling like 65535 could undershoot a tuned-up somaxconn; the + # backlog must exceed it so the kernel's own limit is the only cap. + assert _LISTEN_BACKLOG > 65535 finally: server.server_close() def test_backlog_sizing_does_not_shell_out(): - # The sysctl-spawning, OS-branching helper is gone; backlog sizing no longer - # shells out or raises on platforms without sysctl (regression for #32). + # The sysctl-spawning, OS-branching helper is gone and the module no longer + # imports the subprocess/platform machinery it used, so backlog sizing cannot + # shell out or raise on platforms without sysctl (regression for #32). assert not hasattr(_ThreadedTCPServer, "get_somaxconn") + assert not hasattr(fastagi, "subprocess") + assert not hasattr(fastagi, "platform") From 5b30f5e0029e7393db780a2be05822b6080fb6ca Mon Sep 17 00:00:00 2001 From: Karthic Raghupathi Date: Wed, 24 Jun 2026 23:10:39 -0400 Subject: [PATCH 3/3] Lock exact INT_MAX backlog in test; scope clamp wording to Unix (#32 review) Review feedback: - The regression test asserted only `_LISTEN_BACKLOG > 65535`, which a future fixed ceiling like 1000000 would pass while violating the documented exactly-INT_MAX contract. Assert `_LISTEN_BACKLOG == 2**31 - 1` instead. - The "kernel caps to live somaxconn" wording read as universal but only holds on Unix-like systems; Windows accepts INT_MAX via the Winsock SOMAXCONN sentinel, not a somaxconn clamp. Scope the wording accordingly in the inline comment, README, CHANGELOG, and AGENTS.md so it no longer contradicts the block comment's Windows note. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 2 +- CHANGELOG.md | 2 +- README.md | 2 +- pystrix/agi/fastagi.py | 2 +- tests/test_fastagi_server.py | 7 ++++--- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f04e911..5a5e016 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ Each raw message parses into a generic `_Message`. The reader then looks up the ### FastAGI scaling -This fork's main feature is FastAGI throughput. `_ThreadedTCPServer` (`fastagi.py`) sets `request_queue_size` to `_LISTEN_BACKLOG` (`INT_MAX`) instead of the small Python default, and sets `allow_reuse_address`. The listen backlog directly bounds how many simultaneous calls the server can accept under a surge. Rather than read the system limit, it passes the maximum and lets the kernel cap the backlog to the live `somaxconn` (`net.core.somaxconn` on Linux, `kern.ipc.somaxconn` on macOS/BSD). That tracks a tuned-up limit automatically, runs on every platform, and avoids the old `sysctl` subprocess that raised on non-Linux/macOS hosts. See the `_LISTEN_BACKLOG` comment for the full rationale. +This fork's main feature is FastAGI throughput. `_ThreadedTCPServer` (`fastagi.py`) sets `request_queue_size` to `_LISTEN_BACKLOG` (`INT_MAX`) instead of the small Python default, and sets `allow_reuse_address`. The listen backlog directly bounds how many simultaneous calls the server can accept under a surge. Rather than read the system limit, it passes the maximum and lets the OS cap the backlog. Unix-like kernels clamp it to the live `somaxconn` (`net.core.somaxconn` on Linux, `kern.ipc.somaxconn` on macOS/BSD), which tracks a tuned-up limit automatically; Windows applies the Winsock maximum (its `SOMAXCONN` is itself `INT_MAX`). This runs on every platform and avoids the old `sysctl` subprocess that raised on non-Linux/macOS hosts. See the `_LISTEN_BACKLOG` comment for the full rationale. ## Conventions for extending the library diff --git a/CHANGELOG.md b/CHANGELOG.md index ef5ec64..0c0fd4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `Manager.monitor_connection` no longer crashes its monitoring thread when the connection drops. Pinging a downed connection raised `ManagerSocketError` (broken socket) or `ManagerError` (the liveness check inside `send_action` failing when the connection dropped just after the loop's own check) inside the thread, which dumped a traceback to stderr and killed the monitor. The monitor now catches both and stops cleanly, logging the reason at debug level when a logger is set. The method also returns the monitoring thread so callers can join it (#3). - `Manager.send_action` no longer raises a raw `AttributeError` when a concurrent `disconnect()` clears the connection between the liveness check and the send. It now drops the just-registered request and raises `ManagerSocketError` instead (#3). - The FastAGI server no longer prints an unhandled traceback to stderr when a client disconnects during the AGI environment handshake. A caller hanging up, Asterisk aborting the leg, or a bare TCP probe raised `AGISIGPIPEHangup` from the handler and printed a full traceback for a routine event. The handler now ends the request quietly. Errors raised by the script handler itself still propagate (#49). -- The FastAGI server sizes its listen backlog without shelling out to `sysctl`. It now requests the maximum backlog and lets the kernel cap it to the live system `somaxconn`, which still tracks a tuned-up limit automatically. This also lifts the platform restriction added in 1.3.0: the server previously raised `NotImplementedError` on any host that was neither Linux nor macOS, and now runs everywhere (#32). +- The FastAGI server sizes its listen backlog without shelling out to `sysctl`. It now requests the maximum backlog and lets the operating system cap it: Unix-like kernels clamp it to the live `somaxconn`, which still tracks a tuned-up limit automatically, and Windows applies the Winsock maximum. This also lifts the platform restriction added in 1.3.0: the server previously raised `NotImplementedError` on any host that was neither Linux nor macOS, and now runs everywhere (#32). ## [1.3.0] - 2026-06-24 diff --git a/README.md b/README.md index 70b0775..877cd78 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ server.register_script_handler(None, demo_handler) # default handler server.serve_forever() ``` -The FastAGI server requests the largest listen backlog the socket layer accepts and lets the kernel cap it to the host's configured maximum (`net.core.somaxconn` on Linux, `kern.ipc.somaxconn` on macOS and the BSDs), so it absorbs large bursts of simultaneous calls and automatically tracks a tuned-up limit. This needs no configuration and runs on any platform. +The FastAGI server requests the largest listen backlog the socket layer accepts and lets the operating system cap it, so it absorbs large bursts of simultaneous calls. On Unix-like systems the kernel clamps it to the host's configured maximum (`net.core.somaxconn` on Linux, `kern.ipc.somaxconn` on macOS and the BSDs), so it automatically tracks a tuned-up limit; on Windows, Winsock applies its own maximum. This needs no configuration and runs on any platform. ### AMI — control and monitor the server diff --git a/pystrix/agi/fastagi.py b/pystrix/agi/fastagi.py index c98e808..45870ef 100644 --- a/pystrix/agi/fastagi.py +++ b/pystrix/agi/fastagi.py @@ -71,7 +71,7 @@ # Winsock treats that exact value as a sentinel meaning "use a maximum # reasonable backlog". So INT_MAX is the right value on every platform, though # Windows honors it as that sentinel rather than as a tuned registry limit. -_LISTEN_BACKLOG = 2**31 - 1 # INT_MAX; the kernel caps this to the live somaxconn +_LISTEN_BACKLOG = 2**31 - 1 # INT_MAX; Unix kernels cap this to the live somaxconn class _ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): diff --git a/tests/test_fastagi_server.py b/tests/test_fastagi_server.py index 781af85..3f77c02 100644 --- a/tests/test_fastagi_server.py +++ b/tests/test_fastagi_server.py @@ -12,9 +12,10 @@ def test_server_requests_max_listen_backlog(): server = FastAGIServer(interface="127.0.0.1", port=0) try: assert server.request_queue_size == _LISTEN_BACKLOG - # A fixed ceiling like 65535 could undershoot a tuned-up somaxconn; the - # backlog must exceed it so the kernel's own limit is the only cap. - assert _LISTEN_BACKLOG > 65535 + # Lock the exact contract: the backlog must be INT_MAX, the largest value + # CPython's listen() accepts (2**31 raises OverflowError). A smaller fixed + # ceiling would also undershoot a tuned-up somaxconn. + assert _LISTEN_BACKLOG == 2**31 - 1 finally: server.server_close()