Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

Expand Down
64 changes: 33 additions & 31 deletions pystrix/agi/fastagi.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,52 +36,54 @@
- Neil Tallim <n.tallim@ivrnet.com>
"""

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.
#
# 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; Unix kernels cap this to the live somaxconn


class _ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
"""
Provides a variant of the TCPServer that spawns a new thread to handle each
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)

Expand Down
29 changes: 29 additions & 0 deletions tests/test_fastagi_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Tests for FastAGIServer listen-backlog configuration."""

from pystrix.agi import fastagi
from pystrix.agi.fastagi import _LISTEN_BACKLOG, 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 == _LISTEN_BACKLOG
# 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()


def test_backlog_sizing_does_not_shell_out():
# 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")
Loading