fix(fpm): let php-fpm drain in-flight requests on shutdown - #55
Merged
Conversation
The supervisor program stops php-fpm with SIGQUIT, waits 20s and signals the whole process group -- everything a graceful stop needs, except the one setting that makes the master honour it. process_control_timeout defaults to 0, which means the master does not wait for its children at all: it terminates them where they are. Every rollout, scale-down and eviction cut the requests in flight. The earlier drain measurement, ported from base-nginx, covered a static download through nginx and never touched php-fpm, which is why this went unnoticed. Measured with a request that really occupies a worker for 15s -- a clock loop, not sleep(), which is interrupted by the stop signal and returns early, reporting a pass on an image that drains nothing: before, docker stop -t 30 1.37s exit 0 in-flight -> HTTP 502 before, docker stop -t 10 0.55s exit 0 in-flight -> HTTP 502 after, docker stop -t 30 13.4s exit 0 in-flight -> HTTP 200 The value is 15s, below the supervisor stopwaitsecs of 20s so the master has finished before supervisor gives up. It has to sit below the platform's grace period too, and that is the one interaction worth knowing about: in flight grace period result a 3s request 10s (plain docker stop) completes, stops in ~1.5s, exit 0 a 15s request 30s (Kubernetes default) completes, stops in ~13s, exit 0 a 15s request 10s (plain docker stop) killed at 10s, exit 137 Kubernetes and OpenShift default terminationGracePeriodSeconds to 30 and need no change; compose wants stop_grace_period: 30s, a bare docker stop wants -t 30. An operator who would rather cap the wait lowers the variable instead, which always stops cleanly at the cost of cutting longer requests. Documented as a table in docs/php-fpm.md rather than as a sentence, because the three timeouts only make sense together. The assertion added to tests.web.bats fails against the published 8.5.9 image and passes here. It uses the same clock loop for the same reason.
zebby76
added a commit
that referenced
this pull request
Sep 3, 2026
The supervisor program stops php-fpm with SIGQUIT, waits 20s and signals the whole process group -- everything a graceful stop needs, except the one setting that makes the master honour it. process_control_timeout defaults to 0, which means the master does not wait for its children at all: it terminates them where they are. Every rollout, scale-down and eviction cut the requests in flight. The earlier drain measurement, ported from base-nginx, covered a static download through nginx and never touched php-fpm, which is why this went unnoticed. Measured with a request that really occupies a worker for 15s -- a clock loop, not sleep(), which is interrupted by the stop signal and returns early, reporting a pass on an image that drains nothing: before, docker stop -t 30 1.37s exit 0 in-flight -> HTTP 502 before, docker stop -t 10 0.55s exit 0 in-flight -> HTTP 502 after, docker stop -t 30 13.4s exit 0 in-flight -> HTTP 200 The value is 15s, below the supervisor stopwaitsecs of 20s so the master has finished before supervisor gives up. It has to sit below the platform's grace period too, and that is the one interaction worth knowing about: in flight grace period result a 3s request 10s (plain docker stop) completes, stops in ~1.5s, exit 0 a 15s request 30s (Kubernetes default) completes, stops in ~13s, exit 0 a 15s request 10s (plain docker stop) killed at 10s, exit 137 Kubernetes and OpenShift default terminationGracePeriodSeconds to 30 and need no change; compose wants stop_grace_period: 30s, a bare docker stop wants -t 30. An operator who would rather cap the wait lowers the variable instead, which always stops cleanly at the cost of cutting longer requests. Documented as a table in docs/php-fpm.md rather than as a sentence, because the three timeouts only make sense together. The assertion added to tests.web.bats fails against the published 8.5.9 image and passes here. It uses the same clock loop for the same reason.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The supervisor program stops php-fpm with
SIGQUIT, waits 20s and signals the whole process group— everything a graceful stop needs, except the one setting that makes the master honour it.
process_control_timeoutdefaults to0, which means the master does not wait for its children atall: it terminates them where they are.
Every rollout, scale-down and eviction cut the requests in flight.
The earlier drain measurement, ported from base-nginx, covered a static download through nginx and
never touched php-fpm — which is why this went unnoticed.
Measured
With a request that really occupies a worker for 15s. It has to be a clock loop, not
sleep():sleepis interrupted by the stop signal and returns early, which reports a pass on an image thatdrains nothing.
docker stop -t 300docker stop -t 100docker stop -t 300Choosing the value
15s, below the supervisorstopwaitsecsof 20s so the master finishes before supervisor givesup. It also has to sit below the platform's grace period, and that is the interaction worth
knowing about:
docker stop)00docker stop)137The common case — a short request — is identical either way: 200, clean exit, ~1.5s. The third row
is the cost of the choice, and it is a case where the request was going to be dropped regardless;
what changes is that the exit code says so.
Kubernetes and OpenShift default
terminationGracePeriodSecondsto 30 and need no change; composewants
stop_grace_period: 30s, a baredocker stopwants-t 30. An operator who would rathercap the wait lowers
PHP_FPM_PROCESS_CONTROL_TIMEOUTinstead, which always stops cleanly at thecost of cutting longer requests.
I measured an
8sdefault as the alternative — it keepsdocker stopclean out of the box butnever drains a long request on any platform. Given the deployment targets, draining where it is
possible won.
Documented in
docs/php-fpm.mdas a table rather than a sentence: the three timeouts only makesense together.
Regression assertion
tests.web.batsgains one, using the same clock loop for the same reason. It fails against thepublished 8.5.9 image and passes here.