Repository navigation
sbx-mgr: inject the node-manager client transport and template-manager resolver - #296
Conversation
955e149 to
a314315
Compare
a314315 to
56a295d
Compare
Every node-manager client sandbox-manager builds now comes from one NodeManagerConnector, carried in SandboxManagerServiceOptions and handed to the subsystems the service constructs: the watchdog loops, the snapshot scheduler's checkpoint context, the snapshot-upload worker and its retained-source inspector, the snapshot reaper, the restore pipeline, btrfs-via-node-manager, the encryption helpers, and the startup takeover and recovery probes in run(). NodeManagerConnector::host(), the default, forwards connect(), connect_once() and lazy() to the pplx-space-node-mgr-client functions of the same name. Production therefore dials the same fixed socket with the same retry policy, keeps the same lazy or eager timing per call site, and shares the client crate's process-wide lazy channel. Error mapping and log lines at each call site are unchanged. Two pieces of process-wide state now follow the connector: - The retained-source inspection capability answer is cached per connector. host() always returns the same instance, so production keeps its single cache. - Each immediate-reclaim backlog entry carries the connector that submitted it. The worker that drains the backlog deletes every entry through that entry's own node-manager. Tests reach an in-process node-manager instead: FakeNodeManager serves scripted unary RPCs on a socket in a test-owned temporary directory and refuses every other call with Unimplemented. - fatal_kill_signal_failure_still_schedules_admitted_cleanup moves to service/tests/node_manager_transport_tests.rs. node-manager now refuses StopProcess, and the test asserts the refusal reaches the caller, the fenced StopProcess request, and that the admitted cleanup still stops the same execution with its filesystem kept and ends Stopped. A host node-manager can no longer satisfy it. - blocking_stop_follows_a_retryable_attempt_to_durable_completion drops the cleanup executor's test dispatch hook. It drives the real CleanupSandbox path against a fake that fails once with Unavailable, and asserts both requests and the Stopped row. - The immediate-reclaim staging test and the confirmed-pause test pass a fake connector, so their node-manager fallbacks cannot reach the host. - New tests pin both pieces of per-connector state. immediate_reclaim_backlog_uses_each_entrys_own_node_manager backlogs two entries through two fakes, and send_source_inspection_is_cached_per_connector gives two fakes opposing capability answers. - grpc_core's isolated_service, which lists every option, gives node-manager an unbound connector on an absent socket under the test's scratch directory, beside its absent host-control and credential-manager sockets. Any node-manager call fails closed.
Every template-manager GetTemplate sandbox-manager issues now goes through one TemplateResolver, carried in SandboxManagerServiceOptions as template_resolver. That covers resolve_template_paths and resolve_template_paths_for_start, which serve create, start, wake, pool adoption and donor cloning, plus restore staging's parent-template lookup and the template network-policy cache. Both resolver methods take a tonic::Request<GetTemplateRequest> and return the tonic::Response, so request and response metadata cross the seam; callers call into_inner() as before. The default, HostTemplateManager, makes the same calls the callers made directly. get_template dials pplx_space_template_mgr_client::connect() for each call, and get_template_on_shared_channel uses the client crate's process-wide lazy() channel. Connect and RPC failures stay distinct (TemplateLookupError), so every caller keeps its error codes and messages. TemplateNetworkPolicyCache no longer has a Default constructor that bypassed the injected resolver. Tests substitute FakeTemplateResolver. It records each lookup's channel, request metadata and message, and returns a scripted answer: a response, a refusal, or a connect failure. - create_template_lookup_tests covers create's auto-lifecycle authority checks. Forwarded peers' Local authority, Unspecified and unknown authorities are refused before any lookup. Cluster authority from a forwarded peer, and Local from a local peer, send one dialed lookup for the requested template and surface its refusal as "GetTemplate failed:". An unreachable template-manager surfaces as Internal "cannot connect to template-manager:". No row is created, and node-manager, a refusing fake, receives no call. - The latest-alias start test and the orphaned-sandbox prepare test assert that no template lookup is sent. Previously they relied on an absent template-manager. - The network-policy cache tests build the cache with a refusing fake. The hint-admission and custom-override tests assert that no owner lookup is sent. - grpc_core's isolated_service, which lists every option, gets a FakeTemplateResolver that refuses every lookup, so those tests can no longer reach the fixed template-manager socket.
56a295d to
e1e9927
Compare
Review — 2 findingsNo blocking findings. The production change is a mechanical threading of one connector, and I couldn't find a site it misses: at this head the only Low
Verified against
|
the-gigi-pplx
left a comment
There was a problem hiding this comment.
Approved. 2 non-blocking findings in the review comment above.
…olver sbx-mgr: inject the template-manager resolver
Includes #299. The template-manager resolver seam was reviewed as #299 and merged into this branch, so this PR lands both seams together.
GetTemplategoes through an injectedTemplateResolver. Production usesHostTemplateManager, with the same client construction, error mapping and requests.Summary
Every node-manager client sandbox-manager builds now comes from one injected
NodeManagerConnector. Production keeps today's exact construction. Tests can reach an in-process node-manager instead of the host's fixed/space/run/node-manager/node-manager.sock.The seam
crates/libs/sbx/mgr/server/src/node_manager_transport.rsadds:NodeManagerTransport, a trait withconnect(),connect_once()andlazy(). These mirror the threepplx-space-node-mgr-clientconstructors.NodeManagerConnector, a cloneable handle aroundArc<dyn NodeManagerTransport>.NodeManagerConnector::host(), the default. Each method calls the client-crate function of the same name,pplx_space_node_mgr_client::{connect, connect_once, lazy}.The connector is a new field,
SandboxManagerServiceOptions::node_manager, defaulting toNodeManagerConnector::host().SandboxManagerService::new_with_confighands it to every subsystem it builds:CheckpointContext::node_manager);NodeManagerSourceInspector;GcOwnership::node_manager);Two pieces of process-wide state now follow the connector:
host()always returns the same instance, so production keeps exactly one cache, with today's 30 s / 5 s lifetimes.Service handlers reach it through
self.node_manager. Free functions take it as a parameter:btrfs_via_nodemgr,firecracker_via_nodemgr,runtime_reconciliation,snapshot_encryption,rootfs_encryption, the restore pipeline insnapshot_restore,snapshot_upload::submit_prepared_snapshot, and staging reclaim.run()builds one host connector and uses it for both the takeover-health and startup-recovery probes (connect_once) and the service options.Production wiring is unchanged
connectwhere it dialed with the default retry policy,connect_oncein the two startup probes, andlazywhere it used the lazy channel.host().lazy()returns the client crate's process-wideOnceLockchannel, so every lazy caller still shares one reconnecting channel.map_errtext, so error messages and log lines are identical.async_trait, so each connect future is boxed. That is the only runtime difference.Call-site inventory, production code in
crates/libs/sbx/mgr/server/src:7a41941)pplx_space_node_mgr_client::connect()/connect_once/lazy()HostSocketinnode_manager_transport.rstail_support::connect_node()wrapperconnect_node(&self.node_manager)Before, by owner. Every site is routed through the connector:
connect_node()calls across these modules:cleanup×3,create×4,kill×2,start×2,grpc_spaced_files×2,grpc_spaced_process_snapshots×3,auth_lookup,grpc_core,pause_resume,suspend,suspend_snapshot,artifact_backup,grpc_net_gatewayandgrpc_state_snapshot.grpc_state_snapshot(×3connect),recovery_cross_node(connect) andgrpc_pool_wait_clone::connect_clone_node(connect). Lazy clients inspaced_bridge, thecache_cleanuporphan observer, therecoveryinventory reconciler andgateway_caller_clientinservice/mod.rs. Also theconnectinsidetail_support::connect_nodeitself.encode_rootfs_send,encode_snapshot_rootfs_send,receive_instance,delete_sandbox_instanceandmaterialize_rootfs_send(connect);inspect_send_sourceand the inspection capability probe (lazy).firecracker_via_nodemgr::get_firecracker_vm_state(lazy), used by the watchdog health probe, runtime reconciliation and stop-flush.snapshot_encryption×2,rootfs_encryption×2,snapshot_scheduler×2,watchdog×2 (spaced-host restart, net-gateway relaunch),snapshot_gc×1 (connect), andrecovery/staging_cleanup::reclaim_one(lazy).run.rs×2 (connect_once, takeover health and startup recovery).Clients that are passed in as a
NodeManagerGrpcClientall come from one of the routed sites above, so they are covered: the cleanup dispatch helpers,pause_with_reconciliation, the status readers,SnapshotPages, the endpoint-proof capture and the orphan-execution observer.Call sites left unrouted
None in production code.
..Default::default()still getNodeManagerConnector::host(), exactly as before. Only the tests below were converted.run/test_receiver.rsbuilds its own test client and is unchanged.Tests converted
FakeNodeManager(node_manager_transport/fake.rs, test-only) serves scripted unary RPCs on a socket in a test-ownedtempfiledirectory. It refuses every other method withUnimplementedand records the method names it receives.fatal_kill_signal_failure_still_schedules_admitted_cleanup. Moved fromservice/tests.rsto the newservice/tests/node_manager_transport_tests.rs, because sbx-mgr: keep quarantined rows through inventory and bound recovery that can never succeed #270 (open) and tests: keep three tests off host state they don't own #278 (since merged) editservice/tests.rs. Its test ID becomesservice::tests::node_manager_transport_tests::fatal_kill_signal_failure_still_schedules_admitted_cleanup. Before, it passed only because the host had no node-manager to dial. Now node-manager refusesStopProcess, and the test asserts:StopProcesswas sent, fenced to the observed execution;CleanupSandboxfor the same execution, with the filesystem kept,immediate_kill, and return after slot release;Stopped;StopProcess, thenCleanupSandbox.A live host node-manager cannot satisfy these assertions.
blocking_stop_follows_a_retryable_attempt_to_durable_completion(service/cleanup_executor/tests.rs). Previously it avoided node-manager through the cleanup executor's test dispatch hook. Now it drives the realCleanupSandboxdispatch against a fake that answersUnavailableonce and then succeeds. It asserts both requests (fence,keep_filesystem,immediate_kill,force, return after slot release), that the fake received no other call, and that the row endsStopped.immediate_reclaim_backlog_uses_each_entrys_own_node_manager(new,service/recovery/staging_cleanup/tests.rs). The test holds both reclaim slots, then submits two entries through two different fakes. They are regular files, so the local removal fails even for root. A third submission through a third fake drains the backlog. Each fake receives exactly its own entry'sDeleteCacheArtifact.send_source_inspection_is_cached_per_connector(new modulebtrfs_via_nodemgr/capability_cache_tests.rs, because Defer btrfs-send capture by default in sandbox-manager #287 editsbtrfs_via_nodemgr/tests.rs). Two fakes give opposing capability answers. Each connector gets its own answer twice, from a single probe.Two tests with incidental node-manager fallbacks.
immediate_reclaim_removes_ordinary_entries_and_leaves_the_rest_in_the_laneandconfirmed_node_pause_reports_paused_and_carries_the_execution_fencenow pass a fake connector. ADeleteCacheArtifactor Firecracker-state fallback in those tests can no longer reach the host.Mutation proof
Each mutation was applied, the named test was run with
--retries 0, and the file was restored. Node-manager was the fake throughout.kill:StopProcessdrops the execution fencekill: the scheduled cleanup drops the filesystem (keep_filesystem: false)kill: the admitted cleanup completes toCreated, notStoppedkill: at theStopProcessdispatch'sErrarm, return the error instead of continuingrun_cleanup_after_persistence_failure), which this test never reaches. It moves watchdog and snapshot-scheduler deregistration from the handler to the cleanup. And for blocking kills, it skips the wait for cleanup to complete (run_admitted_cleanup_to_completion/persist_and_retry_cleanup_failure), so a blocking caller would get the error before cleanup finishes. All three remain uncoveredCleanupSandboxinvertsimmediate_killStopProcess/CleanupSandboxDeleteCacheArtifactThe other required seam check, reverting to the default host connector, was not run. It would dial the fixed host socket, which the host-safety rule forbids even on a mutation's regression path. A4 and B4 prove the same dependence without contacting the host.
Host safety
Unimplementedbefore any destructive step.watchdog.remove_sandboxchecksexists()on/space/run/sandboxes/<fresh id>/space-netgtw-svc.pid. The ID is new, so nothing exists there.Overlaps with open PRs
Rebased onto
a8b5df6, which merged #269, #271, #273, #275, #277, #278 and #302. Those PRs change no sbx-mgr production code, so the call-site inventory above, taken at7a41941, still holds. Open PRs were re-checked against this head withgit merge-tree, plus a scan of each diff's added lines for the APIs this PR changes and forSandboxManagerServiceOptionsliterals:isolated_serviceinservice/grpc_core/tests.rslists everySandboxManagerServiceOptionsfield, so this PR now addsnode_managerthere. The value isnode_manager_transport::fake::unbound(&scratch.join("absent-node-manager.sock")): a connector on a socket inside the test's scratch directory that nothing binds, next to the absent host-control and credential-manager sockets it already uses. Any node-manager call from those tests fails closed.fake::unboundis new innode_manager_transport/fake.rsand shares the fake's lazy-client construction. sbx-mgr: characterize stop, delete and create core RPC paths (tests only) #271's tests are otherwise unchanged. Their stop and delete cases still reach node cleanup only through the executor's test dispatch hook.node_manager:line toruntime_liveness_optionsinservice/tests.rs, the function tests: keep three tests off host state they don't own #278 edited.AGENTS.mdit added tells tests to give the code under test a test-served node-manager; this connector is how they do it.SandboxManagerServiceOptionsliteral on main lists every field, and no open PR adds one.cache_cleanup/periodic/tests.rs, is also present against main. This PR'sservice/tests.rsdeletion is far from sbx-mgr: keep quarantined rows through inventory and bound recovery that can never succeed #270's hunks.db.rsandwatchdog.rs. This PR adds one more conflict hunk inwatchdog.rs, because both rewrite theprobe_composed_healthcall incomposed_health_loop's spawned probe. Keeping Quarantine sandboxes after sustained guest-agent probe failure #110's version of that line breaks compilation: it needs thelet node_manager = node_manager.clone();beforepending.spawnand the extra&node_managerargument.AGENTS.mdsaysconnect()/lazy()sites cannot be redirected; with this PR they can.btrfs_via_nodemgr.rs. This PR adds the connector as the last parameter there, so Defer btrfs-send capture by default in sandbox-manager #287's hunk context is untouched, and the merge is clean.grpc_lifecycle.rsis also present against main.Validation
cargo fmt --all -- --check, at56a295dcargo clippy --workspace --locked --all-targets --all-features -- -D warnings, at56a295dcheck-rust-error-policy.sh,generate-env-contract.sh --check,source_guards/env_test_hook_boundary.py, at56a295dcheck_no_inline_rust_test_modules.py(two inline modules inobservability) andsource_guards/thin_service_mains.py, at56a295da8b5df6; files not touched hereSPACE_CI_REQUIRE_TEST_DEPS=1 cargo nextest run --locked -p pplx-space-sbx-mgr --retries 0, two consecutive runs at56a295d(nextest's Postgres setup script passed)#[ignore]test not selected); 0 dependency-skip markersa314315space-localcluster per attempt on the current harness, built from #299's headb196857space-sbxmgr-svcsha2568e9ce23a…7090in both workerspace-sandbox-managerpods, before and after the suites, equals the staged artifact built from this checkout; the synced build source for sbx-mgr is identical tob196857; the binary contains thenode_manager_transportandtemplate_resolvermodule pathstilt get uiresources: 42/42ok, none pending or in errorsmoke-kind-e2e.shBOOTSTRAP_TIMEOUT=1800 DATA_PLANE_URL=http://127.0.0.1:8090smoke-kind-typed.sh all, attempt 1SKIP_BOOTSTRAP=1,SPACE_SMOKE_KIND_ADMIN_TOKEN_FILEandSPACE_SMOKE_KIND_COLD_RECEIPTSset as inkind-ci.yml, unshardedlocal.template_cache_lru_eviction_reclaimsfailed and the following scenarios were inconclusive. Docker's data disk (49 GB) reached 100%, so the Kind nodes' overlays could not write:os error 5under/space/cacheand Postgrescould not write init file. A rerun on that cluster could not start sandbox pods (pod_never_ready), so the cluster was torn downdocker builder prune -afanddocker image prune -fonlysmoke-kind-typed.sh all, rerun on a new clusterecw.cold_create_environment)The mutation proof and Kind evidence come from the pre-rebase heads:
a314315, and #299's headb196857, which containeda314315unchanged. They were not re-run after the rebase. Since then, the only changes to this PR are thefake::unboundtest helper and the newnode_managerfield in #271's testisolated_service, both test-only; main's merged PRs change no sbx-mgr production code.🤖 Generated with Claude Code