Skip to content

sandbox-store: move the node drain's sandbox row writes into the store - #1328

Merged
ac-pplx merged 1 commit into
ac/lifecycle-move-cc-restfrom
ac/lifecycle-move-drain
Nov 3, 2026
Merged

ac-pplx merged 1 commit into
ac/lifecycle-move-cc-restfrom
ac/lifecycle-move-drain

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Mechanical move, no functional change. The sandboxes row writes listed below move into pplx-space-sandbox-store with their SQL byte-for-byte; callers keep their locks, transactions and error handling. Everything else is doc comments. Part of the wave-2 stack (9 of 10, on #1335); it lands with the stack.

Summary

The node drain, its pod-less give-up and the drained-node terminator wrote sandboxes with inline SQL. 16 statements move into a new pplx-space-sandbox-store module, drain, and the terminator's mark-lost moves into the existing lost module. When it flips rows, the terminator's transaction then releases their placement through lost::release_lost_placement; lost::mark_started_lost is the same give-up for started rows, which lost_started_sandboxes runs in its own transaction. Each is one function per transition, on the caller's executor or transaction, and its doc names the fact it writes.

Moved (node_drain/store.rs → sandbox_store::drain, same name unless shown):

  • Operation steps: claim_evacuation_suspend, revert_evacuation_suspend_claim, claim_stopped_evacuation (with StoppedEvacuationClaim); the suspend commit in evacuate_sandbox → commit_evacuation_suspend; the finalize in restore_evacuated_and_finalize → finalize_evacuation_restore; schedule_restore_retry_query → schedule_restore_retry.
  • Observed projection: record_stopped_evacuation_artifact.
  • Inferences: redrive_stranded_suspending_query, mark_sandbox_lost_query, mark_sandbox_lost_on_gone_node_query, adopt_remote_snapshot_on_gone_node_query, mark_sandbox_failed_on_node_query, adopt_remote_snapshot_for_drain_query, abandon_stale_resident_sandboxes_query, fail_resident_sandboxes_on_gone_node_query (each builder keeps its name and gains a binding function).
  • node_drain/store/pod_less_giveup.rs::revert_to_suspended → drain::revert_pod_less_resident_to_suspended (inference, &mut Transaction).
  • drained_node_terminator/store.rs::mark_unrecoverable_sandboxes_lost_query → lost::mark_unrecoverable_sandboxes_lost_query + lost::mark_unrecoverable_sandboxes_lost (inference, &mut Transaction).
  • gone_node_identity_guards is public (the snapshots give-up uses it).

Unchanged:

  • SQL text byte-for-byte (all 17 compared against main); constructions kept.
  • Same binds: PHASE_DRAINING → NodeDrainPhase::Draining (same text). The three lifecycle sets are passed as wire strings; two are the drain's, the resident set is pplx-space-common-svc's.
  • The standalone FOR UPDATE/FOR SHARE reads, candidate lists, node lock, lock timeout, transactions, and the pod-less precheck and its other writes stay with the callers. The in-statement FOR UPDATE SKIP LOCKED of the failed-on-node flip, the stale-resident abandon and the gone-node resident failure moved with their statements. No new dependency edges.

Thin guards, documented, unchanged: the suspend commit's match is unchecked; the claim revert re-checks neither node nor marker; the finalize guards only on restoring; the pod-less revert has no lifecycle/node guard and relies on the precheck under the caller's locks.

sql! locators change. They now name pplx_space_sandbox_store::drain, and pplx_space_sandbox_store::lost for the terminator's statement, in pg_stat_activity and query monitoring.

Docs that named the old locations now name the store functions. sandbox_writes entries removed: node_drain/store.rs 15, pod_less_giveup.rs 3, drained_node_terminator/store.rs 1. node_drain/store.rs's file_size cap (1893) stays.

Validation

  • cargo nextest run -p pplx-space-sandbox-store -p pplx-space-cluster-controller-server -p pplx-space-db-test-fixtures --all-features --no-fail-fast (Postgres running): 1257 passed, 0 skipped.
  • Mutations (not committed): dropping AND NOT {durable} from lost::mark_unrecoverable_sandboxes_lost_query fails mark_unrecoverable_sandboxes_lost_guards and mark_lost_yields_to_concurrent_snapshot_finalize; OR TRUE on the claim's drain_mode guard fails evacuation_claim_refuses_a_node_that_turned_gentle; AND FALSE on the pod-less revert fails 7 pod_less_giveup_tests.
  • cargo clippy --all-targets --all-features -- -D warnings (store, controller server, portability, api), cargo fmt --check, RUSTDOCFLAGS="-D warnings" cargo doc --no-deps (store, controller server): clean.
  • cargo arch against main: 0 required findings (110 advisory, as on main).
  • Test files: only use lines (3 files, +9/−2).
  • Kind, fresh cluster from this commit (up.sh 6m59s, every image at this commit except the Kind-only STS bridge helper): local.forced_drain_evacuates_sandbox passed in 34s, local.gentle_drain_leaves_sandbox_resident in 55s. Postgres statement logging and the controller's logs and sandbox events show that the forced drain changed rows through the evacuation suspend claim, the suspend commit and the restore finalize. The stranded-suspend redrive and the stale-resident abandon ran but matched no row. Stopped evacuation is off on Kind. The give-ups, the retry backoff, the pod-less revert and the terminator's mark-lost matched no row there, so only the Postgres tests cover them. For schedule_restore_retry and the two remote-snapshot adoptions, the Postgres tests bind the builders themselves, so their production binds were checked against main by reading. The full typed suite was not rerun this round.

Severity and risk

Severity: does not apply (refactor). Risk: a moved statement binding differently; SQL and binds checked one by one. Rollback: revert.

Rollout

Old and new controllers issue identical statements and binds; only the locator differs.

Open PRs that will need to rebase

AI assistance: Claude Code wrote the code, ran the validation and drafted this description.

After the wave-2 restack

This PR is one layer of a 10-PR stack, restacked onto main c5fdf0c7 (which includes #826) and then rebased mechanically onto 42a99002, which changed only baseline context lines, in order #1329 → #1336 → #1338 → #1339 → #1342 → #1347 → #1333 → #1335 → #1328 → #1332. The validation earlier in this description ran at pre-restack heads; this layer's head is 8932644f.

@github-actions github-actions Bot added the triage:pending Waiting for a maintainer to triage label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

Thanks for opening this pull request! A SPACE maintainer will take a look.

CONTRIBUTING.md explains how we review outside contributions.

@ac-pplx
ac-pplx force-pushed the ac/lifecycle-move-drain branch from a54232b to 51f4827 Compare November 2, 2026 20:48
@ac-pplx
ac-pplx force-pushed the ac/lifecycle-move-drain branch from 51f4827 to 0beb4a0 Compare November 2, 2026 22:23
@ac-pplx
ac-pplx force-pushed the ac/lifecycle-move-drain branch 2 times, most recently from 482258a to 1e3f5d3 Compare November 2, 2026 23:25
@ac-pplx ac-pplx changed the title sandbox-store: move the node drain's sandbox row writes into a drain module sandbox-store: move the node drain's sandbox row writes into the store Nov 3, 2026
@ac-pplx
ac-pplx changed the base branch from main to ac/lifecycle-move-cc-rest November 3, 2026 03:50
@ac-pplx
ac-pplx added this pull request to stack #1382 November 3, 2026 03:50
@ac-pplx
ac-pplx marked this pull request as ready for review November 3, 2026 03:51
@ac-pplx
ac-pplx marked this pull request as draft November 3, 2026 03:52
@ac-pplx
ac-pplx force-pushed the ac/lifecycle-move-drain branch from 1e3f5d3 to 2396dcd Compare November 3, 2026 06:00
@ac-pplx
ac-pplx marked this pull request as ready for review November 3, 2026 06:29
The cluster controller's node drain and drained-node terminator wrote the
control-plane `sandboxes` row from inline SQL in their own store files.
Sixteen of those statements, the node drain's and its pod-less
give-up's, move into a new `drain` module of `pplx-space-sandbox-store`,
one function per transition that runs on the executor or transaction its
caller passes:

- the evacuation suspend claim, its revert, and the suspend commit;
- the evacuation restore finalize and the restore retry backoff;
- the stopped evacuation claim and its artifact pin;
- the stranded-suspend redrive;
- the give-ups: mark lost (artifact missing and gone node), mark failed
  on node, fail residents on a gone node, the stale-transient abandon,
  and both remote-snapshot adoptions;
- the pod-less give-up's revert to `suspended`, which takes the caller's
  transaction because it relies on the precheck the caller re-ran under
  that transaction's locks.

The terminator's unrecoverable mark-lost moves into the existing `lost`
module instead. When it flips rows, the terminator's transaction then
releases their placement through `lost::release_lost_placement`, and
`lost::mark_started_lost` is the same give-up for `started` rows, which
`lost_started_sandboxes` runs in its own transaction. It also takes the
caller's transaction, because its node guards hold only under the locks
that transaction took.

Statement text is unchanged byte for byte, and each statement keeps its
construction. The binds are the same values: the drain-phase bind is
`NodeDrainPhase::Draining`, which encodes the same text as the drain's
phase constant, and the three lifecycle sets the statements bind are
passed in as wire strings (two are the drain's, the resident set is
`pplx-space-common-svc`'s). The candidate lists, the node row lock and
lock timeout, the standalone FOR UPDATE and FOR SHARE reads and the
transactions stay with the callers, as do the pod-less give-up's
precheck, its execution and port-exposure writes and its observed-state
purge. The in-statement FOR UPDATE SKIP LOCKED of the failed-on-node
flip, the stale-resident abandon and the gone-node resident failure
moved with their statements. The gone-node identity guard fragment moves
with the statements that use it and is public, because the drain's
in-flight snapshot give-up (a `snapshots` write) still interpolates it.
The statement builders are public only because the Postgres tests run
them directly.

Docs elsewhere that named the old locations of the moved statements now
name the store functions.

The moved statements' `sql!` locators now name
`pplx_space_sandbox_store::drain`, and `pplx_space_sandbox_store::lost`
for the terminator's statement. All three caller files leave the
`sandbox_writes` ratchet.
@ac-pplx
ac-pplx force-pushed the ac/lifecycle-move-drain branch from 2396dcd to 8932644 Compare November 3, 2026 21:25
@ac-pplx
ac-pplx added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit e4d1df8 Nov 3, 2026
41 checks passed
@ac-pplx
ac-pplx deleted the ac/lifecycle-move-drain branch November 3, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:pending Waiting for a maintainer to triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants