Conversation
… runs The standalone entry point (scripts/dev/standalone-server-ws.mjs, shipped as server-ws.mjs and started by Docker via dev/run-standalone.mjs, by `omniroute serve` and by Electron) let Next's start-server install its own SIGINT/SIGTERM handlers. Those call server.close() and then process.exit(143) as soon as the HTTP server has no connection left, which preempts src/lib/gracefulShutdown.ts (spend batch flush, call-log flush, DB checkpoint). With a fake Next server.js that mirrors start-server's signal handling, SIGTERM on the base wrapper logs `cleanup-start` and exits 143 without ever reaching `cleanup-done`. run-next.mjs already avoids this via __omnirouteCustomServerOwnsShutdown (diegosouzapw#12074); the standalone path now uses the same model: - set NEXT_MANUAL_SIG_HANDLE=1 before ./server.js loads (only when empty or unset), and clear it in setImmediate after the first 'listening' so spawned child services do not inherit it; - set __omnirouteCustomServerOwnsShutdown so initGracefulShutdown registers its cleanup instead of competing signal listeners; - own SIGINT/SIGTERM/SIGHUP (diegosouzapw#8045): close the HTTP servers, await __omnirouteRequestShutdown, exit 0, bounded by an unref'd force-exit timer of SHUTDOWN_TIMEOUT_MS + 5 s. Duplicate signals are ignored. Tests: tests/unit/standalone-server-ws-shutdown-owner.test.ts runs the real wrapper against the fake server.js (5/7 fail on the base, 7/7 pass), plus a guard that Next still reads NEXT_MANUAL_SIG_HANDLE synchronously in its 'listening' handler.
QuangBlue
force-pushed
the
fix/standalone-sigterm-cleanup
branch
from
October 2, 2026 05:18
6ea393d to
a25011b
Compare
This branch has not been deployed
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.
Problem
On the standalone entry point (
scripts/dev/standalone-server-ws.mjs, shipped asserver-ws.mjsand started by Docker viadev/run-standalone.mjs, byomniroute serveand by Electron), a SIGTERM/SIGINT (docker stop,systemctl stop, Ctrl+C) exits with code 143/130 before OmniRoute's graceful-shutdown cleanup finishes. The spend batch flush, call-log flush and DB checkpoint insrc/lib/gracefulShutdown.tsstart but do not complete.Root cause
Next's standalone
start-server.jsinstalls its own SIGINT/SIGTERM handlers in itslisteninghandler, unlessNEXT_MANUAL_SIG_HANDLEis set. Those handlers callserver.close()and thenprocess.exit(143)as soon as no connection is left. That happens long before the async cleanup thatinitGracefulShutdown()registered on the same signals is done.scripts/dev/run-next.mjsalready avoids this by owning shutdown (__omnirouteCustomServerOwnsShutdown, #12074). The standalone wrapper did not.Fix
standalone-server-ws.mjsnow owns process exit, using the same model asrun-next.mjs:NEXT_MANUAL_SIG_HANDLE=1before./server.jsloads, but only when the variable is empty or unset. An empty value is falsy for Next. The wrapper clears the variable insetImmediateafter the firstlisteningevent, so spawned child services (some are Next apps) do not inherit it. Next reads it synchronously in itslisteninghandler, and a test guards that.__omnirouteCustomServerOwnsShutdown, soinitGracefulShutdown()registers its cleanup as__omnirouteRequestShutdowninstead of adding competing signal listeners.__omnirouteRequestShutdown, then exit 0 one macrotask later (fix(backend): Windows: libuv abort when process.exit() follows a sql.js statement #13306).SHUTDOWN_TIMEOUT_MS+ 5 s.Tests
tests/unit/standalone-server-ws-shutdown-owner.test.ts. It runs the real wrapper with its shipped siblings next to a fakeserver.jswhose signal handling mirrors Next'sstart-server.js.cleanup-startand then exits 143 without reachingcleanup-done.NEXT_MANUAL_SIG_HANDLEread.run-next-*,standalone*,pack-artifact*,assemble*,live-ws-standalone-wiring,graceful-shutdown*: 56/56 pass.npm run typecheck:core, eslint (with the suppressions file) and prettier are clean.Notes
nextServer.close()no longer runs on SIGTERM, because the wrapper has no handle to the NextServer. Pendingafter()/waitUntilwork is therefore not awaited. Nothing insrc/oropen-sse/usesafter()/waitUntiltoday, so there is no effect now. A comment in the wrapper flags this for anyone who adds one later.servesupervisor ignore the exit code.run-next.mjshandles a second signal differently: it exits 1 immediately.omniroute serve, Electron), so the 35 s default bound does not make them hang.