Repository navigation
sbx-mgr: build every test service from explicit test options - #322
Conversation
5292b35 to
7e6b46a
Compare
Review — 5 findingsMedium
Low
Verified against |
the-gigi-pplx
left a comment
There was a problem hiding this comment.
Approved. 5 non-blocking findings in the review comment above.
SandboxManagerServiceOptions::default() is production wiring: it reads SPACE_NODE_ID or the host's node-id file, names the production host-control and credential-manager sockets, probes fc.pid under the fixed sandbox run directory, and dials the host's node-manager and template-manager. About two hundred test call sites built the service from it, through SandboxManagerService::new, `..Default::default()`, `new_with_config(conn, Default::default(), ..)` and helpers built on them. A cfg(test) builder now owns test wiring. SandboxManagerServiceOptions::for_test_in(dir) resolves every host dependency under a test-owned directory: - a fixed node id (test_options::test_node_id); - both runtime liveness probes read dir/<sandbox id>/fc.pid: the service's sandbox_runtime_live, and stop and delete target selection, through a cfg(test) cleanup_run_dir option that replaces the cleanup executor's hand-set run_dir hook; - credential-manager, node-manager and template-manager sockets that nothing binds. node-manager and template-manager clients dial once and fail to connect, as the host clients do when the services are down; - a host-control socket path, which sandbox-manager only passes through to node-manager's Launch, that names no live service. for_test() uses a fresh directory that nothing creates, and SandboxManagerService::for_test(conn) builds a service from it. Tests override the fields they exercise with struct update syntax. Every test-side constructor and helper now goes through the builder: service_for_teardown_test, service_with_runtime_probe and grpc_core's isolated_service keep their names and delegate to it; service_with_runtime_liveness and runtime_liveness_options fold into service_with_runtime_probe; node_manager_transport_tests' isolated_service folds into the builder; route_identity_tests no longer names the host node-manager and template-manager. Default is unchanged for production. SandboxManagerService::new, which production never called, is removed, so a test cannot fall back to the host defaults through it. With Default hidden from test builds, its only remaining user is run.rs's production wiring. The eight tests that set the executor's run directory by hand now get it from the builder. Subsystem test contexts (snapshot GC, upload jobs, scheduler, watchdog, restore) used NodeManagerConnector::host(); three snapshot-GC tests dialed its socket with DeleteCacheArtifact. They now use an unbound connector under a directory nothing creates. The unbound node-manager fake now dials like a real client: connect() and connect_once() dial the absent socket once and fail with a ConnectError, and lazy clients fail at first use. That is what the host connector does on a node whose node-manager is down, without its retry wait. Three checkpoint-restore tests depended on it: the retirement's cleanup cannot start, so the row keeps CleanupRequired. They now say so in a comment. Test follow-ups: - The forwarded-create grpc_core test drops a `!sandbox_exists` assert that held by construction, since create writes no row before template lookup. - Cases whose status code comes from two production branches also match the message: the auto-lifecycle delete's two Internal guards and the pool-clone delete's two FailedPrecondition guards. - node_state_sync::SKIP_NODE_PUBLISH_METADATA is pub(crate), and the skip-publish test uses it. - The completion dirty-ref wait sleeps 5 ms between checks instead of spinning on yield_now. - The node_manager_transport_tests module doc lists the template resolver and the host-control socket's pass-through role. - The sbx-mgr AGENTS.md test guidance points at the builder. The release space-sbxmgr-svc binary's machine code (.text) is identical before and after, with default features and with the Kind feature set. Only source line numbers embedded in location data differ. AI assistance: written with Claude Code.
7e6b46a to
dbc7b0b
Compare
main removed SandboxManagerService::new in favor of the test-options builder (#322). Construct the loopback eviction proof test's service with SandboxManagerService::for_test, as the neighboring eviction tests do.
Based on
main: #296 (with #299) has merged. #327 is stacked on this PR. Tests only: the releasespace-sbxmgr-svcbinary's machine code (.text) is identical tomain's; the edits to production files arecfg(test)fields, apub(crate)constant and the removal of a constructor production never calls (see Validation).What merging does
Every sandbox-manager test service is built from one
cfg(test)options builder instead ofSandboxManagerServiceOptions::default().Defaultis production wiring. It readsSPACE_NODE_IDor/space/etc/node-id, names the production host-control and credential-manager sockets, probesfc.pidunder the fixed sandbox run directory, and dials the host's node-manager and template-manager. Until now, about two hundred test call sites built the service from it.SandboxManagerServiceOptions::for_test_in(dir)(newservice/test_options.rs, wired by#[cfg(test)] pub(crate) mod test_options;) resolves every host dependency under a test-owned directory:Defaultfor_test_in(dir)local_node_idSPACE_NODE_IDor/space/etc/node-idtest_options::test_node_id()sandbox_runtime_livefc.pidunder/space/run/firecracker/sandboxesdir/<id>/fc.pidcleanup_run_dir(new,cfg(test))fc.pidunder the fixed run directorydir, so target selection reads the samedir/<id>/fc.pidcredential_manager_socket_pathdir/absent-credential-manager.sock, which nothing bindshost_control_socket_pathLaunchdir/absent-host-control.sock, which names no live servicenode_managerNodeManagerConnector::host()node_manager_transport::fake::unbound(dir/absent-node-manager.sock)template_resolverHostTemplateManagertemplate_resolver::fake::unbound(dir/absent-template-manager.sock)shutdown_token,task_tracker,node_role,node_managed_by, route bundleNonefor_test()uses a fresh directory under the temp dir that nothing creates.SandboxManagerService::for_test(conn)builds a service from it. Tests override the fields they exercise with struct update syntax (..SandboxManagerServiceOptions::for_test()).Both unbound fakes dial their absent socket once and fail as the host clients do when the service is down, without the retry wait. node-manager's
connect()/connect_once()return aConnectError, and lazy clients fail at first use. A dialed template lookup fails withTemplateLookupError::Connect, which create reports as base'sInternal"cannot connect to template-manager: …". A shared-channel lookup fails withUnavailable.Both liveness probes
Stop and delete target selection reads liveness through a
cfg(test)branch of its own, which defaulted to the fixed run directory. Eight tests redirected it by hand through the cleanup executor'srun_dirhook. The builder now sets acfg(test)cleanup_run_diroption, carried on acfg(test)service field, so both probes read the test's directory. The hook and the eight hand-set copies are removed.cancelled_cleanup_rpc_tests, which marks a runtime live, builds its service withfor_test_inon that directory.Production wiring is unchanged
impl Default for SandboxManagerServiceOptions,new_with_options,new_with_configandrun.rsbehave as before. The new options field and service field exist only in test builds.SandboxManagerService::new, which production never called, is removed, so a test cannot fall back to the host defaults through it. WithDefaulttemporarily hidden from test builds (#[cfg(not(test))], reverted), the only remaining user isrun.rs:852, the productionrun()wiring, which no test calls.space-sbxmgr-svc,main(9214edf) vs this head:.textis identical with default features (a57348a2…) and with the Kind feature set (otel,datadog,test-hooks;8022cd2f…), and the file sizes are equal. The remaining bytes (73 in.rodata; 240 in.data.rel.ro, 243 with the Kind set; the build id) are source line numbers:file.rs:linestrings and panicLocationline fields. One-edit-at-a-time variants of the base confirmed this: a#[cfg(not(test))]line, the removal ofnewand thecfg(test)fields each left.textunchanged. An earlier draft that reordered the executor's construction did change.text, and was replaced.Call-site inventory (test files under
crates/libs/sbx/mgr/server/src)mainSandboxManagerService::new(conn)..Default::default()in aSandboxManagerServiceOptionsliteralnew_with_config(conn, Default::default(), ..)SandboxManagerServiceOptions::default()NodeManagerConnector::host()/HostTemplateManagerNodeManagerConnector::host(): GC, upload jobs, scheduler, watchdog and restore test files (10), plus thecfg(test)SandboxWatchdog::new_for_tests(2)cleanup_executor.run_dirSandboxManagerService::for_test(conn)newcalls, plusservice_for_teardown_test)..SandboxManagerServiceOptions::for_test()/for_test_in(dir)test_options.rs)Helpers:
service_for_teardown_test(118 calls) andservice_with_runtime_probekeep their names and delegate to the builder.service_with_runtime_liveness(65 calls onmain, three of them from sbx-mgr: keep quarantined rows through inventory and bound recovery that can never succeed #270) andruntime_liveness_optionsfolded intoservice_with_runtime_probe. They used the host node-manager and template-manager.service_with_runtime_liveness_and_absent_socketsnow usesfor_test_in(sockets).grpc_core/tests.rs'sisolated_servicekeeps its signature and becomesnode_roleoverfor_test_in(scratch).node_manager_transport_tests'sisolated_serviceis folded into the builder.service_with_node_manager(conn, connector)remains as a one-field override, andcreate_template_lookup_testssets its fakes directly.route_identity_testsno longer names the host node-manager and template-manager.Host paths each test touches,
mainvs this PREvery test of the package was run alone under
strace -f -e trace=%file,connect. The table counts the tests that name each fixed/spacepath.main/space/run/node-manager/node-manager.sock/space/run/template-manager/template-manager.sock/space/etc/node-idThe unbound node-manager fake dials like a real client
fake::unboundused to return a lazy client fromconnect(), so a dial "succeeded" and the first RPC failed withUnavailable. It now fails atconnect(), as a real dial does.This is the reason three tests pass, on
mainthrough the host connector and now through the builder:checkpoint_retry_keeps_retirement_for_dangling_pincheckpoint_start_retires_existing_restoring_runtime_before_retryrestore_routes_provisional_suspended_retirement_through_cleanup_before_consumptionEach asserts that a restore's runtime retirement leaves the row
CleanupRequired. That holds because node-manager cannot be dialed, so the retirement's cleanup never starts. Each test now says so in a comment.On
mainthe default template resolver isHostTemplateManager, which dials the fixed template-manager socket. The test default dials an absent socket under the test directory instead, and fails to connect the same way. A file marker on both of its methods, over the full suite, showed one test reaching it:failed_paused_wake_does_not_adopt_the_next_activation_attempt, through the shared channel, which isUnavailableonmainas well. It passes unchanged.Follow-ups folded in
grpc_core/tests.rs(from sbx-mgr: characterize stop, delete and create core RPC paths (tests only) #271's review):!sandbox_existsassert that held by construction, along with the same assert increate_template_lookup_tests. The create arm ofcore_rpcs_refuse_unauthenticated_and_unlisted_peers_without_mutationkeeps its assert.Internalguards, and the pool-clone delete's twoFailedPreconditionguards.node_state_sync::SKIP_NODE_PUBLISH_METADATAispub(crate), and the skip-publish test uses it.BARRIER_AUTHORITY's doc says that a lookup past the barrier stops at the test service's refusing template-manager fake.Launch, not dialed.node_manager_transport_testsmodule doc lists the template resolver.start/tests.rsandgrpc_core/tests.rsnow say every test service's template-manager fails to connect.AGENTS.mdtest guidance points at the builder.Mutation proofs
Each mutation was a scratch edit, reverted with
git checkoutafter the run. Every run was understrace -f, and none wrote under/spaceor connected to a/spacesocket.Internalcode)auto_lifecycle_delete_requires_one_canonical_id_and_the_expected_policymain's test file passes it.FailedPreconditioncode)delete_of_a_pool_clone_disposal_requires_an_operator_after_quarantinemain's test file passes it.unboundconnect()succeeds lazily (the old fake)CleanupFailed≠CleanupRequired)start.rs: skip routing a pendingMarkSuspendedretirement through cleanuprestore_routes_provisional_…failsstart.rs: change the ambiguous-restoring retirement reasoncheckpoint_start_retires_…failscleanup_run_dirunset (target selection reads the fixed run directory)cancelled_cleanup_rpc_tests, #327's unpersisted testcleanup_run_dirCall sites left unrouted
These need production seams this PR does not add.
activation_support.rs:201setsidentity.current_node_id = local_space_node_id()directly. It is a free function that re-reads the identity on each activation, and routing it through the service'slocal_node_idwould change production to read it once at startup. These 13 tests still read/space/etc/node-id:service::start::tests::restore_artifact_as_new_*(7);service_tests::base_snapshot_create_wake_accepts_the_open_activation_identity;service_tests::no_op_wake_leaves_a_locally_minted_provisional_attempt_inert;service_tests::restore_artifact_as_new_start_reattaches_existing_started_runtime;service_tests::runtime_activation_identity_does_not_promote_a_locally_minted_kubernetes_attempt;service_tests::runtime_activation_identity_preserves_explicit_kubernetes_substrate_without_pod_uid;service_tests::wake_start_requests_preserve_claim_ineligible_wire_identity.ENOENTon fresh-UUID paths):activation_support::tests::restore_finalizer_retains_cleanup_until_state_persistence_succeeds,…restore_defers_guest_readiness_only_while_pausedand…activation_env_failure_keeps_setup_context_for_callers_and_cleanup, and inservice_tests::start_readiness_failure_does_not_overwrite_concurrent_delete;service_tests::resume_fails_closed_when_network_policy_cannot_confirm,service_tests::pre_resume_network_policy_apply_fails_closed_without_gatewayandactivation_support::tests::restore_completion_requires_gateway_readiness_only_when_starting.watchdog.rspid files,grpc_state_snapshot.rs:821) and fixedCACHE_DIRstats of fresh-UUID paths.enter_route_test_process.Host safety
Every test service now gets its node id, both liveness probes, sockets, node-manager and template-manager from test-owned values. Every subsystem test context gets an unbound node-manager, including the watchdog's
cfg(test)constructor, whose loops tick immediately. The three snapshot-GC tests that sentDeleteCacheArtifactto the host node-manager's socket onmainnow dial an absent socket.Pre-existing, unchanged by this PR: seven tests reach restore staging, which calls
create_dir_allon/space/cache/firecracker/sandboxes/.<id>.restore-<uuid>under the fixed cache root:service::recovery::testsorphan-restore tests;checkpoint_retry_preserves_row_for_resumable_restore.None of them is in the
privilegednextest group, so onmain's CI they run in the unprivileged step, wherecreate_dir_allfails withEACCESat/space. As root they would create those directories and then dial the fixed volume-manager socket (/space/run/volume-manager/volume-manager.sock,snapshot_restore.rs:2169,cleanup.rs:550). Unprivileged,checkpoint_retry_preserves_row_for_resumable_restore's assertedUnavailablecomes from the failedcreate_dir_all; as root it would come from that volume-manager connect.Follow-up: a cache-root and volume-manager seam for those seven tests.
Overlap with open PRs and merge order
service/tests.rs: sbx-mgr: keep quarantined rows through inventory and bound recovery that can never succeed #270's quarantine block inevict_cluster_deleted_sandbox_reclaims_retained_restoring_and_quarantined_rowssits beside the renamedservice_with_runtime_liveness(conn, false). The rebase also converted sbx-mgr: keep quarantined rows through inventory and bound recovery that can never succeed #270's three newservice_with_runtime_livenesscalls toservice_with_runtime_probe: two incache_cleanup/periodic/tests.rs, andrecovery/tests.rs'sbound_test_service. Their assertions are unchanged. sbx-mgr: keep quarantined rows through inventory and bound recovery that can never succeed #270 as merged adds noSandboxManagerService::newcall.service_with_runtime_liveness→service_with_runtime_probe,SandboxManagerService::new→SandboxManagerService::for_test, and a hand-setcleanup_executor.run_dir→for_test_in(dir). With this PR, leftover calls fail to compile instead of silently using host defaults.SandboxManagerService::new(2),service_with_runtime_liveness(1) andcleanup_executor.run_dir(1). It no longer applies tomaineither.main, and adds oneSandboxManagerService::newcall.mainbut not to this head: itssnapshot_upload_jobs/tests.rshunk overlaps this PR's edit there. It also adds oneSandboxManagerService::newcall.service_with_runtime_livenesscall.SandboxManagerService::newcall. It already fails to apply onmain.crates/libs/sbx/mgr/serverapplies to this head exactly as it does tomain: Defer btrfs-send capture by default in sandbox-manager #287, Validate encrypted backup containers at admission #344, Observability wave 2a: correct sparse Kind counters, classify build failures, add bounded outcome labels #348 and Observability wave 2b: request-scoped traces, canonical sandbox_id, request-to-trace correlation #354 apply; sbx-mgr: resume a teardown suspend's unfinished publication through recovery #366, PoC: claim BYOK warm sandboxes by switching Firecracker drives #336, Approve bounded publication of validated retained suspends #283, [api] stop retrying restore pods whose snapshot the host CPU cannot load #221, Preserve authenticated scope in cached template lookups #217, Add an opt-in Firecracker balloon device per template #212, [sbx-mgr] record the capture host CPU on snapshots and publish it as a node label #203, sbx-mgr: keep teardown polls pending while a delete is in flight #201, [api] restore full snapshots only onto CPU-compatible hosts, behind a default-off gate #176 and Quarantine sandboxes after sustained guest-agent probe failure #110 fail onmainas well.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 run.space-sbxmgr-svcbinary vsmain.textidentical (default and Kind feature sets)dbc7b0bAI assistance: the code and this description were written with Claude Code.
🤖 Generated with Claude Code