Uvicorn signal handling
Stopped with local, uncommitted changes on develop. No commits, pushes, or PRs.
Implemented: Uvicorn 0.54.0, compatible event-loop initialization, worker signal fixes, process-group test cleanup, and regression tests.
The investigation found separate problems:
- Lost coverage: Uvicorn 0.29+ restores and replays signals after ASGI shutdown. Inboard's default handlers then terminated workers without Gunicorn cleanup or Python exit hooks. The same four passing tests produced 98% worker coverage on 0.28.1 versus 70% on 0.29.0. Explicit Gunicorn handlers fix this.
- Ineffective SIGQUIT: The handler changed Gunicorn's
aliveflag, which Uvicorn does not read. It now invokes Gunicorn's quick-exit handler. - Leaked test processes: SIGUSR2 created replacement masters that teardown missed. The baseline leaked six masters; stopping them produced 12 additional coverage files. Teardown now signals the entire test process group.
- Application-owned fork children: This upstream hazard still reproduces on 0.54.0. Children can inherit Uvicorn's handler and swallow SIGTERM. Applications must use spawn or reset child handlers and explicitly stop/join children. This limitation is documented.
Verification so far
- 12 shutdown regression cases passed on Python 3.11.
- Six replacement-master cleanup cases passed on both 3.11 and 3.14.
- Standalone shutdown smoke tests passed with reload enabled and disabled.
- Style checks, package builds, and strict documentation builds passed. The existing typing baseline includes five additional
Anydiagnostics from Uvicorn's expanded configuration signature.
Still pending: Full suites remain running on Python 3.11 and 3.14, last reporting 73% progress. Combined coverage, late-file checks, and final leftover-process checks are not complete. Docker testing is unavailable because the orb has no running Docker daemon.
All proposed changes are within inboard. There are no changes to upstream Uvicorn source code, no fork, and no monkey-patching.
Inboard upgrades to the published Uvicorn 0.54.0 package and adapts its own Gunicorn workers to Uvicorn's signal replay and event-loop APIs. The remaining changes are inboard's tests, dependency declarations, types, documentation, and typing baseline.
Uvicorn's standalone signal behavior remains unchanged. The application-owned fork-child issue is documented as an upstream limitation, not fixed by modifying Uvicorn.
Verification is complete. Changes remain local and uncommitted. Nothing has been pushed, and no PR has been opened.
coverage run: 300 tests passed on both Python 3.11 and 3.14.coverage combineandcoverage report: 100% coverage, including the Gunicorn workers.- Late-file check: 218 coverage files before and after ten seconds on each version.
- Process check: no leftover Gunicorn test processes.
- Ruff, BasedPyright, Prettier, spell checks, package builds, strict documentation build, and dependency compatibility checks passed.
Coverage exposed one obsolete startup-failure check. Uvicorn now exits directly with Gunicorn's expected status 3, so I removed the redundant check and verified that both worker classes stop the master without respawning.
Both suites emitted the existing Starlette test-client deprecation warning. Docker testing remains unverified because the orb has no running Docker daemon.
The next step is review and delivery, not more implementation.
- Review the diff, particularly the shutdown behavior, documented fork-child limitation, and five additional
Anydiagnostics in the typing baseline. - Create a feature branch and commit the changes. They currently remain uncommitted on
develop. - With your approval, push the branch and open a draft PR targeting
develop. CI can then cover Python 3.12/3.13 and the Docker builds/tests that were not verified here. - After review and green CI, merge and prepare a minor inboard release, following the project's policy for Uvicorn minor-version upgrades.
I recommend reviewing the diff before committing. No further action is needed to resolve the reproduced inboard signal and coverage failures.
Addressed all five comments. The docstrings now explain the shutdown expectations and replacement-master lifecycle. The documentation connects the coverage failure to the 0.72.0 release notes and explains the fork-child problem step by step.
Verified:
- Ruff, formatting, spelling, and
git diff --checkpass. - Strict Zensical build reports no issues.
- The relative changelog link builds to
changelog.html#0720-2025-01-10, and that anchor exists.
These were documentation and docstring changes only, so I did not rerun the full test suites. Changes remain uncommitted, with no push or PR.
Opened draft PR #158: Fix signal handling and update Uvicorn, targeting develop.
- Pushed branch
fix-uvicorn-signal-handling. - Commit is signed and verified.
- Working tree is clean.
- PR includes validation results and remaining limitations.
- CI is running, including Python 3.11-3.14 checks. Nothing has been merged.
You are correct: the reset originated in Uvicorn, not inboard. My wording obscured that provenance.
The history is:
December 2020: Uvicorn PR #895 replaced an empty
init_signals()with:for s in self.SIGNALS: signal.signal(s, signal.SIG_DFL)This fixed issue #894: workers inherited Gunicorn's arbiter
SIGCHLDhandler, which interfered with application subprocess exit handling and could report an incorrect return code.March 2024: Uvicorn PR #1600 introduced signal restoration and replay for Uvicorn 0.29. It did not introduce the worker reset. The revision you linked already contains it.
December 2024: Inboard copied the worker code with that reset intact.
The coverage failure therefore came from the interaction between Uvicorn's existing worker reset and its later signal replay, not a reset independently introduced by inboard.
Our proposed fix retains the reset for its original purpose, then installs explicit SIGTERM/SIGINT handlers so replay allows worker cleanup and Python exit hooks to run.
I corrected the documentation locally with this attribution, rationale, and source links. Formatting, spelling, and the strict documentation build pass. This clarification is not yet committed or pushed.
I'll commit the documentation clarification, push it to the existing PR branch, and check whether the PR description needs an update.