Skip to content

fix(db): defer process.exit(0) by a macrotask on graceful shutdown (#13306) - #13778

Merged
diegosouzapw merged 3 commits into
release/v3.8.51from
fix/13306-windows-libuv-abort-sqljs-exit
Sep 16, 2026
Merged

diegosouzapw merged 3 commits into
release/v3.8.51from
fix/13306-windows-libuv-abort-sqljs-exit

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Refs #13306

Root cause (short)

sql.js is an Emscripten WASM build; once a statement has run (db.run()/adapter.exec()), Emscripten leaves async cleanup/teardown work registered with libuv. gracefulShutdown.ts's shutdown closure called process.exit(0) in the same tick the cleanup promise resolved, tearing the event loop down synchronously before that teardown work settled. On Windows, libuv's async-handle close path (src/win/async.c) asserts !(handle->flags & UV_HANDLE_CLOSING) when this happens, aborting the process (exit 127). Linux's unix backend has no equivalent assertion, which is why CI (Linux-only) never caught it and it stayed invisible until a Windows contributor hit it locally.

Fix

Defer process.exit(0) by one macrotask (setTimeout(() => process.exit(0), 0)) instead of calling it synchronously right after the cleanup promise resolves. This mirrors the pattern already used throughout 9router's own shutdown call sites (appUpdater.js:199, cli/cli.js:675 etc.) and gives sql.js's pending libuv teardown work a chance to run before the event loop tears down. The delay is unconditional (applies to every driver, not just sql.js) since a 0ms setTimeout is a no-op-cost change when sql.js isn't involved.

Regression test (path + RED output excerpt on unfixed code + GREEN excerpt)

tests/unit/graceful-shutdown-deferred-exit-13306.test.ts — pins the ordering contract the fix depends on (this cannot reproduce the Windows libuv abort itself; Linux has no equivalent assertion, see "Live check" below):

RED (before the fix):

✖ graceful shutdown defers process.exit(0) to a macrotask after cleanup resolves (#13306)
  AssertionError [ERR_ASSERTION]: process.exit(0) must not fire in the same microtask turn the cleanup promise resolves in
  + actual - expected
  + [ 0 ]
  - []

GREEN (after the fix):

✔ graceful shutdown defers process.exit(0) to a macrotask after cleanup resolves (#13306) (788.956645ms)
ℹ tests 1
ℹ pass 1
ℹ fail 0

Gates run

  • npx eslint --suppressions-location config/quality/eslint-suppressions.json src/lib/gracefulShutdown.ts tests/unit/graceful-shutdown-deferred-exit-13306.test.ts → clean, exit 0
  • npm run typecheck:core → clean, no errors
  • node scripts/check/check-file-size.mjs → no ✗ on touched files (one pre-existing ✗ on open-sse/utils/stream.ts, a file this PR does not touch — confirmed identical to origin/release/v3.8.51, base drift)
  • node scripts/check/check-complexity.mjs → OK, no violation on the touched function
  • node scripts/check/check-cognitive-complexity.mjs → OK — 1276 violações (baseline 1437), no regression
  • node scripts/check/check-test-discovery.mjs → OK — 5780 arquivos de teste, new test file discovered
  • Existing gracefulShutdown.ts tests: tests/unit/graceful-shutdown-sighup-8045.test.ts → both cases pass unchanged

Existing tests aligned

None needed — no existing test encoded the old synchronous-exit behavior.

Live check (required before merge)

This fix cannot be verified on Linux CI (no libuv assertion equivalent on the unix backend). A live Windows run is required before merge:

  1. Real Windows 10/11 machine (not WSL, not MSYS/Git-Bash — that shell can't deliver a real signal to a native Windows process), running OmniRoute with the sql.js fallback driver active (no better-sqlite3 prebuilt binary available, so preInitSqlJs() is used).
  2. Perform at least one DB write (any request touching src/lib/db/) so a sql.js statement has executed at least once.
  3. Trigger the real shutdown path: close the console window (SIGHUP, fix(startup): Windows restart fails with "Database closed" / SQLite driver detection (500) #8045), taskkill /PID <pid> without /F (SIGTERM), or Ctrl+C (SIGINT).
  4. Confirm process exit is clean ([Shutdown] Bye. + exit 0) and stderr does not contain Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94.
  5. Report back on fix(backend): Windows: libuv abort when process.exit() follows a sql.js statement #13306.

Not covered here

  • The Windows libuv abort itself is not reproduced by the regression test (impossible on Linux) — this PR ships the fix + the ordering-contract test per the plan-file's needs-vps verdict, and the live-check above is the outstanding validation step.
  • driverFactory.test.ts is unrelated to this trigger (confirmed by the reporter's own follow-up) and is untouched.

…13306)

sql.js's Emscripten WASM build leaves pending libuv async-handle teardown
work in flight after a statement has run. gracefulShutdown.ts's shutdown
closure called process.exit(0) in the same tick as the cleanup promise
resolving, tearing the event loop down before that teardown settled. On
Windows, libuv's async-handle close path asserts
!(handle->flags & UV_HANDLE_CLOSING) when this happens, aborting the
process (exit 127); Linux's unix backend has no equivalent assertion,
which is why this was invisible on CI. Deferring process.exit(0) by one
macrotask (setTimeout(..., 0)) mirrors the pattern already used
throughout 9router's own shutdown call sites and gives sql.js's pending
libuv work a chance to settle first.

Regression test: tests/unit/graceful-shutdown-deferred-exit-13306.test.ts
pins the ordering contract (process.exit(0) must not fire in the same
microtask turn cleanup() resolves in). The Windows abort itself cannot
be reproduced on Linux CI — see the PR body for the required live
Windows validation.
@diegosouzapw diegosouzapw added the hold-vps PR verde, merge aguardando validação live (release-drain) label Sep 15, 2026
@diegosouzapw
diegosouzapw merged commit 74e44d7 into release/v3.8.51 Sep 16, 2026
19 of 21 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…iegosouzapw#13306) (diegosouzapw#13778)

Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hold-vps PR verde, merge aguardando validação live (release-drain)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant