Skip to content

0.2.1: keep the process alive until shutdown() settles - #6

Merged
gabemule merged 2 commits into
mainfrom
fix/shutdown-holds-loop
Oct 20, 2026
Merged

gabemule merged 2 commits into
mainfrom
fix/shutdown-holds-loop

Conversation

@gabemule

Copy link
Copy Markdown
Contributor

Why

Finding L-1 from the architect's review of #5. In 0.2.0, waitExit unref'd its grace timer, and opencode serve is unref'd too. So once a host had closed its own handles, nothing held the event loop and the process exited before shutdown() settled:

  • code after await shutdown() never ran;
  • a signal handler's own exit code was lost (0 instead of 130);
  • a process ignoring SIGTERM never got the SIGKILL.

What

  • timer.unref() is removed from waitExit. The timer is cleared as soon as every pending process has exited, so the loop is held only while something is left to wait for. The longest it can be held is graceMs plus 1 s, which the README now states.
  • Two child-script tests, each run with a child that obeys SIGTERM and one that ignores it:
    • a top-level await shutdown();
    • a SIGINT handler that clears its own timer, then await shutdown(), then a log line, then exit(130).

Verified locally

  • Node 24 and Node 22: 170/170. Bun 1.3.14: 160 pass, 0 fail. verify:pack ok.
  • With unref restored, 4 tests go red on Node and 2 on Bun (the handler shape: the top-level shape does not trip Bun). With the fix, all are green.
  • After a script awaits shutdown() with nothing pending, the process exits within ordinary runtime start-up time (about 100 to 280 ms), never after the 3 s grace.
  • No orphaned processes after the runs.

waitExit unref'd its grace timer, and opencode serve is unref'd too, so once
a host had closed its own handles nothing held the event loop: the process
exited before shutdown() settled. Code after `await shutdown()` never ran, a
signal handler's own exit code was lost, and a process ignoring SIGTERM never
got the SIGKILL. shutdown() is awaited explicitly, so holding the loop is its
job; the timer is cleared as soon as every pending process has exited, so it
holds the loop only while something is left to wait for.

Two child-script tests pin it: a top-level `await shutdown()` and a SIGINT
handler that closes its own timer first, each with a child that obeys SIGTERM
and one that ignores it. The handler shape is the one that fails on Bun as
well as Node.
@gabemule
gabemule merged commit 9a80a00 into main Oct 20, 2026
3 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.

1 participant