Skip to content

fix(serve): exit explicitly after a graceful shutdown - #38

Merged
zumbrunn merged 2 commits into
mainfrom
fix/serve-exit-after-shutdown
Oct 27, 2026
Merged

zumbrunn merged 2 commits into
mainfrom
fix/serve-exit-after-shutdown

Conversation

@zumbrunn

@zumbrunn zumbrunn commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Fixes #47

Problem

After SIGTERM, dune serve closes the listener, drains, prints shutdown complete... and then just returns. The process stays alive for as long as the event loop has any work. One ref'd plugin timer is enough to keep it running forever at 0% CPU with no listener: an "idle" instance that a second SIGTERM is needed to end. About 45 of them had piled up on a developer machine.

The trigger found there was @dune/plugin-admin's collab eviction setInterval (fixed separately in duneorg/plugin-admin#8), but core should not depend on every plugin unref'ing its timers.

Fix

exitAfterShutdown() (Deno.exit(0)) once the drain is done, in both the single-site and multi-site paths. By then the listener is closed and in-flight requests are drained (or the drain deadline has passed); there is nothing left worth waiting for.

Tests

The existing graceful-shutdown tests re-implement their helpers locally and never run the real command, which is how this went unnoticed. tests/cli/serve_exit_after_shutdown_test.ts runs the real dune serve as a subprocess with a plugin that holds a ref'd timer (so it doesn't depend on which plugin-admin version is installed), sends one SIGTERM, and requires a clean exit (code 0, shutdown complete printed). Against the old code the process is still alive 15 s later and the test fails; with the fix it passes in under a second.

  • deno task check clean; deno task test: 1877 passed, 0 failed
  • The multi-site shutdown path is covered too: a second test runs dune serve against a config/sites.yaml fixture (checked to fail when the multi-site exitAfterShutdown() call is removed).

Context

Reproduced and root-caused before fixing: one SIGTERM leaves serve listening on nothing and alive for 24 s+ (a second SIGTERM ends it); process.getActiveResourcesInfo() after shutdown showed exactly one Timeout, and recording timer creators showed it came from plugin-admin 3.1.2's createCollabManager.

CHANGELOG entry is under the single [Unreleased] section; no version bump (released together with the other pending changes).

🤖 Generated with Claude Code

On SIGTERM `dune serve` closed the listener, drained, printed "shutdown
complete" and then simply returned, leaving the process alive for as long as
the event loop had work. One ref'd plugin timer (plugin-admin's collab
eviction setInterval, fixed in 3.1.3) was enough to keep it alive forever at
0% CPU with no listener: an "idle" dune process, which a second SIGTERM was
needed to end. Roughly 45 of them had piled up on a developer machine.

Exit explicitly once the drain is done, in both the single-site and
multi-site paths. By then the listener is closed and in-flight requests are
drained (or the drain deadline has passed); there is nothing left worth
waiting for, and core should not depend on every plugin unref'ing its timers.

The existing graceful-shutdown tests re-implement their helpers locally and
never run the real command, which is how this went unnoticed. The new test
runs the real `dune serve` as a subprocess with a plugin that holds a ref'd
timer (so it does not depend on which plugin-admin version is installed),
sends one SIGTERM, and requires a clean exit. Against the old code it is
still alive 15 s later and fails; with the fix it passes in under a second.
The multi-site path has the same change but no end-to-end test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e shutdown path

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@zumbrunn
zumbrunn merged commit b3daa72 into main Oct 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dune serve stays alive after a graceful shutdown when anything holds the event loop

1 participant