You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
With multiple workers, SIGTERM kills in-flight requests because the master terminates busy workers once the first idle worker exits #386
With --workers > 1, a SIGTERM (e.g. docker stop / Kubernetes pod termination) does not shut down
gracefully. In-flight requests are cut off almost immediately ("Empty reply from server"). With -w 1 the same
request completes normally.
Cause
In hypercorn/run.py, the shutdown signal handler sets shutdown_event and active = False. The master is
blocked in:
wait(process.sentinelforprocessinprocesses)
multiprocessing.connection.wait returns as soon as any worker exits. Idle workers notice the shutdown
event and exit right away, so wait() returns while other workers are still serving requests. Since active
is now False, the loop ends and the master runs:
forprocessinprocesses:
process.terminate()
The code is the same in 0.17.3, 0.18.0 and current main.
Reproduction
Versions: hypercorn 0.17.3 (also checked 0.18.0 source), Python 3.12.13, Linux (python:3.12.13-alpine Docker
image), asyncio worker, Quart 0.20.0 as the app.
app.py:
importasynciofromquartimportQuart, requestapp=Quart(__name__)
@app.get("/slow")asyncdefslow():
seconds=float(request.args.get("s", 10))
awaitasyncio.sleep(seconds)
returnf"done after {seconds}s\n"
Dockerfile:
FROM python:3.12.13-alpine
RUN pip install --no-cache-dir hypercorn==0.17.3 quart==0.20.0
WORKDIR /srv
COPY app.py /srv/
docker build -t hc-test .
docker run -d --name hc -p 18080:8080 hc-test hypercorn -b 0.0.0.0:8080 -w 3 app:app
sleep 3
curl -s -w "http=%{http_code} time=%{time_total}s %{errormsg}\n""localhost:18080/slow?s=10"&
sleep 1
docker stop -t 30 hc # SIGTERM to PID 1 (the hypercorn master), SIGKILL after 30swait
Results
Setup
Result
-w 3, stock 0.17.3
http=000 time=1.1s Empty reply from server (3/3 runs)
-w 1, stock 0.17.3
http=200 time=10.0s
-w 3, process.terminate() → process.join() in run.py
http=200 time=10.0s (3/3 runs)
-w 3, 3 concurrent requests (one per worker, no idle worker)
all http=200
Swapping that single terminate() call fixes the problem, which confirms the cause.
Expected behaviour
On SIGTERM/SIGINT, the master should wait for all workers to finish draining (each one is already bounded by
its own graceful shutdown) before terminating anything. Only as a fallback, e.g. after graceful_timeout, should
it call terminate().
Possible fix
After the main loop ends because of a shutdown signal, join the remaining workers instead of terminating them
right away, for example:
On Python 3.12+ the worker-side drain is effectively unbounded because of Server.wait_closed(), as noted in #308. The timeout used here may therefore need to differ from graceful_timeout, or that issue needs fixing first.
Activity