Repository navigation
sbx-mgr: test create after template lookup, unpersisted cleanup and endpoint capture on the seams (tests only) - #327
Conversation
f93fc01 to
70edf34
Compare
70edf34 to
56897e8
Compare
Review — 4 findingsMedium
Low
Verified against |
the-gigi-pplx
left a comment
There was a problem hiding this comment.
Approved. 4 non-blocking findings in the review comment above.
…ndpoint capture on the seams Tests only. These paths were deferred because they reached fixed template-manager or node-manager sockets. They now run against FakeTemplateResolver and FakeNodeManager. Every test that builds a service runs in a child process without the feature-flag provider settings, so construction cannot contact a provider. Create after template lookup (grpc_core/create_after_lookup_tests.rs). A create that inserts a new row goes on to create the sandbox's cache root under the fixed host cache directory. Each service here therefore carries a temporary SQLite trigger that refuses every new sandboxes row and reports the row in its refusal, so a create that reaches the insert stops there. The tests pin create's own delta over the lower-layer intent tests: - A fresh create inserts a Creating row with the resolved build hash, the request's owner and environment, the template's default network authority, and the template's default auto-lifecycle policy and authority unless the request carries a policy. It honours a requested id, and an empty pool falls back to a cold create. - An autostart create asks node-manager's GetNetworkPoolStatus before the insert. No ready slot refuses ResourceExhausted, and a refused status passes its code through. - A pool create adopts a paused donor of the resolved build. The donor takes the request's owner and environment and the template's defaults, and no row is inserted. A donor of another build is never adopted and is left untouched. - A requested-id create against an existing compatible row is swept over every sandbox state, with the expected outcome written out per state. Created, Started, Paused, Suspended and Stopped rows are reused and take the request's policy. CleanupRequired and CleanupFailed refuse with retryable Unavailable. Every other state, Quarantined included, refuses AlreadyExists without touching the row. A row of another build refuses AlreadyExists with or without a policy change. Reuse keeps the row's own environment. Each create is bounded on real time. Unpersisted stop and delete (grpc_core/unpersisted_cleanup_tests.rs). A trigger refuses the stopping or deleting claim write. Stop (with immediate_kill, which skips the guest flush) and delete, blocking or not, then clean the runtime up through node-manager's CleanupSandbox with the expected keep-filesystem, slot-release and kill options. They report Internal "runtime cleanup completed but ... could not be persisted", or node-manager's refusal, and leave the row, episode, jobs and dirty refs untouched. Endpoint capture (service/tests/endpoint_capture_tests.rs). Opening and closing a loopback bridge on a restoring sandbox as node-ctl, and a trusted node-api readiness wait, each ask node-manager to capture this sandbox's endpoint proof, and surface its refusal as PermissionDenied "spaced endpoint attestation failed". With node-manager down, each fails retryably with Unavailable "spaced endpoint proof is temporarily unavailable", before any spaced-host connection. This replaces five tests that inferred the same from an unreachable node-manager; two pairs of them had identical bodies. Snapshot GC. reaps_idle_uploaded_unpinned_artifact asserts that the reaped artifact's .gc-pending staging directory is handed to node-manager's DeleteCacheArtifact, bounded on real time because the test's clock is paused. AI assistance: written with Claude Code.
56897e8 to
8d7088c
Compare
Stacked: this PR is based on #322 (
ac/sbxmgr-explicit-test-service), which is based onmain. It will be retargeted tomainonce #322 merges. Tests only: the releasespace-sbxmgr-svcbinary's machine code is identical tomain's.What merging does
Adds tests for sandbox-manager paths that earlier characterization deferred because they reached fixed template-manager or node-manager sockets. They now run against
FakeTemplateResolver,FakeNodeManagerand #322's test options.Every test that builds a service runs in a child process (
enter_route_test_process) that clearsEPPO_SDK_KEYandSPACE_FLAGD_ENABLED, soFeatureFlagClient::from_envin service construction cannot contact a provider.Create after template lookup —
service/grpc_core/create_after_lookup_tests.rsHost fence. Once create inserts a new sandbox row,
create_with_idcallsensure_sandbox_cache_root. That iscreate_dir_allunder the fixed/space/cache/firecracker/sandboxes, which would create host directories when the tests run as root. Every service in this module carries a temporary SQLite trigger that refuses each newsandboxesrow (BEFORE INSERT … RAISE(ABORT, <the row as JSON>)). A create that reaches the insert stops there; this was verified for the cases and the mutations listed below. The refusal carries the row create tried to write, which the cases assert on.The template resolver answers with a build in a test-owned directory, built for this node with a supported spaced guest. The template's defaults are an access-idle suspend policy at 600 s, auto-lifecycle authority
Localand network-policy authorityLocal, so a hard-codedClustercannot pass for a template default. Seeded rows carry their own environment (ROW_ENV=from-row), distinct from every request's (CREATE_ENV=from-request). Each create is bounded byCREATE_WAIT(30 s, real time).create_reaches_the_row_insert_with_the_templates_build_and_defaultscreating, with the resolved build hash, the request's owner and environment, and the template's network authoritylocal. It takes the template's default policy andlocalauto-lifecycle authority unless the request carries a policy. A requested id is used as the row id.BootMode::Poolwith an empty pool falls back to a cold create. Each case sends one lookup by name and makes no node-manager call.autostart_create_checks_ready_network_slots_before_the_row_insertGetNetworkPoolStatusbefore inserting. Zero ready slots →ResourceExhausted"no ready network slots". A refused status → its code with the "network pool status failed:" prefix. One ready slot → the insert.pool_create_adopts_a_donor_of_the_resolved_build_with_the_templates_defaultsBootMode::Poolwith a paused donor of the resolved build returns the donor's id withboot_mode_used = Pooland inserts no row. The donor takes the request's owner and environment, replacing its own, and the template's default policy and authority. No node-manager call.pool_create_never_adopts_a_donor_of_another_buildrequested_id_create_reuses_only_a_compatible_settled_rowSandboxState::all()(18 states), with the expected outcome written out per state in the test. The request's policy differs from the row's.created,started,paused,suspendedandstoppedrows are reused (reused_existing) and take the request's policy.cleanup_requiredandcleanup_failedrefuse with retryableUnavailable"…still completing activation cleanup; retry". Every other state,quarantinedincluded, refusesAlreadyExists"…already exists in state " and leaves the policy. Controls: a compatible started row with the same policy is reused. A started row of another build refusesAlreadyExists"…incompatible create parameters: template_hash", both with the same policy and with a changed one; the build is checked once up front and again under the wake lock when the policy changes. Every case keeps the row's own environment.Finding: a pool create with only stale donors does not fall through.
manager.proto:366documentsboot_modeas "fallthrough: pool → base_snapshot → cold", and so does thespace-sbxmgrCLI help (args.rs:245). Create checks pool occupancy per template (reconcile.rs:995-1003,find_pool_sandbox(template, None, None)) but adopts per resolved build (adopt_pool_sandboxwith the resolvedcontent_hash). To reproduce:pool_template = 'seed') at build A.seedat build B.Create { template: "seed", boot_mode: POOL }with no requested id.Create returns
ResourceExhausted"no available pool sandboxes for template 'seed'" instead of falling through to a base-snapshot or cold create. Reachability is low: in this repository only thespace-sbxmgrCLI sendsBootMode::Poolto sandbox-manager. The API's direct substrate refusespoolitself (direct.rs:173), and template-manager sendsBaseSnapshotorCold.Unpersisted stop and delete —
service/grpc_core/unpersisted_cleanup_tests.rsThis is #271's deferred "unpersisted stop/delete" path. A trigger refuses the
stopping/deletingclaim write, soUnfencedadmission returnsUnpersisted. Stop runs withimmediate_kill, which skips the guest flush, because that flush dials the guest's fixed spaced-host socket.unpersisted_stop_and_delete_clean_up_through_node_manager_and_report_the_claim_failurecovers stop and delete × blocking and non-blocking × node-manager accepting or refusing:CleanupSandboxwithforce, the row's execution fence, and stop's options (keep filesystem, return after slot release, immediate kill) or delete's (none of them);Internal"runtime cleanup completed but {stopping|deleting} transition could not be persisted for sandbox : claim cleanup lifecycle failed: …";Started, with no cleanup intent, episode, active cleanup job or dirty ref.Stop's target selection reads the test's run directory through #322's builder.
Endpoint capture —
service/tests/endpoint_capture_tests.rsgated_spaced_calls_capture_the_endpoint_proof_from_node_managercovers three calls:Each runs twice:
CaptureSandboxEndpointProof { sandbox_id }. node-manager'sFailedPreconditionsurfaces asPermissionDenied"spaced endpoint attestation failed".Unavailable"spaced endpoint proof is temporarily unavailable", before any spaced-host connection.This replaces five
service/tests.rstests that asserted only the node-manager-down outcome. Two pairs of them had identical bodies:spaced_open_loopback_bridge_passes_state_gate_while_restoringand…_exempts_node_ctl_without_route_token;spaced_close_loopback_bridge_passes_state_gate_while_restoringand…_exempts_node_ctl_without_route_token;spaced_wait_ready_trusted_node_api_reaches_spaced_host_without_route_token.Their rationale comment, on why the bridge must not gate on
Started, moves with them. Three helpers they shared becomepub(crate).Snapshot GC —
snapshot_gc/tests.rsreaps_idle_uploaded_unpinned_artifactnow serves node-manager and asserts that the reaped artifact's<path>.gc-pending-<id>staging directory is handed toDeleteCacheArtifact. Before, the test passed whether or not that call happened. The test's clock is paused, so the call is bounded byNODE_MANAGER_WAIT(30 s) on real time, through a watchdog thread rather than a tokio timeout. #322 moves the module's other tests to an unbound node-manager.Mutation proofs
Each mutation was a scratch edit, reverted with
git checkoutafter the run, against only the tests listed. Runs were strictly sequential, each understrace -f -e trace=%file,connect. None wrote under/spaceor connected to a/spacesocket. The only/spacesyscalls were stats of fresh-UUID pid files (see Host safety).grpc_core.rs: ignore the template's default auto-lifecycle authority (alwaysCluster)create.rs: ignore the template's default network authority (alwaysCluster)grpc_core.rs: a requested id with no row gets a fresh idcreate.rs: skip the autostart ready-slot checkgrpc_core.rs: pool adoption ignores the resolved buildgrpc_core.rs: skip the up-front requested-id mismatch checkgrpc_core.rs: onlyCleanupRequiredgets the retryable refusalcleanup_failed)grpc_core.rs: skip the reused row's policy updatecreate_support.rs: aQuarantinedrow is reusablequarantined)grpc_core.rs: a refused reuse writes the requested policy firstgrpc_core.rs: reuse clears the row's environmentgrpc_core.rs: the unpersisted stop wipes the filesystemgrpc_core.rs: the unpersisted delete drops the persistence errorgrpc_core.rs: the unpersisted stop drops the persistence errorspaced_bridge.rs: a refused proof reports "temporarily unavailable"state.rs: the bridge gate refusesRestoringspaced_bridge.rs: a down node-manager reportsInternalsnapshot_gc.rs: the reaper skips the staging cleanupreaps_idle_uploaded_unpinned_artifactsnapshot_gc::testspassThe seams and bounds are real:
CREATE_WAITset to 1 nsreaps_idle_uploaded_unpinned_artifactNODE_MANAGER_WAITset to 1 nsreaps_idle_uploaded_unpinned_artifact(the real-time bound fires under the paused clock)An earlier draft let A5 survive: with a changed request policy, the second build check under the wake lock still refused. The same-policy control was added for that.
Still unreachable
Aborted"clone disposal hold changed" result. Both need a concurrent writer between the clone-disposal read and the fenced re-arm, and there is no await point between them. An interleaving hook is needed.ensure_sandbox_cache_root, base-snapshot staging, launch) writes under the fixed cache root. It needs a cache-directory seam.immediate_kill = false) dials the guest's fixed spaced-host socket. It needs a spaced-host seam.suspend_proceeds_when_wake_predates_requestinservice/tests.rsstill asserts only "notAborted". With a served node-manager, suspend's next step dials the guest's fixed spaced-host socket (verified with strace).checkpoint_retry_keeps_mark_suspended_for_restoring_row_with_registered_artifactinservice/tests.rsstill applies the cleanup completion by hand. Letting node-manager complete it would take the restore on into fixed cache-dir staging.restart_drain_allows_process_reconnect_for_started_sandboxinservice/tests.rsreaches the same endpoint capture through a route-token helper. It stays on the unbound default, because its helpers sit beside lines sbx-mgr: keep quarantined rows through inventory and bound recovery that can never succeed #270 edits.delete_defers_*tests inservice/tests.rsand thecleanup_executorlifecycle_commit_and_recovery_*tests reach node-manager only incidentally, and pass on the unbound default.Host safety
/spacepath.statfresh-UUIDfc.pidandspace-netgtw-svc.pidpaths under/space/run/firecracker/sandboxes. These come from fixed run-directory probes outside the service options (watchdog removal). No writes, no socket connects.snapshot_gc::tests::run_loop_startup_does_not_reset_live_restore_lease, whichread_dirs the fixed/space/cachethroughsweep_orphans(read-only;ENOENThere).main, a regressed in-loop cancel check in a GC tick would have sentDeleteCacheArtifactfor any real host.gc-pending-*directory to the host node-manager.TEMPand live only on each test's in-memory database.Overlap with open PRs
Every open PR's diff that touches
crates/libs/sbx/mgr/serverapplies to this head exactly as it does to #322 (see #322 for #270's and #331's merge-order notes):service/tests.rs: onemodline, three helpers madepub(crate), and the five superseded tests and their comment removed.service/grpc_core.rs: two#[cfg(test)] modlines.Validation
cargo fmt --all --checkcargo clippy --workspace --locked --all-targets --all-features -- -D warningscargo nextest run --locked -p pplx-space-sbx-mgr --retries 0,SPACE_CI_REQUIRE_TEST_DEPS=1, 3 consecutive runs#[ignore]test), 0 dependency skips; the Postgres setup script ran in each runspace-sbxmgr-svcbinary vsmain.textidentical (tests only)8d7088cAI assistance: the tests and this description were written with Claude Code.
🤖 Generated with Claude Code