Repository navigation
feat(relay): fence and drain file and git response streams on shutdown - #24185
Conversation
|
| await responses | ||
| await fileStreams |
There was a problem hiding this comment.
Shutdown can wait forever When shutdown starts while a client socket has stopped draining, a file terminal frame or git chunk write can remain unsettled. These awaits block until the stream drains finish, but the dispatcher writer is closed only afterward. The close that would settle the write is therefore never reached, so shutdown cannot complete.
| const failures = results.filter((result) => result.status === 'rejected') | ||
| if (failures.length > 0 || this.streams.size > 0) { |
There was a problem hiding this comment.
Recovered close still fails shutdown If a handle close fails during disposal but an in-flight pump retries and closes it before its operation finishes,
failures still contains the first rejection. Disposal reports relay_file_stream_shutdown_incomplete even though no handles remain, causing the lifecycle to defer shutdown until another attempt.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the stream-shutdown slice of the Node runtime plan (Design D9/T1), reviewed at 5da66b8 against 04c9331.
RelayStreamRegistrydrain and fence. Adds adisposedflag,beginOperation()/finish()refcounting, a coalesced asyncrelease(EBADF tolerated, other close failures retained for retry), and adisposeAll()that aborts every stream, awaits each close and every admitted operation, and rejects withrelay_file_stream_shutdown_incompletewhile a handle remains.- File-stream pump accounting.
readRelayFileStreamMetadatawraps metadata prep in one operation and the detached pump in another; terminal-frame slot reservation moved verbatim intofs-stream-terminal-frame-slots.ts, where each slot is itself a registry operation so shutdown also waits for undelivered terminal frames. GitResponseStreamRegistrydrain and fence. Fences new streams viadisposed, tracks in-flight pumps inpendingPumps, addsdisposeAllAndWait, and suppressesgit.responseEnd/git.responseErroronce a pump is aborted or stale.disposeOwnedProcesses. Kicks off both registry drains, aggregates their failures, and rejectsrelay_owned_process_shutdown_incompleteso the grace lifecycle defers shutdown and retries instead of exiting with a file handle still open.- Tests. New shutdown suites for both registries and for
disposeOwnedProcesses, plus an fs-handler case for an undelivered terminal frame. All 33 tests in the four touched files pass.
I traced the hang/leak candidates in this change independently: disposed is set synchronously before the first await, so the operations/pendingPumps snapshots are always complete; the register fence and the post-register beginOperation share a gap-free synchronous window, so no slot or descriptor can be acquired without being drained; and the slot-release closure is idempotent and reached on every internal path. The only path that can hold a drain open is a sink that never settles — the "client socket stops draining" case the PR documents. Wire is unchanged.
ℹ️ Shutdown deferral has no upper bound
disposeOwnedProcesses now rejects when a drain cannot finish, and relay-grace-lifecycle.ts only re-arms a retry while readSocketClientCount() === 0. If a client that stops draining keeps a control write or a bulk notifyBulk unsettled, no retry is armed and the daemon lingers indefinitely. This is the behaviour the PR documents as known, so no change is requested here — flagging only so the boundary is explicit.
Technical details
# Deferred shutdown retry is client-count-gated
## Affected sites
- `src/relay/relay-runtime-services.ts:126` — `disposeOwnedProcesses` now rejects `relay_owned_process_shutdown_incomplete` when a registry drain fails.
- `src/relay/relay-grace-lifecycle.ts:132-140` — on rejection it clears `shutdownInFlight` and re-arms a `shutdown-deferred` grace only when `readSocketClientCount() === 0`.
- `src/relay/relay-grace-branch.ts:121-138` — the `shutdown-deferred` branch is bounded by `idleRelayGraceMs`, so retries do continue while the relay is clientless.
## Required outcome
None if indefinite deferral is the intended posture. Otherwise, cap the retries (or force exit after N) so a permanently unstuck sink cannot keep the daemon alive.
## Open questions for the human
- Is a bound on deferral wanted, or is unbounded deferral intended until the T2 lifecycle port revisits this?DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
04c9331 to
42e1b12
Compare
5da66b8 to
4446ea6
Compare
42e1b12 to
808078a
Compare
Third T1 slice of the #16741 port: the relay's two detached stream producers can now be fenced and awaited, not just flagged. RelayStreamRegistry (fs.readFileStream): - disposeAll permanently fences register/beginOperation (relay_file_stream_shutdown_fenced), waits for every close and for every admitted operation, and rejects with relay_file_stream_shutdown_incomplete while any handle is still unclosed, so a later disposeAll retries only the failed handles. - release coalesces concurrent callers onto one close, tolerates only EBADF as already-closed, and keeps a failed-close entry (aborted) for retry. - beginOperation / releaseUnregisteredHandle let work that opens a handle before registration be drained too (wired in the next commit). GitResponseStreamRegistry: disposeAll fences startStream (relay_response_stream_shutdown_fenced); disposeAllAndWait also awaits every scheduled or in-flight pump, and an aborted pump no longer publishes git.responseEnd after its final chunk write settles. reserveTerminalFrameSlot moves unchanged into fs-stream-terminal-frame-slots and now holds a registry operation per slot, so shutdown also waits for undelivered fs.streamEnd/fs.streamError frames. No wire change: no new methods, fields, or opcodes. Mixed versions are unaffected; a fenced relay answers with an ordinary request error. Porting note (source: #16741 head a68b6f3, merge-base 277c289; T1 per the track manifest): - Taken verbatim: fs-stream-registry delta, git-response-stream delta, fs-stream-registry-shutdown.test, git-response-stream-shutdown.test. - Adapted: Promise.withResolvers replaced with plain promises (the relay bundle targets node18). fs-stream-terminal-frame-slots is extracted from the reserveTerminalFrameSlot that main already has inline in fs-handler-file-read (keeping its rationale comment) rather than added as a second copy. The EBADF check uses a type guard instead of a cast, and the tests' stub casts carry SAFETY rationales. - Dropped: nothing.
…processes Wires the stream registries into relay shutdown. The dispatcher's RelayWorkAdmission drain (#24181) ends once fs.readFileStream answers its metadata or a handler answers a git response-stream sentinel; the pumps and file descriptors behind those answers are detached producers it does not see. RelayRuntimeServices.disposeOwnedProcesses now also awaits GitResponseStreamRegistry.disposeAllAndWait and FsHandler.disposeFileStreams, and rejects with relay_owned_process_shutdown_incomplete if either fails, so the grace lifecycle defers shutdown (its existing catch path) instead of exiting with an unclosed fd; the next attempt retries only failed handles. fs.readFileStream now holds a registry operation across metadata preparation (including the binary-prefix probe handle) and across each pump, closes pre-registration handles through releaseUnregisteredHandle, releases its terminal-frame slot even when the handle close fails, and publishes no terminal frame for a stream that was aborted or went stale while its last read was in flight. Behavior today: the grace/SIGTERM shutdown path already calls disposeOwnedProcesses after PTY disposal, so a shutdown now also waits for in-flight stream pumps and queued terminal frames (which settle on client close or dispatcher disposal). After disposal, stream requests are refused with a fenced error; if shutdown is deferred by a failed close, the relay stays up with streams fenced, matching #16741's one-way drain. No wire change. Porting note (source: #16741 head a68b6f3, merge-base 277c289; these files are T2 in the track manifest, taken here only as far as the stream drain needs them): - Taken verbatim: fs-handler-file-read delta, fs-handler-utils isBinaryFilePrefix releaseHandle parameter, fs-handler disposeFileStreams. - Adapted: relay-runtime-services takes only the responseStreams field and the response/file-stream drains in disposeOwnedProcesses. Skill-upload and AI Vault cleanup stay log-and-continue rather than joining the aggregate failure. Added a fs-handler-stream test proving disposeFileStreams waits for an undelivered terminal frame and fences new reads. - Dropped (left for T2): agentExecHandler disposal, disposeWatchers, the ownership-transfer adapter/capture wiring, and the relay-grace-lifecycle beginWorkDrain -> disposeOwnedProcesses -> beginWorkDrain sequence.
disposeOwnedProcesses now waits for the git response-stream and file-stream drains and rejects with relay_owned_process_shutdown_incomplete when either fails, but nothing exercised that path: relay-daemon tests stub the whole runtime. These tests pin that shutdown is not acknowledged before either drain settles, that both drain failures are aggregated after every owner was attempted (and a retry succeeds), and that skill/AI Vault failures stay log-and-continue in this slice. Porting note (source: #16741 head a68b6f3, src/relay/relay-runtime-owned-process-shutdown.test.ts, T2 in the track manifest): - Taken: the prototype-stub fixture, the "does not acknowledge cleanup before %s have settled" case (responses, fileStreams), and the no-vault-service case. - Adapted: Promise.withResolvers replaced with a plain promise; the fixture cast carries a SAFETY rationale; the retry case targets the stream drains instead of skill/vault, and a new case pins skill/vault as log-and-continue to match this slice's adaptation. - Dropped (left for T2): agentExecHandler and disposeWatchers cases, and skill/vault aggregate-failure cases.
4446ea6 to
3977d5b
Compare
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (1)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (11)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughFile and Git response stream registries now track active work and fence new streams during shutdown. File stream setup and cleanup release handles through the registry, and terminal-frame slots remain tracked until released. Runtime-owned process cleanup awaits both stream drains and reports their failures together. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The stream drains have no identified blocking defect. Disconnected clients release pending terminal frames; a connected client that stops draining can intentionally delay shutdown. Mergeable subject to normal checks. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to The change improves cleanup but already affects production shutdown. A live client with stalled output can postpone graceful shutdown and therefore delay restart or rollback. Disconnect releases pending writes, and the supported impact is limited to one relay process. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
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. Comment |
* Revert "feat(orcad): source-side dormant export of a relay-hosted SSH target (#16741 T6-8) (#24519)" This reverts commit 783101b. * Revert "feat(ssh): update, roll back, recover and stop a managed orcad server (#16741 T6-5 follow-up) (#24463)" This reverts commit 38c2d1d. * Revert "feat(ssh): deploy and pair an empty managed orcad server over SSH (#16741 T6-5) (#24453)" This reverts commit 8b76683. * Revert "fix(ssh): orcad GC honors the activation journal; readiness requires proven daemon coverage (#16741 T6 follow-up) (#24451)" This reverts commit d3f8c50. * Revert "feat(ssh): remote orcad stop by request file and journaled decommission (#16741 T6-4) (#24449)" This reverts commit 43d9b43. * Revert "feat(orcad): supervisable server: stop requests, managed stop receipts and a lifetime that keeps its lock on failed teardown (#16741 T6-3) (#24433)" This reverts commit b093d3a. * Revert "feat(ssh): crash-safe orcad activation, rollback and recovery (#16741 T6-2) (#24423)" This reverts commit 1a9ac0e. * Revert "feat(runtime): SSH access links for paired servers in a downgrade-safe sidecar (#16741 T5-1+T5-2) (#24420)" This reverts commit 99db2bf. * Revert "feat(relay): capability-gated owner reset with a durable preparation journal (#16741 T3 R1) (#24418)" This reverts commit 34a582b. * Revert "feat(ssh): track connection-manager drains, test probes and provider continuations (#16741 T2 P3+P8a) (#24407)" This reverts commit d53063d. * Revert "feat(daemon): idle retirement, session census and recovery-only provider (#16741 T2 P4b) (#24409)" This reverts commit ff212db. * Revert "feat(ssh): add pty.resumeClient and split SSH PTY process listing (#16741 T2 P5+P6) (#24414)" This reverts commit 92cb717. * Revert "feat(relay): await owned watcher and agent children on shutdown (#16741 T2 P1) (#24400)" This reverts commit 6b36e4f. * Revert "feat(session): retry failed renderer session writes and verify local folder PTYs (#16741 T2 P9) (#24406)" This reverts commit d23ecef. * Revert "feat(ssh): remote orcad primitives on the pinned Node runtime (#16741 T6-1) (#24419)" This reverts commit dd87ae5. * Revert "fix(runtime): fence runtime-environment subscriptions and status probes by identity (#16741 T5-3) (#24421)" This reverts commit ece9e4d. * Revert "feat(orcad): migration manifest and dormant-state contracts (#16741 T6-7) (#24422)" This reverts commit 3fbdaba. * Revert "feat(ssh): wire SshConnection through the work and transport close ledgers (#16741 T2 P2) (#24401)" This reverts commit 4e8edc8. * Revert "feat(profiles): carry markdown frontmatter visibility in project transfers (#16741 T2 P7) (#24405)" This reverts commit 60c9326. * Revert "fix(runtime): project the PTY incarnation onto mobile session tabs (#24413)" This reverts commit 99e0303. * Revert "feat(daemon): tag daemon stream data with the PTY incarnation id (#16741 T2 P4a) (#24402)" This reverts commit 817af76. * Revert "feat(ssh): port the SSH connection work ledger and transport close ledger (#16741 T2) (#24210)" This reverts commit c991893. * Revert "feat(relay): fence and drain file and git response streams on shutdown (#24185)" This reverts commit dc08ffe. * Revert "refactor(runtime-rpc): extract the Node WebSocket lifecycle; opt-in pinned port (#24186)" This reverts commit a789233. * Revert "feat(relay): route relay handlers through work admission; producer publication drain (#24181)" This reverts commit 0b812bd. * Revert "feat(relay): land the #16741 T1 seam (work drain, publication drain, release gate) (#24156)" This reverts commit 3aa2d3a. --------- Co-authored-by: m4air <m4air@Mac.localdomain>

ELI5
Before the relay shuts down, it now waits for in-flight file and git response streams to finish and close their file handles. Once shutdown starts, new streams are refused. This ports the stream-shutdown slice of #16741. Nothing in production starts a drain yet.
What Changed
RelayStreamRegistry:disposeAllblocks new streams and waits for every close and admitted operation.relay_file_stream_shutdown_incompleteif a handle stays open.GitResponseStreamRegistry:disposeAllAndWaitfor in-flight pumps.git.responseEnd.fs-stream-terminal-frame-slots.ts. Each slot now holds a registry operation.disposeOwnedProcesses: now waits for both stream drains. If either fails, shutdown is postponed instead of exiting with a handle still open.Promise.withResolvers, since the relay targets node18; type guards instead of casts.Why
Design D9, T1: this lets a later shutdown or reset (T3) drain safely without leaking file handles.
Linked Issue
Part of the Node runtime plan (Phase 3). Stacked on #24181.
Visual Proof
N/A. There's no user-visible change yet.
Testing
37 stream tests pass.
Added in review: coverage for the stream drains in
disposeOwnedProcesses.The full relay suite is green apart from 3 PTY tests that time out under full-suite load; they pass on their own.
Known behaviour, same as feat(orcad): consolidate Bun runtime and SSH/Remote migration #16741: a connected client whose socket stops draining can keep shutdown open.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.