Skip to content

sbx-mgr: pause writes only over the state it expects - #417

Merged
ac-pplx merged 1 commit into
ac/sbxmgr-sw-pr5a-snapshotsfrom
ac/sbxmgr-sw-pr2-pause
Oct 14, 2026
Merged

ac-pplx merged 1 commit into
ac/sbxmgr-sw-pr5a-snapshotsfrom
ac/sbxmgr-sw-pr2-pause

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #421 (on #416, #413, #418, #438 and main). Base: ac/sbxmgr-sw-pr5a-snapshots @ 9f30aa4. This is plan §5 PR 2: one commit on top of #421. Stack, bottom to top: #438 → #418 → #413 → #416 → #421 → #417 → #414. #372 and #388 have merged to main.

All state here is sbx-manager's node-local SQLite sandboxes.state, not the control-plane Postgres public.sandboxes.lifecycle.

What merging does

Converts pause's state writes to update_state_from

All of these are in service/grpc_lifecycle/pause_resume.rs. Outcomes go through #438's shared helpers.

Call site Before After (edge, helper)
pause: begin update_state(Pausing) → require_state_write PAUSE_BEGIN ({started} → pausing) → require_state_write_from
pause: rollback after a failed guest preparation update_state(Started), refusal only logged PAUSE_ROLLBACK ({pausing} → started) → state_write_result. A superseded rejection is returned through StateWriteRejected::into_status; any other rejection is logged as before
pause: best-effort rollback after a node-manager connect failure update_state_best_effort(Started) PAUSE_ROLLBACK → update_state_from_best_effort
pause_with_reconciliation: best-effort rollback when the VM is still running after the last attempt update_state_best_effort(Started) PAUSE_ROLLBACK → update_state_from_best_effort
pause_with_reconciliation: best-effort rollback when the retry's guest preparation fails update_state_best_effort(Started) PAUSE_ROLLBACK → update_state_from_best_effort
pause: completion update_state(Paused) → require_state_write PAUSE_COMPLETE ({pausing} → paused) → require_state_write_from

Two small local helpers remain:

  • superseded(rejected, edge) is the found-based test !edge.from().contains(found). A guard refusal of a stopping row is superseded too, because the generic guards answer before the from-set.
  • roll_back_pause_best_effort wraps update_state_from_best_effort(PAUSE_ROLLBACK) and returns the rejection only when it was superseded.

Unchanged: side columns, the caller's mutex scopes, and the artifact-lock scope. update_state_from adds its own IMMEDIATE transaction around its reads and the UPDATE in autocommit, which update_state did not. That is harmless on the single connection.

#438's helpers: #418 and #421, below this PR, already removed #[cfg_attr(not(test), expect(dead_code, …))] from require_state_write_from, StateWriteRejected (and its impl), StateWriteGuard, state_write_result and update_state_from_best_effort; pause now calls them in production too.

Fixes plan finding 1: pause over a concurrent Stop

Pause holds only the artifact lock while it awaits guest preparation and node-manager. Stop takes the cleanup fence and admits a pausing row: #372's stop_admission_answers_every_source_state pins pausing as Admitted. A Stop that settled stopped in that window used to be overwritten, by started (rollbacks) or paused (completion). That left a row with its runtime fields cleared and no VM. Now stopped, created, suspended and every other state outside the edge's from-set refuse the write, and pausing must be the found state.

The quarantine-miss path. This is the likeliest Stop-during-pause path:

  1. Stop kills the VM, so pause_sandbox fails and the VM-state probe errors.
  2. pause_with_reconciliation reaches mark_quarantined_best_effort(Pausing).
  3. That compare-and-set refuses the moved row. Its result used to be discarded, and Pause said "reconciled to quarantined" for a row that was stopping or stopped.

Now a refused quarantine re-reads the row. mark_quarantined_best_effort answers false only when its own write did not apply, so a re-read that is no longer pausing means another operation owns the row. That includes quarantined, which another writer must have made. Pause then returns StateWriteRejected::FromState { found } and answers the canonical superseded Aborted. A poisoned mutex, a failed read, a row still pausing (a database error) or a missing row still reports quarantined.

Behavior changes, including client-visible ones

What Pause returns when it loses to another operation. It returns Aborted with #438's canonical text, which the API maps to 409:

  • FromState: "<context>: sandbox state write was superseded: sandbox is <found>"
  • a guard refusal of a moved row (stopping, deleting, cleanup_required, cleanup_failed → cleanup-owned; quarantined): "<context>: sandbox state write was not applied: superseded_by_cleanup_episode | blocked_by_quarantine"
Exit Context Before, over a moved row After
Rollback after failed guest prep roll back failed pause preparation row overwritten to started (or guard-refused); Internal "guest snapshot preparation before pause failed: …" row kept; Aborted, canonical text
Rollback after connect failure roll back pause after node-manager connect failure row overwritten to started; Internal "cannot connect to node-manager: …" row kept; Aborted, canonical text
pause_with_reconciliation rollbacks (both) reconcile unconfirmed pause row overwritten; Internal "pause failed; sandbox reconciled to started" row kept; Aborted, canonical text
pause_with_reconciliation quarantine refused reconcile unconfirmed pause Internal "pause failed; sandbox reconciled to quarantined", row not quarantined Aborted "…: sandbox state write was superseded: sandbox is <found>" (from the re-read, so always the FromState text)
Completion complete pause row overwritten to paused; Ok row kept; Aborted, canonical text
Begin begin pause guard refusals Aborted "…: sandbox state write was not applied: <outcome>"; other states applied guard refusals unchanged; any other non-started row is Aborted "begin pause: sandbox state write was superseded: sandbox is <found>"

Unchanged:

  • Uncontended pauses and every admission refusal (pause_refuses_rows_that_cannot_enter_pausing_before_writing_state).
  • A guard refusal of a row still pausing, and a missing row: the guest-prep rollback logs a warning and returns the original Internal, and the best-effort rollbacks return started, as before.

Message changes:

  • New text: sandbox state write was superseded: sandbox is <found>, for from-state refusals at every exit.
  • New Aborted answers replace Internal (both rollbacks, both reconciliation rollbacks, the refused quarantine) and Ok (completion) over a moved row.
  • The legacy guard texts are unchanged.

pause_with_reconciliation now returns Result<SandboxState, StateWriteRejected>. Err is a superseded rejection from a rollback or from the quarantine re-read; it is never Missing.

  • The suspend auto-pause is its other caller, and it fails closed: it continues only on Ok(paused), so no rejection, including one that found paused, lets suspend proceed. It reports the found state, as before, as Internal "auto-pause failed; sandbox reconciled to <state>". This is a small change in suspend.rs.
  • No test pins the suspend side. Its auto-pause calls prepare_guest_for_vm_snapshot_via_spaced_host directly, which dials the host's per-sandbox spaced-host socket, so reaching it would mean adding the seam to suspend.rs, the file sbx-mgr: suspend writes only over the state it expects #414 converts. Rejections are unreachable there today under suspend's fences. The fail-closed property holds by construction: the only continue condition is pause_result == Ok(SandboxState::Paused).
  • The pinned confirmed_node_pause_reports_paused_and_carries_the_execution_fence changes from assert_eq!(state, SandboxState::Paused) to assert_eq!(state, Ok(SandboxState::Paused)). That is a type-only change.

The begin gap

lookup_sandbox and the PAUSE_BEGIN write take separate db_conn() acquisitions of the std mutex on a multithreaded runtime. Another worker can commit between them, for example cleanup admission's blocking task. An unlocked warm start no longer can: #418, below this PR, refuses a started row. The {started} from-set refuses whatever that worker left. A code comment says so.

No test interleaves at that point, so mutation 5 below survives because no seam exists there. That is acceptable:

Edges and allowlist

Test-only seam

prepare_guest_for_pause replaces the two direct prepare_guest_for_vm_snapshot_via_spaced_host calls in this file. Under #[cfg(test)] it checks a tokio::task_local! hook, GUEST_PREPARATION; release builds compile the direct call. A task-local needs no field on SandboxManagerService, and service/mod.rs is at its cargo arch cap.

Tests

The new file is service/grpc_lifecycle/pause_resume/supersession_tests.rs. Each case runs the real Pause RPC on new_with_options(…, for_test_in(tempdir)), with in-memory SQLite, the seam, and either an unbound node-manager or a test-served FakeNodeManager.

The interleaving runs at the guest-preparation call before the write under test. Its whole-database snapshot is then compared with #372's assert_unchanged.

Expected outcomes are written out per state:

Exit Write under test Interleave at prep call
PreparationFails rollback after failed guest prep 1 (fails)
NodeUnreachable best-effort rollback after connect failure 1
NodeConfirms completion 1
RetryPreparationFails reconciliation, retry-prep rollback 2 (fails)
VmStaysRunning reconciliation, last-attempt rollback 3
VmUnobservable (new) reconciliation quarantine: PauseSandbox and GetFirecrackerVmState both fail 1
  • pause_leaves_a_row_a_concurrent_stop_settled (red/green, finding 1). This runs plan finding 1's Stop at each of the six exits, exactly sbx-mgr: characterize the state writers before the state-write helper (tests only) #372's reproduction: cleanup admission's transaction (update_state_superseding_activation_in_transaction(Stopping) + set_cleanup_intent(MarkStopped)), then complete_row_intent_cleanup_for_job(MarkStopped).
    • expected: Aborted "<context>: sandbox state write was superseded: sandbox is stopped", and the database exactly as Stop left it.
  • pause_writes_only_over_its_own_pausing_row. All 18 states × 6 exits, using force_state (now pub(crate)).
    • pausing is the accepting control: rolled back to started with today's Internal, Ok with paused, or quarantined with Internal "pause failed; sandbox reconciled to quarantined".
    • Every other state answers the canonical Aborted text in full, with the database unchanged.
    • VmUnobservable over a row another writer quarantined answers Aborted "reconcile unconfirmed pause: sandbox state write was superseded: sandbox is quarantined", like every other exit.

Literal red run. I applied only the seam (behavior-neutral) and this test file onto #388's head 2fc6457, in a disposable worktree that was then removed. Both tests fail on the whole-database diff, for example:

PreparationFails, Stop: a refused write must leave every row unchanged; changed: [… sandboxes …]

The row is overwritten to started.

Unchanged and green: #372's matrix and accepted-shape tests, #438's tests, and the plan's pins: pause_refuses_rows_that_cannot_enter_pausing_before_writing_state, teardown_suspend_pause_failure_recovery_allows_same_generation_retry, the suspend tests, and confirmed_node_pause_… (type-only change above).

Mutation proof

Each mutation ran alone, from one backup, against supersession_tests, pause_refuses_rows and confirmed_node_pause, and was then restored. The final restore was byte-compared with the backup.

# Mutation Result (first failing case)
1 Restore all three generic writes (prep rollback, best effort, completion) both tests fail: PreparationFails, Stop: "a refused write must leave every row unchanged"
2 Restore update_state(Started) at the guest-prep rollback only both fail: PreparationFails row diff
3 Restore update_state_best_effort(Started) in the best-effort rollback only both fail: NodeUnreachable row and dirty-revision diff
4 Restore update_state(Paused) at completion only both fail: NodeConfirms row and dirty-revision diff
5 Restore update_state(Pausing) at begin survives. No seam exists between lookup_sandbox and the begin write (see "The begin gap"). The from-set is proven at the DB level by #388's edge-binding test
6 PAUSE_ROLLBACK from-set → StateSet::ALL both fail: PreparationFails row diff
7 PAUSE_COMPLETE from-set → StateSet::ALL both fail: NodeConfirms row diff
8 superseded always false both fail: PreparationFails, Internal where Aborted expected
9 Pause reads a reconciliation rejection as started both fail: RetryPreparationFails, Stop, Internal "…reconciled to started"
10 Guest-prep rollback keeps Internal when superseded both fail: PreparationFails, Stop
11 Discard mark_quarantined_best_effort's bool again both fail: the Stop test at VmUnobservable, Stop and the sweep at VmUnobservable, Force(Creating), each Internal "…reconciled to quarantined"
12 A guard refusal of a moved row is not superseded sweep fails: PreparationFails, Force(Stopping)
13 Restore the quarantined exclusion in the quarantine re-read sweep fails: VmUnobservable, Force(Quarantined), Internal "…reconciled to quarantined"

Overlaps with the other PRs in the stack

Findings not acted on

  • A quarantine that fails for a database error or poisoned mutex, with the row still pausing, still reports quarantined. This is unchanged.

Decisions made without an operator

  • The reconciliation rollbacks return the real rejection (Result<_, StateWriteRejected>), so guard refusals there keep the legacy guard text. Only the quarantine re-read builds FromState { found }, as requested.
  • The suspend auto-pause fails closed on any Err and reports the found state in its existing Internal message. Its Missing arm (never produced) reports started, which is what a missing row's rollback reported before.
  • Stop in the race test is sbx-mgr: characterize the state writers before the state-write helper (tests only) #372's DB-level sequence. The real Stop RPC stats the fixed sidecar pid path. The call sites themselves run through the real Pause RPC.

Validation of the restacked stack

The stack was restacked linearly on main (b18051f) after #372 and #388 merged: #438 → #418 → #413 → #416 → #421 → #417 → #414. The per-PR validation further down ran on this PR's pre-restack head.

This PR's head (51bee12): pull CI is green. fmt, workspace clippy --all-targets --all-features -D warnings and cargo arch (0 required findings) all pass. SPACE_CI_REQUIRE_TEST_DEPS=1 cargo nextest run --locked -p pplx-space-sbx-mgr --retries 0 passed 1649/1649, with 0 dependency skips. Tested at 12cb312; the final head differs from it only in architecture-baseline.toml, which only cargo arch reads, and cargo arch was rerun on the final head. Kind Pull CI ran on #417, which contains this PR: all 4 Firecracker smoke shards passed.

The top of the stack (#414 @ 9a0a251, which contains this PR):

Check Result
sbx-mgr nextest ×3 1650/1650 each, 0 dependency skips
Whole-workspace nextest 8985/9021 passed, and every API crate passed. The 36 failures are all outside sbx-mgr. 34 fail identically on main: spaced-guest and spaced-host-cli process tests (the test box can't switch identity) and one net-gateway test. The other 2 are common-grpc retry-deadline tests that pass on rerun on both heads.
The stack's 30 new or changed tests, ×30 900/900
cargo test -p pplx-space-sbx-mgr (shared process) 1650/1650
Mutation spot-check on the top head #417's quarantine-miss mutation and #418's warm-start admission mutation are both caught
Kind, fresh cluster; the deployed sbx-mgr binary is from this head smoke-kind-e2e.sh passed. smoke-kind-typed.sh all: native 23/23; ECW 76 passed, 2 infra errors. local.ecw_template_build_from_oci_boots passed, and so did the leader-election brownout.
The 2 ECW infra errors node_group_boundary_schedules and node_selector_extras_render_and_enforce. Both scenarios restart the API Deployment, and the same failure family appears on main in an A/B on the same cluster. In the failed runs the kill never reached sbx-mgr.
Lifecycle race soak on Kind 11 concurrent operation pairs × 40 iterations, on the stack and on main on the same fresh cluster, checking that sbx-mgr's SQLite sandboxes.state, control-plane Postgres lifecycle and the VM agree. No change-induced violation: across 880 sandboxes, no quarantine, panic, restart or stuck state. Every violation reproduces on main with the same signature: an ECW pause that loses to Stop answers 503 instead of 409; after Suspend vs Delete, the node row can outlive the control-plane row until the next eviction pass (pass timing, not the build); and a refused quarantine write after Pause vs Delete is still logged. The soak's 0–200 ms API-level offsets did not reproduce the interleavings this stack closes, so the unit-level race tests remain the direct evidence for those.

Validation on the pre-restack head

Head 5994075, one commit on #438's final head f1d1c9b.

Round 2 (2b6fa52 → 5994075) is a small error-mapping change: the quarantine re-read now treats quarantined as superseded, and suspend fails closed. For it I reran fmt, workspace clippy, cargo arch, the error-policy script and pre-commit (all pass), sbx-mgr nextest once (1632/1632, 0 dependency skips), and mutations 11 and 13 (both caught). Per the owner, Kind was not rerun, because Kind cannot reach this path. Kind ran at 3ae9116: the same commit on #438's previous head 7e2e41c. The rebase to f1d1c9b changed only #438's own files (git diff 3ae9116 2b6fa52 is exactly #438's delta: require_state_write_from now returns Result<(), Status>), so per the owner only nextest was rerun after it.

Check Result
cargo fmt --all -- --check pass (at both heads)
cargo clippy --workspace --locked --all-targets --all-features -- -D warnings pass, 0 warnings (at both heads)
scripts/check-rust-error-policy.sh pass
pre-commit (Gitleaks) pass at the amend (3ae9116); after the cherry-pick, pre-commit run --from-ref f1d1c9b --to-ref HEAD passes
cargo arch pass: 0 required findings (39 advisories, none in files this PR touches), at both heads
sbx-mgr nextest × 3 at 3ae9116 (SPACE_CI_REQUIRE_TEST_DEPS=1, SPACE_TEST_DEP_SKIP_LOG set) 1632/1632 each run, 0 dependency skips; 1 skipped is the #[ignore] privileged transfer_directory test, unrelated
sbx-mgr nextest × 1 at 2b6fa52 after the final rebase 1632/1632 passed, 0 dependency skips
Round 2 at 5994075: fmt, workspace clippy -D warnings, cargo arch, error-policy script, pre-commit all pass (arch: 0 required findings)
Round 2 at 5994075: sbx-mgr nextest × 1 (SPACE_CI_REQUIRE_TEST_DEPS=1, skip log set) 1632/1632 passed, 0 dependency skips
Round 2: mutations 11 and 13 both caught
Literal red run: seam and test only, on 2fc6457 both new tests fail on the whole-database diff (row overwritten)
Mutation proof (12 mutations above) 11 caught; M5 (begin) survives because no seam exists there, as documented
Public-safety review of the commit diff no findings in the reviewed scope
Kind: fresh cluster (none existed) via smoke-kind-e2e.sh with BOOTSTRAP_TIMEOUT=1800 DATA_PLANE_URL=http://127.0.0.1:8090 Tilt quiet (0 pending or in-progress builds); e2e PASS 7/7
Kind: provenance artifact built at 00:36:21Z, after the 00:30:26Z commit, from rust-src (pause_resume.rs byte-identical to this worktree). All 4 sbx-mgr pods run space-sbxmgr-svc sha256 26d0ce1b…78616, identical to the artifact. The binary contains reconcile unconfirmed pause (new at this head; the previous head's roll back unconfirmed pause is absent) and sandbox state write was superseded: sandbox is
Kind: smoke-kind-typed.sh all, unsharded, admin token file and SPACE_SMOKE_KIND_COLD_RECEIPTS set as in CI ECW 78 passed / 0 failed / 0 inconclusive; native 23 passed / 0 failed / 0 inconclusive; exit 0 on the first run (no rerun or classification needed). A worker sbx-mgr logged 4 uncontended pause_requested → pause_completed, 0 superseded
Kind teardown infra/space/kind/scripts/teardown.sh, exit 0; no clusters left

🤖 Generated with Claude Code

@ac-pplx
ac-pplx force-pushed the ac/sbxmgr-sw-pr2-pause branch from 50186ed to 3ae9116 Compare October 14, 2026 00:32
@ac-pplx
ac-pplx changed the base branch from ac/sbxmgr-state-write-pr1-helper to ac/sbxmgr-sw-pr1b-refusal-status October 14, 2026 00:32
@ac-pplx
ac-pplx force-pushed the ac/sbxmgr-sw-pr2-pause branch 2 times, most recently from 2b6fa52 to 5994075 Compare October 14, 2026 01:49
Pause held only the artifact lock while it awaited guest preparation and
node-manager, and wrote sbx-mgr's node-local SQLite `sandboxes.state`
through the generic `update_state`, which has no expected-state
predicate. Stop takes the cleanup fence, not the artifact lock, and
admits a `pausing` row. A Stop that settled `stopped` in that window was
then overwritten: the rollbacks wrote `started` and the completion wrote
`paused`, leaving a row with cleared runtime fields and no VM (plan
finding 1). This is not the control-plane Postgres
`public.sandboxes.lifecycle`.

Pause's writes now go through `update_state_from` with the existing
edges, and its outcomes through the shared `require_state_write_from`,
`StateWriteRejected` and `update_state_from_best_effort`:

- begin: `PAUSE_BEGIN` ({started} -> pausing);
- rollback after a failed guest preparation, the best-effort rollback
  after a node-manager connect failure, and both best-effort rollbacks
  in `pause_with_reconciliation`: `PAUSE_ROLLBACK`
  ({pausing} -> started);
- completion: `PAUSE_COMPLETE` ({pausing} -> paused).

A write that finds the row outside the edge's from-set means another
operation moved it, and Pause answers `Aborted` with the shared
canonical text: "<context>: sandbox state write was superseded: sandbox
is <state>", or `update_state`'s guard message when a generic guard
answered first. This now covers the rollbacks, which used to answer
`Internal`, and the completion, which used to answer `Ok`.

`pause_with_reconciliation` returns the rejection instead of claiming
`started`. When its final quarantine compare-and-set is refused, it
re-reads the row and returns a from-state rejection if the row is no
longer `pausing`, including a row another writer quarantined; this is
the likely path for a Stop that kills the VM mid-pause. A failed read
or a missing row still reports `quarantined`. The suspend auto-pause
fails closed on any rejection, so only pause's own confirmed `paused`
lets it continue, and it still reports the found state as "auto-pause
failed; sandbox reconciled to <state>".

Side columns, the caller's mutex and the lifecycle-lock scopes are
unchanged. `update_state_from` adds its own IMMEDIATE transaction
around its reads and the UPDATE in autocommit, which is harmless on the
single connection.

The edge table drops the `PAUSE_ROLLBACK_AFTER_STOP` and
`PAUSE_COMPLETE_AFTER_STOP` known races, and the `PAUSE_*` doc comments
name callers rather than write functions. The allowlist drops
`stopped -> started` and `stopped -> paused`, which no edge writes now.
These were the last known-race edges, so the `KnownRace` edge kind,
`update_state_from`'s refusal of it and the refused-edge test's
known-race assertion are retired. Pause's rollbacks were the last
callers of the legacy `update_state_best_effort`, which is deleted, and
`activation_support.rs`'s archcheck cap is lowered from 1592 to 1574.

Tests: the Pause RPC runs with a test-only guest-preparation seam (a
task-local, so no test dials the host's spaced-host socket) and an
unbound or fake node-manager. One test runs plan finding 1's Stop at
each of six exits; one sweeps every `SandboxState` at each exit, with
`pausing` as the accepting control. Both fail on the base with only the
seam applied, and when the generic writes are restored.

@nikhilgupta-pplx nikhilgupta-pplx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve. Begin/rollback/completion and both pause_with_reconciliation rollbacks use the right edges; {started} matches the only allowed Started → Pausing. In every refused case Stop/Delete owns the row and the VM, so a paused-VM/refused-row split resolves through that owner. KnownRace retirement is clean, and losing to Stop now gives Aborted/409 instead of Internal.

Non-blocking: the comment at pause_resume.rs:198-224 says mark_quarantined_best_effort returns false only when its own write did not apply, but it also returns false on poison/DB error (runtime_reconciliation.rs). The behavior is pre-existing; please correct the comment (or return a three-way outcome).

On the soak observation: the ECW 503 comes from the control plane's claim_suspend_execution CAS in crates/libs/api, which this stack does not touch, so I don't see it as introduced here.

@ac-pplx
ac-pplx added this pull request to the merge queue Oct 14, 2026
@the-gigi-pplx

Copy link
Copy Markdown
Collaborator

Review — 3 findings

Low

  1. crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume.rs:281 — when the rollback is superseded, the original failure is never logged.
    On the guest-prep exit, a superseded rollback returns rejected.into_status(CONTEXT) and drops e, and nothing logs it: update_state_from here isn't the best-effort writer, so no warning fires. The connect-failure exit at line 302 drops its e the same way; update_state_from_best_effort logs the refusal there, but not the connect error. Before this PR both errors reached the caller in the Internal text. The comment's reasoning, that a Stop killing the VM decided the outcome, is right for the status code. But when preparation fails for its own reason (a flush timeout, say) in the same window as a Stop, nothing records it. A tracing::warn! with %e before each return Err(rejected.into_status(..)) keeps the Aborted answer and the diagnosis.
  2. crates/libs/sbx/mgr/server/src/service/grpc_lifecycle/pause_resume.rs:165 — pause_with_reconciliation has the same match roll_back_pause_best_effort(..) { Some(rejected) => Err(rejected), None => Ok(SandboxState::Started) } twice (here and at line 186). Having the helper return Result<SandboxState, StateWriteRejected> would leave one copy.
  3. crates/libs/sbx/mgr/server/src/db/state_write.rs:513 — removing the KnownRace clause left the doc comment unreflowed, so the line breaks after "A write that matches".

What I checked

  • Both retired races are closed at every pause exit. PAUSE_ROLLBACK_AFTER_STOP (stopped → started) and PAUSE_COMPLETE_AFTER_STOP (stopped → paused) were the only races KnownRace still tolerated. The guest-prep rollback, both best-effort rollbacks and completion now write through PAUSE_ROLLBACK or PAUSE_COMPLETE, whose from-set is {pausing}. A Stop that settled stopped is refused and left as Stop wrote it, and Pause answers Aborted instead of overwriting the row. None of the exits turns the race into a stuck state: the row stays Stop's.
  • The quarantine-miss path. mark_quarantined_best_effort (runtime_reconciliation.rs:107) returns false for a poisoned connection, a database error, or a compare-and-set that didn't apply, and true only when its own write applied. So the re-read's rule holds: a row that has left pausing, including one that's quarantined, belongs to another writer. A poisoned connection or a missing row falls through to Ok(Quarantined), as the body says.
  • Err(Missing) is never produced. superseded is false for Missing, so the rollbacks return Ok(Started), and the quarantine re-read of a missing row yields None. The Missing arm in suspend.rs is unreachable, as its comment says.
  • The suspend auto-pause. It takes try_cleanup_fence before it pauses (suspend.rs, prepare_suspend_snapshot_publication_after_locks), and that's the fence Stop's admission needs. That's why its own generic update_state(Pausing/Started/Paused) writes at lines 360, 372 and 446 don't reopen the Stop race at this head, before PR sbx-mgr: suspend writes only over the state it expects #414 converts them. Its fail-closed pause_result != Ok(SandboxState::Paused) is the only continue condition.
  • Pre-existing, not graded. A guard refusal of a row still pausing returns Ok(Started) from the best-effort rollbacks even though nothing rolled back. The body lists this as unchanged.
  • Public content. I ran the public-content check over the added lines, the commit message and the body, and it found nothing.

Verified against 51bee12aac04. Reproduced locally: cargo clippy -p pplx-space-sbx-mgr --all-targets --all-features -D warnings (clean) and cargo nextest run -p pplx-space-sbx-mgr -p pplx-space-db-test-fixtures --all-features (1660 passed, 0 failed) at this head.

@the-gigi-pplx the-gigi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved. 3 non-blocking findings in the review comment above.

Merged via the queue into main with commit c38d5f3 Oct 14, 2026
25 checks passed
@ac-pplx
ac-pplx deleted the ac/sbxmgr-sw-pr2-pause branch October 14, 2026 20:52
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.

3 participants