fix(gunicorn): align --graceful-timeout with --timeout and expose as config#418
Open
mrrobot47 wants to merge 2 commits into
Open
fix(gunicorn): align --graceful-timeout with --timeout and expose as config#418mrrobot47 wants to merge 2 commits into
mrrobot47 wants to merge 2 commits into
Conversation
…config bench_supervisor._write_gunicorn_wrapper hardcoded --graceful-timeout 30 while -t (--timeout) was sourced from http_timeout (default 120). When a worker hit --max-requests and got SIGTERM'd, in-flight requests had only 30s to drain before SIGKILL fired, even though those same requests were otherwise allowed up to http_timeout to run. The SIGKILL closed upstream sockets abruptly, surfacing at the nginx layer as upstream RSTs and ultimately as 502s to clients. Expose gunicorn_graceful_timeout as a common_site_config.json knob defaulting to http_timeout. Behavior change: the default for the graceful drain window grows from 30s to whatever http_timeout is set to (120s by default). Operators who specifically need a shorter drain window can set gunicorn_graceful_timeout explicitly.
There was a problem hiding this comment.
Pull request overview
Aligns Gunicorn’s shutdown drain behavior with the configured HTTP request timeout by replacing a hardcoded --graceful-timeout 30 with a configurable value sourced from common_site_config.json, defaulting to http_timeout. This aims to reduce 502s caused by workers being SIGKILL’d mid-request during routine recycling.
Changes:
- Introduces
gunicorn_graceful_timeoutin the supervisor config rendering context, defaulting tohttp_timeout. - Updates the generated Gunicorn wrapper args to emit
--graceful-timeout {gunicorn_graceful_timeout}instead of a fixed30.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The web program's supervisor stanza had a hardcoded stopwaitsecs=40,
but this PR can now default --graceful-timeout to http_timeout (120s).
On SIGTERM supervisor would SIGKILL the gunicorn master at 40s,
truncating the drain window the PR is intended to extend and reverting
to the same 502/RST behavior on worker recycle.
Drive stopwaitsecs from the same gunicorn_graceful_timeout value plus
10s of headroom for the master's post-drain reaping, exposed to the
template as supervisor_web_stopwaitsecs. The pre-existing template
comment ('graceful timeout should always be lower than stopwaitsecs to
avoid orphan gunicorn workers') already documents this invariant.
Also coerce http_timeout and gunicorn_graceful_timeout via a small
positive-int helper. common_site_config.json is user-editable, and a
stringified ('120') or negative value would otherwise land verbatim in
the generated wrapper / supervisor config and break gunicorn boot.
Invalid values fall back to the documented defaults.
Member
Author
|
Pushed 1124823. Addressed:
CI: green (Analyze actions / Analyze python / CodeQL / pytest 3.13 all pass). |
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.
Issue
frappe_manager/site_manager/modules/bench_supervisor.py(lines 381-391) hardcodes--graceful-timeout 30on the gunicorn command line while-t(--timeout) is sourced fromcontext['http_timeout'](default120, configurable viacommon_site_config.json):When a worker hits
--max-requests(or any other SIGTERM), gunicorn gives in-flight requests only 30 seconds to finish before SIGKILL fires — even though those same requests are otherwise allowed up tohttp_timeout(default 120s) to run. The SIGKILL closes upstream sockets abruptly, which surfaces at the nginx layer as upstream RSTs and ultimately as 502s returned to clients.Impact
Affects every FM-deployed site running under sustained load, where
--max-requestsrecycles happen frequently enough to coincide with in-flight long-running requests. The failure mode is non-deterministic 502s during otherwise-normal worker recycling: a request that would have completed within the configuredhttp_timeoutgets SIGKILL'd mid-flight and the client sees a 502 instead of a successful response.APM data on FM-deployed production-scale installs measured worker-recycle RST events as a meaningful contributor to chronic 5xx rate. The 30s graceful window being shorter than the 120s request timeout is the root cause: any request that takes 30-120s — slow custom-app endpoints, large reports, sync HTTP calls to external services, large
frappe.dboperations — is at risk during a routine recycle.This is a configuration-only issue; the upstream gunicorn default of
--graceful-timeout 30is not appropriate when--timeoutis set to a value larger than 30.Solution
Expose
--graceful-timeoutas acommon_site_config.jsonknob (gunicorn_graceful_timeout), defaulting tohttp_timeoutfor backward-compatible behavior:generate_supervisor_configcontext:gunicorn_graceful_timeout = config.get("gunicorn_graceful_timeout", http_timeout)._write_gunicorn_wrappernow emits--graceful-timeout {context['gunicorn_graceful_timeout']}instead of the hardcoded30.The default is
http_timeout(not the previous30) on the principle that the graceful drain window should match the request timeout: any request gunicorn was willing to let run synchronously should also be allowed to drain on shutdown. Operators who specifically need a shorter drain (e.g. to bound rolling-restart window length) can setgunicorn_graceful_timeoutlower thanhttp_timeoutincommon_site_config.json.Alternatives considered:
30to{context['http_timeout']}without adding a new knob. Rejected because some operators legitimately want a shorter graceful window to bound restart duration — exposing the knob costs ~3 lines and preserves that flexibility.Test plan
fm createsite with the defaultcommon_site_config.json. Inspectconfig/fm-web-server.sh—--graceful-timeout 120(matches defaulthttp_timeout)."gunicorn_graceful_timeout": 60incommon_site_config.json, re-runfm bench supervisor setup --force. Inspect the wrapper —--graceful-timeout 60."http_timeout": 180(nogunicorn_graceful_timeout), re-run setup. Inspect the wrapper —--graceful-timeout 180.gunicorn_max_requestslow, send sustained traffic). Confirm no 502 spikes from RST during recycles for requests that complete withinhttp_timeout.Rollback
Revert the merge commit. The config field is opt-in (read with
config.get(..., http_timeout)), so on rollback all existing sites continue to work with the previously-hardcoded30.References
frappe_manager/site_manager/modules/bench_supervisor.py:381-391—_write_gunicorn_wrapperconstructinggunicorn_argsfrappe_manager/site_manager/modules/bench_supervisor.py:218-236—generate_supervisor_configcontext dict where the new field is sourced--graceful-timeoutsemantics — workers receive SIGTERM, then SIGKILL after this many seconds; default 30