Skip to content

Commit b48cdba

Browse files
committed
Remove max_requests feature from server
This feature (inherited from gunicorn) auto-restarted workers after N requests to mitigate memory leaks. It was disabled by default and not actively used. Removing it eliminates scattered conditional logic, jitter calculations, and request counting from the worker and H2 handler hot paths.
1 parent 084a5d9 commit b48cdba

File tree

6 files changed

+13
-71
lines changed

6 files changed

+13
-71
lines changed

plain/plain/cli/server.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,6 @@
5959
setting="SERVER_ACCESS_LOG",
6060
help="Enable/disable access logging to stdout",
6161
)
62-
@click.option(
63-
"--max-requests",
64-
type=int,
65-
cls=SettingOption,
66-
setting="SERVER_MAX_REQUESTS",
67-
help="Max requests before worker restart (0=disabled)",
68-
)
6962
@click.option(
7063
"--pidfile",
7164
type=click.Path(),
@@ -80,7 +73,6 @@ def server(
8073
keyfile: str | None,
8174
reload: bool,
8275
access_log: bool,
83-
max_requests: int,
8476
pidfile: str | None,
8577
) -> None:
8678
"""Production-ready HTTP server"""
@@ -105,7 +97,6 @@ def server(
10597
threads=threads,
10698
workers=workers,
10799
timeout=timeout,
108-
max_requests=max_requests,
109100
reload=reload,
110101
pidfile=pidfile,
111102
certfile=certfile,

plain/plain/runtime/global_settings.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@
177177
SERVER_WORKERS: int = int(os.environ.get("WEB_CONCURRENCY", 0)) # 0 = auto (CPU count)
178178
SERVER_THREADS: int = 4
179179
SERVER_TIMEOUT: int = 30
180-
SERVER_MAX_REQUESTS: int = 0
181180
SERVER_ACCESS_LOG: bool = True
182181
SERVER_ACCESS_LOG_FIELDS: list[str] = [
183182
"method",

plain/plain/server/README.md

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,17 @@ All options are available via the command line. Run `plain server --help` to see
5959

6060
Most options can also be configured via settings (see below). CLI arguments take priority over settings.
6161

62-
| Option | Setting | Description |
63-
| -------------------------------- | --------------------- | ------------------------------------ |
64-
| `--bind` / `-b` | - | Address to bind (can repeat) |
65-
| `--workers` / `-w` | `SERVER_WORKERS` | Worker processes (0=auto, CPU count) |
66-
| `--threads` | `SERVER_THREADS` | Threads per worker |
67-
| `--timeout` / `-t` | `SERVER_TIMEOUT` | Worker timeout in seconds |
68-
| `--access-log / --no-access-log` | `SERVER_ACCESS_LOG` | Enable/disable access logging |
69-
| `--max-requests` | `SERVER_MAX_REQUESTS` | Max requests before worker restart |
70-
| `--reload` | - | Restart workers on code changes |
71-
| `--certfile` | - | Path to SSL certificate file |
72-
| `--keyfile` | - | Path to SSL key file |
73-
| `--pidfile` | - | PID file path |
62+
| Option | Setting | Description |
63+
| -------------------------------- | ------------------- | ------------------------------------ |
64+
| `--bind` / `-b` | - | Address to bind (can repeat) |
65+
| `--workers` / `-w` | `SERVER_WORKERS` | Worker processes (0=auto, CPU count) |
66+
| `--threads` | `SERVER_THREADS` | Threads per worker |
67+
| `--timeout` / `-t` | `SERVER_TIMEOUT` | Worker timeout in seconds |
68+
| `--access-log / --no-access-log` | `SERVER_ACCESS_LOG` | Enable/disable access logging |
69+
| `--reload` | - | Restart workers on code changes |
70+
| `--certfile` | - | Path to SSL certificate file |
71+
| `--keyfile` | - | Path to SSL key file |
72+
| `--pidfile` | - | PID file path |
7473

7574
## Settings
7675

@@ -80,7 +79,6 @@ Server behavior can be configured in your `settings.py` file. These are the defa
8079
SERVER_WORKERS = 0 # 0 = auto (one per CPU core)
8180
SERVER_THREADS = 4
8281
SERVER_TIMEOUT = 30
83-
SERVER_MAX_REQUESTS = 0 # 0 = disabled
8482
SERVER_ACCESS_LOG = True
8583
SERVER_ACCESS_LOG_FIELDS = ["method", "path", "query", "status", "duration_ms", "size", "ip", "user_agent", "referer"]
8684
SERVER_GRACEFUL_TIMEOUT = 30

plain/plain/server/app.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ def __init__(
2424
workers: int,
2525
threads: int,
2626
timeout: int,
27-
max_requests: int,
2827
reload: bool,
2928
pidfile: str | None,
3029
certfile: str | None,
@@ -35,7 +34,6 @@ def __init__(
3534
self.workers = workers
3635
self.threads = threads
3736
self.timeout = timeout
38-
self.max_requests = max_requests
3937
self.reload = reload
4038
self.pidfile = pidfile
4139
self.certfile = certfile

plain/plain/server/http/h2handler.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,12 @@ async def async_handle_h2_connection(
297297
handler: Any,
298298
is_ssl: bool,
299299
executor: ThreadPoolExecutor,
300-
max_requests: int = 0,
301-
) -> int:
300+
) -> None:
302301
"""Async HTTP/2 connection loop.
303302
304303
Reads frames from the socket in a dedicated thread (to avoid exhausting
305304
the shared executor pool), dispatches each completed stream as an
306305
independent asyncio task.
307-
308-
Returns the number of streams (requests) completed.
309306
"""
310307
config = h2.config.H2Configuration(client_side=False)
311308
conn = h2.connection.H2Connection(config=config)
@@ -324,7 +321,6 @@ async def async_handle_h2_connection(
324321
sock.settimeout(30)
325322

326323
stream_tasks: dict[int, asyncio.Task[None]] = {}
327-
streams_completed = 0
328324

329325
recv_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
330326
reader_stop = threading.Event()
@@ -432,10 +428,8 @@ def _reader_thread() -> None:
432428
def _on_stream_done(
433429
t: asyncio.Task[None], sid: int = event.stream_id
434430
) -> None:
435-
nonlocal streams_completed
436431
stream_tasks.pop(sid, None)
437432
state.cleanup_stream(sid)
438-
streams_completed += 1
439433

440434
task.add_done_callback(_on_stream_done)
441435

@@ -467,12 +461,6 @@ def _on_stream_done(
467461
else:
468462
async with state.write_lock:
469463
await state.flush()
470-
if max_requests and streams_completed >= max_requests:
471-
log.info(
472-
"HTTP/2 connection reached max_requests (%d), closing",
473-
max_requests,
474-
)
475-
break
476464
continue
477465
# break from for-loop reached — exit while-loop too
478466
break
@@ -508,8 +496,6 @@ def _on_stream_done(
508496
if reader_thread.is_alive():
509497
log.warning("H2 reader thread did not exit cleanly")
510498

511-
return streams_completed
512-
513499

514500
async def _async_handle_stream(
515501
state: H2ConnectionState,

plain/plain/server/workers/thread.py

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import time
2727
from concurrent.futures import ThreadPoolExecutor
2828
from datetime import datetime
29-
from random import randint
3029
from types import FrameType
3130
from typing import TYPE_CHECKING, Any
3231

@@ -63,9 +62,6 @@ class _H2Sentinel:
6362

6463
_H2_SENTINEL = _H2Sentinel()
6564

66-
# Maximum jitter to add to max_requests to stagger worker restarts
67-
MAX_REQUESTS_JITTER = 50
68-
6965
# Keep-alive connection timeout in seconds
7066
KEEPALIVE = 2
7167

@@ -159,14 +155,6 @@ def __init__(
159155
self.booted = False
160156
self.reloader: Any = None
161157

162-
self.nr = 0
163-
164-
if app.max_requests > 0:
165-
jitter = randint(0, MAX_REQUESTS_JITTER)
166-
self.max_requests = app.max_requests + jitter
167-
else:
168-
self.max_requests = sys.maxsize
169-
170158
self.alive = True
171159
self.log = logging.getLogger("plain.server")
172160
self.heartbeat = heartbeat
@@ -313,27 +301,15 @@ async def _handle_connection(self, conn: TConn) -> None:
313301
) = await loop.run_in_executor(self.tpool, self._parse_request, conn)
314302

315303
if isinstance(parse_result, _H2Sentinel):
316-
remaining = self.max_requests - self.nr
317-
if self.max_requests < sys.maxsize and remaining <= 0:
318-
self.log.info("Autorestarting worker after current request.")
319-
self.alive = False
320-
return
321304
conn.handed_off = True
322-
h2_streams = await async_handle_h2_connection(
305+
await async_handle_h2_connection(
323306
conn.sock,
324307
conn.client,
325308
conn.server,
326309
self.handler,
327310
self.app.is_ssl,
328311
self.tpool,
329-
max_requests=remaining
330-
if self.max_requests < sys.maxsize
331-
else 0,
332312
)
333-
self.nr += h2_streams
334-
if self.nr >= self.max_requests:
335-
self.log.info("Autorestarting worker after current request.")
336-
self.alive = False
337313
return
338314

339315
if parse_result is None:
@@ -571,12 +547,6 @@ def _parse_request(
571547
http_request = create_request(req, conn.sock, conn.client, conn.server)
572548

573549
resp = Response(req, conn.sock, is_ssl=self.app.is_ssl)
574-
self.nr += 1
575-
if self.nr >= self.max_requests:
576-
if self.alive:
577-
self.log.info("Autorestarting worker after current request.")
578-
self.alive = False
579-
resp.force_close()
580550

581551
if not self.alive:
582552
resp.force_close()

0 commit comments

Comments
 (0)