Skip to content

fix(server): close the serve handover teardown race - #777

Open
dezren39 wants to merge 3 commits into
mainfrom
queue/2687-81-systemd-handover
Open

dezren39 wants to merge 3 commits into
mainfrom
queue/2687-81-systemd-handover

Conversation

@dezren39

@dezren39 dezren39 commented Oct 28, 2026 •

Copy link
Copy Markdown

Summary

The serve drain now keeps serving connections the listener already accepted before closing them, so a request that arrives after teardown began is answered instead of reset.

 drain
   status.beginStopping
   bound.server.close()          # stop accepting (Bun no longer drops idle connections here)
   untilIdle(grace)              # existing sessions and parsed requests
+  waitForAccepted(drainLinger)  # connections accepted before close() can still send their request
+  waitForRequests(grace)        # finish a request that arrived during the linger
   finish

The Bun adapter no longer calls closeIdleConnections() on close; accepted connections are served through a bounded linger (500 ms) and the finalizer's closeAllConnections() still ends them. A second SIGTERM cuts the whole wait.

 packages/server/src/process.ts          # track accepted sockets; linger + finish in drain
 packages/server/src/inherited-socket.ts # close stops accepting only
 packages/cli/test/serve-handover.test.ts # fails-before test on both runtimes
 TODO.md, ROADMAP.md                     # verified outcome

Evidence

Before: ten systemctl --user restart handovers of the upstream binary, fresh connection every 5 ms: 4 ECONNRESET in 1851 requests (0.22%). The handover order alone (successor ready before SIGTERM) saw 1 in 7376.

After: ten systemctl --user restart handovers of the fixed source: 0 resets in 2065 requests. Handover-order repro: 0 in 7115.

Fails before, passes after: bun test test/serve-handover.test.ts -t "a drain answers a request on a connection the server accepted before it began"

before: (fail) ... no response within 5s
after:  (pass) ... 200

Full test/serve-handover.test.ts: 9 pass, 0 fail on both runtimes. Root bun run typecheck: 36/36.

Real systemd user session (systemd 261, Linger=yes): a socket unit (ListenStream=127.0.0.1:4319) and a Type=notify service whose ExecStart runs the launcher, so the server child — not the unit's main pid — sends READY=1; NotifyAccess=all accepts it and each generation logs Started + opencode: ready. Two generations under a 10 ms loop: 0 refused, 0 failed. The unit and transcript are on #81.

Merge Danger

Door: two-way

Blast Radius: serve shutdown

The drain can now take up to one 500 ms linger longer when a connection is open at shutdown; a second SIGTERM still ends it immediately.

Closes #81


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

A connection the predecessor accepted in the last instant before it closed could be reset when its
request arrived after teardown began. The drain tracked only requests whose headers had been parsed,
and the Bun listener dropped idle connections as soon as it closed, so a connection that had connected
but not yet sent its request was reset.

Track connections the listener accepts and, after the existing session and request wait, keep serving
them for a bounded linger (500 ms) before the process closes them, then wait for a request that
arrived during the linger to finish. The Bun adapter no longer drops idle connections on close; the
finalizer still closes everything at the end.

Measured before the fix: 4 resets in 1851 requests across ten systemctl restarts (0.22%), and 1 in
7376 under the handover order. The new test opens a connection, lets the server accept it, starts the
drain, and sends the request afterwards; it fails without this change.
The handover is verified under a real systemd user session (systemd 261, Linger=yes) with a socket
unit and a Type=notify service that needs NotifyAccess=all because the server child, not the unit's
main pid, sends READY=1. The teardown race is fixed with a fails-before test, so the process-behavior
note comes out of TODO.md and the roadmap row is dropped per the forward-looking roadmap convention.
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 25 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a1208621-66c7-4740-bc84-21b66903c130
Commits

Reviewing files that changed from the base of the PR and between c275e48 and 4fc206a.

Files selected for processing: 5
  • ROADMAP.md
  • TODO.md
  • packages/cli/test/serve-handover.test.ts
  • packages/server/src/inherited-socket.ts
  • packages/server/src/process.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T22:12:39.468626Z e484ca2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e484ca2360

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// connections cannot hold shutdown.
yield* waitForAccepted.pipe(Effect.timeoutOption(drainLinger))
// A request that arrived during the linger is now in flight; finish it before closing.
yield* waitForRequests.pipe(Effect.timeoutOption(grace))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the shutdown grace deadline across both request waits

If a non-SSE request already in flight never finishes (for example, the partial-body request used by heldSession), untilIdle consumes the entire grace period, but this unconditional wait then grants that same request another full grace period. With the default configured in packages/cli/src/server-process.ts, shutdown now takes approximately 60.5 seconds instead of 30 seconds, rather than adding only the documented 500 ms linger. Preserve a shared deadline, or distinguish requests admitted during the linger, so requests that already exhausted their grace do not receive it again.

Useful? React with 👍 / 👎.

Comment on lines +145 to +146
})
const waitForRequests = Effect.gen(function* () {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for session execution started during the accepted-socket linger

If an otherwise idle server receives a prompt on an accepted connection during the new linger, the prompt handler in packages/server/src/handlers/session.ts returns after admission and SessionExecution.wake, not after model execution finishes. Once that response closes its connection, both waitForAccepted and waitForRequests finish even though the session is still running; the remaining-session check only logs before teardown interrupts it. Consequently, a prompt successfully answered during handover gets at most the linger duration to execute rather than the configured session grace. Make the post-linger wait observe active sessions as well as HTTP responses, within the shutdown deadline.

AGENTS.md reference: AGENTS.md:L179-L179

Useful? React with 👍 / 👎.

Event streams are open-ended and the drain does not wait for them, so a connection that has already
asked for one must not hold the accepted-connection linger either.
@github-actions

Copy link
Copy Markdown

The following comment was made by an LLM, it may be inaccurate:

@github-actions

Copy link
Copy Markdown

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 72 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@dezren39

Copy link
Copy Markdown
Author

🤖 Agent note · triage evidence (issue-sweep-rotating, run 2026-10-29 21:24 CDT) — for the queue lane owning task 2687

Discrepancy found this run: issue #81 is CLOSED (status:done) but this PR — which carries the verified fix — is still OPEN and the fix is not on main.

Suggested resume for the owning lane: merge origin/main into queue/2687-81-systemd-handover, confirm check:generated clean, merge this PR, and note on #81 that the fix landed via this PR after the issue was closed.

@dezren39

dezren39 commented Nov 1, 2026

Copy link
Copy Markdown
Author

Stale-PR warden: no activity for 3 days. Resolution task: #1300 — this PR should be merged, fixed, or closed.

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.

serve handover: verify under a real systemd user session and close the teardown race

1 participant