Repository navigation
sbx-mgr: guard rolled-back restores against late kubelet retries on Kind - #594
Closed
nickdujay-pplx wants to merge 4 commits into
Closed
nickdujay-pplx wants to merge 4 commits into
nickdujay-pplx wants to merge 4 commits into
Conversation
nickdujay-pplx
marked this pull request as ready for review
October 16, 2026 21:41
A completed activation rollback records a receipt for the exact execution generation and activation attempt it retired, but a late request for that same attempt could still claim the row. Activation cleared the receipt, the retired execution reached a started state, and its teardown then took and uploaded a new snapshot instead of leaving the original artifact as the restore source for the retry. Activation admission now refuses a request that repeats the rolled-back generation/attempt, matching either the receipt's attempt or the retained route evidence, and leaves the row, receipt and artifact pin untouched. Start and restore map the refusal to FAILED_PRECONDITION so the caller mints a new attempt. A new attempt for the same generation is still admitted. Teardown release evidence now also honors a rollback back to suspended for the exact rolled-back generation, so that execution's teardown is answered as superseded without binding a suspend operation or queueing a backup. Older generations keep their durable answer for the retained artifact.
Local eviction deletes the sandbox row and with it the activation-rollback receipt. A same-attempt restore retry could then rebuild the row from the remote snapshot and activate the rolled-back generation again. Record the delete receipt before the row delete, at the larger of the route generation and the rolled-back generation, and refuse activation of a generation that a delete receipt has retired on this node.
Start now checks the rolled-back attempt and retired-generation guards before any restore or start work, including before a missing row is rebuilt from the remote snapshot. The claim repeats the check atomically. One shared function maps both refusals to FAILED_PRECONDITION. Add RPC-level tests for the restore, cold-start and evicted-row paths, and a test for the inventory stale-row delete after a rollback.
nickdujay-pplx
force-pushed
the
sbxmgr-restore-rollback-e2e-guard
branch
from
October 19, 2026 16:41
f0ba85f to
aa200d2
Compare
Add a Kind smoke that fails a restore after its activation claim, lets kubelet retry the same attempt late, deletes the pod, and connects again. It requires that the sandbox keeps exactly its original snapshot and that the next restore activates that snapshot's artifact. - sandbox-manager: a one-shot restore-prepare fault, compiled only with the test-hooks feature. A marker file named after the sandbox id, in the directory SPACE_SBXMGR_TEST_RESTORE_PREPARE_FAULT_DIR names, fails the next restore prepare once and is removed when it fires. Without the feature the hook is a constant None. - Kind chart: set the directory for sandbox workers only, under the container's private /tmp. - space_smoke: RestoreRollbackControl with arm/disarm operations and the reads the verdict needs, plus the scenario local.restore_rollback_late_retry_keeps_original_snapshot. - Kind pull CI: run on any sandbox-manager server source change.
nickdujay-pplx
force-pushed
the
sbxmgr-restore-rollback-e2e-guard
branch
from
October 19, 2026 16:43
aa200d2 to
eaa9870
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR #583 stops a rolled-back restore from going live when Kubernetes retries it late. Unit tests cover each refusal. No test runs the real sequence on a cluster, because nothing in a running cluster makes a restore fail on demand.
This PR adds that end-to-end test. It is a Kind smoke scenario. It fails one restore on purpose, lets kubelet retry it late, and checks that the sandbox keeps its original snapshot.
Self-assessed severity: Medium. This PR changes no production behaviour. It adds a regression guard for a High-severity bug (see #583). Release builds do not compile the new hook.
Stack
This PR is the second in a
gh stack:sbx-mgr: skip teardown snapshot after a confirmed activation rollback: the fix.The scenario needs the fix to pass. Review #583 first. This PR can merge only after #583, or together with it.
Terms
test-hooksbuilds read. Kind builds enable the feature; release builds do not.Problem
The bug in #583 needs four things in order:
Steps 1 and 3 do not happen on demand. A real prepare failure, such as a disk-admission refusal, needs a full disk. Filling the disk, or restarting a service, also moves placement or races the restore. So the Kind suite had no way to run this sequence, and a regression would pass CI.
The user's files survive even on the broken build. So a test that only reads the files back cannot catch the bug. The test has to check the snapshot rows and the restored artifact.
What changed
Sandbox-manager: a one-shot restore-prepare fault
New file:
crates/libs/sbx/mgr/server/src/service/restore_prepare_fault.rs.test-hooks, the hook readsSPACE_SBXMGR_TEST_RESTORE_PREPARE_FAULT_DIR.RESOURCE_EXHAUSTED. That is the code a real disk-admission refusal carries. The rollback path is unchanged.test-hooks, the hook is an#[inline(always)]function that returnsNone. It reads no environment and no files.env_test_hook_boundary.pypasses.The only change in
create.rsis in the restore path. It takes the fault, if one is armed, instead of callingprepare_sandbox. The result goes into the existing error handling.Kind chart
space-sandbox-manager-localsets the variable to a directory under the container's private/tmp(testHooks.restorePrepareFaultDir). A marker therefore never outlives its pod.values-builder.yaml). An empty value renders no variable.infra/space/kindnames it. A new chart test checks this.Smoke framework
RestoreRollbackControlinspace_smoke/capabilities.py, with a Kind backend inadapters.py. It builds on main'sRestoreStateReadandRestorePodRead:arm_restore_prepare_fault,armed_restore_prepare_faultsanddisarm_restore_prepare_faultwrite, read and remove the marker. They run in the sandbox-manager container as its own user, on every sandbox worker, because the API chooses placement.snapshot_artifact_rowsreads everysnapshotsrow of the sandbox, in any state.sandbox_start_failuresreads the pod'sFailedCreatePodSandBoxevents. They are failure evidence only.RestoreActivationnow carrieslocal_artifact_idfrom theruntime_activatedcontract event.space_smoke/restore_rollback.pyholds the scenario steps and the verdict.Scenario
local.restore_rollback_late_retry_keeps_original_snapshotSteps:
The verdict follows
space_smoke/AGENTS.md: pass or fail comes from state, not log lines. The scenario asserts:runtime_activatedcontract event for that generation reports a snapshot boot of the original snapshot's local artifact (the scenario polls for the event and judges its fields);Whether kubelet's late retries were refused, and whether the faulted generation activated, shows only in pod events and the sandbox-manager log. The scenario records these as failure evidence and does not assert on them. After the rolled-back placement, the successor pod carried no storage URI annotation on the bench, so the pod cannot name the snapshot. The activation event is the only exposed record of which artifact was restored.
The scenario is
local.*, notecw.*. The scenarios README reserveslocal.*for scenarios that need Kind-only control.ecw.*scenarios must hold on every backend, and this one needs the hook.CI path filter
kind-pull-ci.ymlnow runs on any change undercrates/libs/sbx/mgr/server/src/**. It used to run only onactivation_support.rs. The restore, rollback and activation paths this scenario guards are spread across the server source.How it was validated
Kind: passes with the fix, fails without it
I ran both builds on one local Kind bench, deployed through Tilt, after rebasing the stack on
mainat a5c67e5.FailedPrecondition: activation attempt ... was rolled back.main(a5c67e5) plus only this PR's commita new snapshot was published after the rolled-back restore; its late retry went live and its teardown suspended it againlocal_only) on kubelet's retry, and generation 3 activated the new artifact.I ran the scenario at aa200d2. The head, eaa9870, changes only one docstring in
space_smoke/restore_rollback.py. The failing build was a local branch only and was not pushed. Its smoke, chart and CI files are the same as aa200d2's.Existing Kind smokes
Before the rebase, I ran CI's Kind shard 4 of 4 (
scripts/tests/smoke-kind-typed.sh all 4/4) against this stack. I have not rerun it on the rebased stack. That shard holds the new scenario and the existing restore-heavy ones:ecw.pause_connect_race,local.encrypted_resuspend_preserves_cow,local.forced_drain_evacuates_sandboxandlocal.snapshot_reaper_reclaims_backed_fork_snapshot.ecw.cold_create_environmentwas inconclusive:cold setup requires the isolated local Kind API. Its setup only runs on a cluster whose context is exactlykind-space-local, and my bench is a named cluster. This is a limit of my bench, not a result of this change. CI runs it onkind-space-local.I removed the test sandboxes, the run-scoped API keys and the fault markers afterwards.
Unit and repository checks
All of these were rerun on the rebased tree.
cargo test -p pplx-space-sbx-mgr --lib restore_prepare_fault: 8 passed withouttest-hooksand 7 with it. One case runs only without the feature: it checks that no fault ever fires.cargo clippy -p pplx-space-sbx-mgr --tests -- -D warnings, with and without--features test-hooks, andcargo fmt --check: clean.cargo arch: passes.space_smoke/tests(pytest with thetestdependency group, asci/scripts/run-python-tests.shruns it): 545 passed on the rebased tree. This includes the newtest_restore_rollback_scenario.py, the inventory, sharding and protocol-conformance tests. The new unit tests check four things:test_kind_restore_prepare_fault_chart.py(new),test_kind_known_egress_chart.py,test_kind_node_service_env.py,test_cluster_chart_validate.py,test_kind_telemetry_config.pyandtest_env_test_hook_boundary.py: 310 passed.helm lintpasses.env_test_hook_boundary.py,node_selector_extras_smoke.pyandobservability_vendor_boundary.pypass.thin_service_mains.pyreports a finding incrates/apps/vol/mgr/svc/src/main.rs. This PR does not touch that file.Open points
AI disclosure
Perplexity Computer, an AI agent, wrote all of this PR: the Rust hook and its tests, the chart change, the smoke capability, the scenario and its tests, the CI filter change, and this description. It also ran the Kind validation. Nick Dujay directed the work and reviews it.