Repository navigation
fix(server): preserve newer runtime state on shutdown - #9045
reubengrussell wants to merge 1 commit into
Conversation
| (state) => | ||
| state === null | ||
| ? Effect.void | ||
| : clearPersistedServerRuntimeState(config.serverRuntimeStatePath, state).pipe( |
There was a problem hiding this comment.
🟠 High src/server.ts:518
The old server can delete a newer server's runtime-state file: clearPersistedServerRuntimeState verifies ownership with readPersistedServerRuntimeState, then performs fs.remove separately, allowing an overlapping startup to replace the file between those operations. This removes the active endpoint record and breaks discovery; the check-and-remove must be atomic (or otherwise revalidate ownership immediately before deletion).
Also found in 1 other location(s)
apps/server/src/serverRuntimeState.ts:96
The owner check is a time-of-check/time-of-use race: after
readPersistedServerRuntimeState(path)confirms that the old instance owns the file, a newer server can atomically replace the state beforefs.remove(path)runs. The old shutdown then unlinks the newer server's record, recreating the endpoint-discovery failure this guard is intended to prevent.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/server.ts around line 518:
The old server can delete a newer server's runtime-state file: `clearPersistedServerRuntimeState` verifies ownership with `readPersistedServerRuntimeState`, then performs `fs.remove` separately, allowing an overlapping startup to replace the file between those operations. This removes the active endpoint record and breaks discovery; the check-and-remove must be atomic (or otherwise revalidate ownership immediately before deletion).
Also found in 1 other location(s):
- apps/server/src/serverRuntimeState.ts:96 -- The owner check is a time-of-check/time-of-use race: after `readPersistedServerRuntimeState(path)` confirms that the old instance owns the file, a newer server can atomically replace the state before `fs.remove(path)` runs. The old shutdown then unlinks the newer server's record, recreating the endpoint-discovery failure this guard is intended to prevent.
ApprovabilityVerdict: Would Approve Macroscope's review found this PR approvable — This is a focused three-file shutdown bug fix that preserves newer runtime-state records while retaining existing unconditional cleanup for other callers, with targeted tests and no schema, deployment, security, billing, or default changes. An unresolved High-severity finding still identifies a possible check-then-remove race in the ownership guard, which is a separate blocking correctness concern. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
|
This fixes one of the triggers in #5749. There, three reports (Sep 15, 24 and 26) describe a stopped server deleting the live service's Setup: two worktrees: Sequence:
Each build gave the same result in 3 out of 3 runs. This matches what I saw on a real host (0.0.45, macOS desktop → SSH → systemd service). The launcher adopted the service at 14:28, and the old managed server shut down at 14:28:58. At 14:40 a stale-tunnel stop and relaunch started a managed server on 3774, while the service was still healthy ( Not covered:
|
What Changed
pidandstartedAtstill match that owner.Why
Two T3 servers can overlap briefly during an SSH reconnect. An older server previously removed the shared
server-runtime.jsonduring shutdown, even after a newer server replaced it. This left endpoint discovery without the current server record.The owner check keeps the newer record while preserving existing stale-state cleanup behavior.
Checklist
Validation:
vp test run apps/server/src/serverRuntimeState.test.ts: 8 tests passed.vp run --filter t3 typecheck: passed with existing suggestions in unrelated files.Note
Medium Risk
Changes shared runtime-state lifecycle on shutdown; incorrect owner matching could leave stale files or skip needed cleanup, though behavior is covered by new tests and CLI paths stay unconditional.
Overview
Fixes a race where an older server process could delete
server-runtime.jsonon shutdown after a newer server had already written it (e.g. brief overlap on SSH reconnect), breaking endpoint discovery.clearPersistedServerRuntimeStatenow accepts an optional owner (pid+startedAt). When provided, it reads the file and skips removal unless the on-disk record still matches that owner. Callers that omit the owner (e.g. CLI stale cleanup) still delete unconditionally.The server
acquireReleasehook returns the persisted state (ornullwhen no port was written) and passes it into shutdown cleanup so only the stopping instance clears its own record.Reviewed by Cursor Bugbot for commit 0d22f86. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix
clearPersistedServerRuntimeStateto skip clearing newer server state on shutdownexpectedOwnertoclearPersistedServerRuntimeState(path, expectedOwner)so it only deletes the persisted file whenpidandstartedAtstill match the current instancenulland skips the clear entirelyclearPersistedServerRuntimeStatesignature gains a second optionalexpectedOwnerparameter; callers that omit it keep the old unconditional-delete behavior📊 Macroscope summarized 0d22f86. 2 files reviewed, 2 issues evaluated, 1 issue filtered, 1 comment posted
🗂️ Filtered Issues
apps/server/src/serverRuntimeState.ts — 0 comments posted, 1 evaluated, 1 filtered
readPersistedServerRuntimeState(path)confirms that the old instance owns the file, a newer server can atomically replace the state beforefs.remove(path)runs. The old shutdown then unlinks the newer server's record, recreating the endpoint-discovery failure this guard is intended to prevent. [ Cross-file consolidated ]