feat(restart): graceful gunicorn reload via supervisorctl signal HUP#419
Open
mrrobot47 wants to merge 3 commits into
Open
feat(restart): graceful gunicorn reload via supervisorctl signal HUP#419mrrobot47 wants to merge 3 commits into
mrrobot47 wants to merge 3 commits into
Conversation
…IGHUP Restarting the web service through `fm restart` currently issues `supervisorctl stop && start`, which closes the gunicorn listening socket for a brief window. Any client connecting in that window observes a TCP RST or a `connect: connection refused`, and upstream proxies surface those as 502s. For production deploys behind a reverse proxy this is the sole source of a small but consistent 502 burst on every restart. This adds a `--graceful` flag to `fm restart` that signals the frappe (gunicorn) container with `supervisorctl signal HUP all` instead. The gunicorn master keeps its listening socket open, forks fresh workers against it, and only then retires the old workers — eliminating the connection-refused window from the upstream's perspective. Scope and constraints: * Only the frappe (gunicorn) container honours graceful; the socketio container has no graceful-reload signal (node terminates on SIGHUP), so it is restarted normally even when `--graceful` is set. * Worker containers are never graceful — RQ workers do not implement HUP semantics. * `--graceful` is mutually exclusive with `--force` and ignored with `--container`. The existing hard-restart paths remain reachable unchanged for callers that need a parent re-exec (for example to pick up code changes when gunicorn is running with --preload). Adds unit tests covering the three branches of `BenchSupervisor.restart_supervisor_service` (default / force / graceful), the mutual-exclusion guard, and the wiring in `Bench.restart_web_containers_services` that limits graceful to the frappe container.
…ainer Mirrors the host-side `fm restart --graceful` flag for `fmx restart` running inside the frappe container. The flag wires a new `_graceful_reload_service` action into `execute_parallel_command` that discovers the running processes for the target service via the supervisor XML-RPC API and dispatches `signal HUP` to each one through the existing `signal_service` helper. For the gunicorn-backed `frappe` service this hands SIGHUP to the master process, which forks new workers using the still-open listening socket and only then retires the old workers — no connection-refused window for upstream proxies. `--graceful` is mutually exclusive with `--migrate` (which intentionally performs a real restart) and slots into the same maintenance-mode / RQ drain orchestration as the existing restart path so callers can compose it with `--maintenance-mode` and `--drain-workers` as before.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in “graceful restart” path for the web (gunicorn) service to avoid the brief connection-refused window caused by stop/start-style restarts, by using SIGHUP-based reloads via supervisord/supervisorctl.
Changes:
- Introduces
--gracefultofm restart, wiring it throughBench.restart_web_containers_servicesandBenchSupervisor.restart_supervisor_service. - Adds an analogous
--gracefulpath tofmx restart, implemented as a supervisor XML-RPC signal dispatch. - Adds/updates docs and unit tests covering graceful vs force vs default behavior and wiring (frappe-only graceful).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
frappe_manager/site_manager/modules/bench_supervisor.py |
Adds graceful branch to restart supervisor services via supervisorctl signal HUP all and enforces force/graceful mutual exclusion. |
frappe_manager/site_manager/site.py |
Threads graceful through bench restart APIs; scopes graceful reload to frappe container only. |
frappe_manager/commands/restart.py |
Adds --graceful CLI option and validation/wiring into bench web restart. |
Docker/frappe/fmx/fmx/commands/restart.py |
Adds --graceful to container-side fmx restart and implements per-process SIGHUP signaling. |
docs/commands/restart.md |
Documents the new --graceful option and caveats (gunicorn --preload). |
tests/unit/site_manager/test_bench_supervisor_restart.py |
New unit tests covering graceful path, exclusivity, error propagation, and bench wiring behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
* `fm restart --graceful --container` now warns and proceeds with a normal container restart instead of exiting, matching the PR-body contract that --graceful is "ignored when combined with --container". * `fmx restart --graceful` prints a success/failure summary on completion. `execute_parallel_command` only formats summaries for the built-in restart/start/stop handlers, so the graceful path now owns its own summary mirroring `_handle_restart_results`. * `BenchSupervisor.restart_supervisor_service` docstring rewritten to describe all three branches (graceful, force, default) distinctly -- the old `force` line incorrectly called the default path "graceful". * `test_graceful_sends_sighup_via_supervisorctl` adds an explicit assertion that exactly one SIGHUP exec call is issued, guarding against future regressions that would send it multiple times. No new ruff findings (baseline 59 -> 59).
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.
Summary
fm restart(andfmx restartinside the container) currently issuesupervisorctl stop && startfor the web service. That stop brieflycloses the gunicorn listening socket; any client that connects in the
window between
stopandstartsees a TCP RST orconnect: connection refused, which reverse proxies surface as a 502.For production deployments this is the dominant cause of a small but
consistent 502 burst on every restart — clients reading at the moment
of the stop also see in-flight requests RST'd.
This adds an opt-in
--gracefulflag that recycles workers viasupervisorctl signal HUP allinstead. The gunicorn master keeps itslistening socket open across the reload, so upstream proxies observe no
connection-refused window and no in-flight RSTs from the listener.
Behavior
fm restart <bench> --graceful— signals the frappe (gunicorn)container with SIGHUP via supervisorctl. The socketio container is
restarted normally (node has no graceful-reload signal). Worker
containers are not affected by this flag.
fm restart <bench>(default) — unchanged, still runssupervisorctl restart all.fm restart <bench> --force— unchanged, still does the explicitstop && startcycle for callers that need a hard restart.fmx restart <service> --graceful— same semantics inside thecontainer, dispatching SIGHUP to each process via the supervisor
XML-RPC API. Composes with the existing
--maintenance-modeand--drain-workersorchestration; mutually exclusive with--migrate.--gracefulis mutually exclusive with--forceand ignored whencombined with
--container(a full container restart can't be madegraceful at this layer).
Mechanism
The web service runs gunicorn with
--preload. On SIGHUP, thegunicorn master:
is ready.
At no point does the master close the listening socket, so a
connecting client never observes connect-refused / RST during the
swap. The existing
--preloadsetup in_write_gunicorn_wrapperisalready compatible with this — no template changes required.
Backward compatibility
Default behavior is unchanged. The flag is opt-in and the existing
hard-restart path remains the default and is still reachable via
--force. No existing callers need to be updated. The pre-existingforceand default branches ofBenchSupervisor.restart_supervisor_serviceare untouched; the new
gracefulbranch is added alongside them.Test plan
tests/unit/site_manager/test_bench_supervisor_restart.py:supervisorctl signal HUP alland nothing elseBenchOperationExceptionbefore anydocker command is executed
BenchOperationExceptionrestart allBench.restart_web_containers_servicesonly applies graceful tothe frappe container, never to socketio
use_container_restart=Truebypasses the supervisor path entirelyfm restart <bench> --gracefulagainst a running benchwhile a load generator hits
/api/method/ping— confirm zeroconnection refusedin the access log across the reload window(covered manually before opening this PR; reproduction script is
out of scope here).
Caveats / follow-up
gunicorn --preload, SIGHUP recycles workers but does notre-import application code in the master. This flag is suitable for
restart-without-RST (release leaked memory, apply runtime config
changes that workers re-read on fork) but is not a substitute for
a full restart when picking up new application code — for that, a
follow-up could wire a
--graceful=usr2path that re-execs thegunicorn master. The docs and help text call this out so callers pick
the right flag.
under
--gracefulbecause node terminates on SIGHUP. A follow-upcould investigate a custom signal handler in the socketio entrypoint
to make socketio reloads non-disruptive too.
--container) cannot be made graceful atthis layer — the right place for that is upstream connection-draining
at the proxy, which is out of scope here.