Repository navigation
node-mgr: let tests inject the runtime supervisor and host-reading defaults - #298
Conversation
…faults Node-manager's service constructor built its runtime-supervisor client and its memory-admission sources itself, so only the production supervisor socket, /proc/meminfo and the Firecracker cgroup base could back them. The startup settings (ServiceSettings) now carry both, beside the runtime paths and lifecycle limiter they already carried. ServiceSettings::from_env builds exactly what the constructor built before: the same lazily connected client over the production socket, with the same dial, liveness check and retry policy, and the same default memory sources. Constructing the lazy client opens no connection and logs nothing, so building it earlier in startup changes no behavior. The unit-test service constructor now owns all four dependencies: - Runtime discovery and memory admission read paths under a vacant temporary directory, so both fail closed instead of reading the node's Firecracker installation, /proc/meminfo or its cgroups. - The runtime-supervisor client reaches an in-process supervisor that refuses every RPC with Unimplemented at once. It no longer dials the production socket, and so no longer retries for 15 seconds where that socket is absent. - The lifecycle limiter uses the compiled-in limits, whatever SPACE_NODEMGR_LIMIT_* the runner sets. Tests that reach these paths now inject what they need: - cancelled_probe_retains_capacity_until_blocking_work_finishes discovers a test-owned Firecracker installation and requires discovery to succeed while diagnostics are saturated. It used to read the node's fixed installation paths and accept any failure other than ResourceExhausted. - The capacity case of blocked_cleanup_phase_times_out_before_stopping_the_runtime injects a 1:1 cleanup limiter instead of re-running itself in a child process with SPACE_NODEMGR_LIMIT_CLEANUP_SANDBOX set. It also holds the next phase's artifact lock, so a cleanup that ignores capacity stops before it reads the run directory. The fixture claims its lease off the Tokio runtime, so allocation no longer dials node-ctl's fixed socket. - UpdateCgroup gains tests through an in-process scripted supervisor: it forwards the current execution with memory clamped to the node's cap, refuses a stale execution before the supervisor sees it, and keeps the supervisor's status code when the supervisor refuses. - concurrent_new_lease_admission_allows_only_one_reserved_cap starts from the unit-test constructor instead of a hand-built service that carried the production supervisor client, the host runtime paths and the environment-read limiter. RuntimePaths::default, the fixed host paths that only tests used, is removed, as is node-manager's now-unused pplx-space-test-env dev-dependency.
nickdujay-pplx
left a comment
There was a problem hiding this comment.
Reviewed cebb548. Production still constructs the same lazy supervisor client and memory-admission sources; moving their construction into ServiceSettings does not introduce an eager connection or change the existing defaults. The test fixtures now isolate those dependencies, and the new UpdateCgroup coverage checks execution fencing, memory clamping, and error-code propagation.
Verified the CI test log: all 463 node-manager tests passed, including both new UpdateCgroup tests, the four cleanup-capacity/lock cases, and the runtime-discovery test. The full primary Rust run passed 8,873 tests with zero skips and zero dependency-skip markers. No blocking findings. The Kind runs described in the PR were not independently rerun.
|
Review summary for The production constructor still supplies the same lazy supervisor client and memory source. I checked the changed dependency consumers and the new UpdateCgroup coverage for stale execution rejection, memory clamping, forwarding, and error-code preservation. The CI log confirms all 463 node-manager tests passed; the overall Rust run reported 8,873 passes with no skips. I did not independently rerun Kind. |
What this changes
Node-manager's service constructor built two process-boundary dependencies itself: the runtime-supervisor client (the production Unix socket) and the memory-admission sources (
/proc/meminfoand the Firecracker cgroup base). Only those production endpoints could back them. This PR moves both intoServiceSettings, the options struct the constructor already takes.ServiceSettingsalready carried the other two dependencies, the runtime paths and the lifecycle limiter.The unit-test constructor (
service_for_unit_tests) now defaults all four to test-owned values. Tests that reached a host dependency now inject what they need, and UpdateCgroup gains unit coverage.The seams and their production wiring
RuntimeSupervisorLazyClient::new()insideNodeManagerService::with_memory_admission_config_and_task_trackerRuntimeSupervisorLazyClient::new()inServiceSettings::from_env; the constructor moves it into the serviceMemoryAdmissionSources::default()inside the constructorMemoryAdmissionSources::default()inServiceSettings::from_envServiceSettings::from_env(SPACE_FIRECRACKER_*or the fixed defaults)LifecycleLimiter::from_env()inServiceSettings::from_env, given the task tracker by the constructorProduction builds the same values as before:
connect_supervisor_proven_livedial with its health round-trip, same retry policy and connect timeout.RuntimeSupervisorLazyClient::new()only allocates. It opens no connection and logs nothing, so building it at the start ofrunrather than after the pool opens changes no behavior, ordering or log line.Defaultvalue.There is no change to wire formats, schema, metrics, configuration or log lines.
ServiceSettingsderivesDebug, so the client gets a field-lessDebugimpl. The startup log keeps listing explicit fields, not aDebugdump.Every place the service reads these dependencies goes through the injected field. That was true before and still is:
runtime_supervisorcontrol.rsupdate_cgroup→UpdateCgroup;runtime.rslaunch_inner→LaunchSidecar,stop_process_inner→StopProcess;launch_prepare.rslaunch_sandbox_inner→LaunchFirecracker;cleanup_helpers.rsstop_sandbox_runtime_with_supervisor→StopSandboxRuntime;cleanup_inspect.rscleanup_sandbox_phases→BindExecution,RetireExecution;execution_fence.rsbind_execution_for_allocation→BindExecutionmemory_admission_sourcesruntime.rsallocate_new_lease→MemoryAdmissionConfig::observe_active_leasesruntime_pathsservice.rsrefresh_runtime_inventorylifecycle_limiteracquireincontrol.rs,filesystem_work.rs,runtime.rs,launch_prepare.rs,cleanup_inspect.rs,snapshots.rs,encrypted_volumes.rs;closeincleanup_inspect.rsRuntimePaths::default()returned the fixed host paths, and only tests used it, so this PR removes it.ServiceSettings::from_envbuilds its paths explicitly, as it did before.Unit-test defaults
service_for_unit_testsused the production supervisor client,RuntimePaths::default(),MemoryAdmissionSources::default()andLifecycleLimiter::from_env(). It now uses:runtime_supervisor_client::tests::refusing_client).Unimplemented.scripted_in_process_client, serves the existing stub supervisor the same way, with no socket for the test to own.installed_runtime()orMemoryAdmissionSources::new).Unavailable) before it claims a slot.lifecycle_limiter::tests::default_limiter), whateverSPACE_NODEMGR_LIMIT_*the runner sets.Which tests relied on a host default. At the base commit I made each host default panic and ran the package's tests:
cancelled_probe_retains_capacity_until_blocking_work_finishesreached one: it read the fixed runtime paths. It is converted below./proc/meminfo.Tests converted
All mutation runs below used a tripwire build. It panics on the node-ctl dirty-mark dial, on
read_pid_from_symlink(run directory), on a dial throughRuntimeSupervisorLazyClient::new(), on a/proc/meminforead and on runtime discovery under/space. No mutation's regression path tripped it.At the base commit, the same tripwire fired in the four cases of
blocked_cleanup_phase_times_out_before_stopping_the_runtime(node-ctl dial from the fixture's on-runtime allocation) and incancelled_probe_retains_capacity_until_blocking_work_finishes(fixed runtime paths). Now they pass with no trip.service::tests::cancelled_probe_retains_capacity_until_blocking_work_finishes(runtime assets).ResourceExhausted.off_runtime_probe→ fails withResourceExhausted: node diagnostic capacity exhausted; retry.runtime_pathsoverride → fails withFailedPrecondition: kernel installation /tmp/space-node-mgr-unit-…/assets/default/kernel/vmlinux.bin is missing. That is the test-owned default, not a host path.service::cleanup_inspect::tests::blocked_cleanup_phase_times_out_before_stopping_the_runtime::case_1_capacity(lifecycle limiter).SPACE_NODEMGR_LIMIT_CLEANUP_SANDBOX=1:1to pin the limit.CleanupNodefixture now claims the lease off the Tokio runtime (allocate_test_lease), so allocation no longer dials node-ctl's fixed socket. This also covers the otherCleanupNodeusers.Releaseclass instead ofCleanupSandbox→ fails (timed out waiting for artifact transfer ownership≠timed out waiting for cleanup capacity).service::control::tests::update_cgroup_forwards_current_execution_with_clamped_memory(supervisor; new).FailedPrecondition) before the supervisor sees it. A current one is forwarded with its execution id and limits, with memory clamped to the node's per-sandbox cap.unwrap_erronOk).memory_mibis8192, not4096.Unimplemented … refuses UpdateCgroup).service::control::tests::update_cgroup_reports_a_supervisor_refusal_with_its_code(supervisor default; new).Internal→ fails.unwrap_erronOk).concurrent_new_lease_admission_allows_only_one_reserved_capnow starts from the unit-test constructor. Before, it hand-built a service carrying the production supervisor client, the fixed runtime paths and the environment-read limiter. Its assertions are unchanged.It is not host-clean: like every on-runtime allocation test, it still reaches the node-ctl dirty mark (below). So it is not claimed as a converted test.
installed_runtime()moved fromlaunch_prepare/tests.rsto sit besideservice_for_unit_tests, so both modules share it.pplx-space-test-envis no longer used by node-manager and is dropped from its dev-dependencies.Call sites left unrouted
node_state_sync::mark_node_status_dirtyis a free function thatslot_poolcalls on allocation, release and config updates. On a Tokio runtime it spawns a dial to node-ctl's fixed socket.NetworkPool, outside the service options this PR uses.every_failed_stop_preserves_the_active_lease_and_reservation(its on-runtimepool.allocate, after it first trips on run-directory pid links),runtime::tests(allocate, release and stop-process tables),execution_fence::tests, threelaunch_prepare/servicetests,cleanup_does_not_stop_a_lease_replaced_while_it_waited_for_the_gate,concurrent_new_lease_admission_allows_only_one_reserved_cap,network_pool_metrics::shutdown_stops_a_bounded_pass_of_wedged_probesand 12slot_pooltests.RUN_DIR,CACHE_DIR,TEMPLATE_DIRandJAIL_DIRinservice.rs, the host cgroup hierarchy read throughpplx_space_node_runtime_core::cgroup(e.g.execution_fence'ssandbox_is_quiescent, whichexecution_fence::testspreflight cases reach), andruntime_paths::RuntimePaths::firecracker()(jail layout) insnapshots.rs, are used throughout business logic. Routing them would put seams inside business logic.every_failed_stop_preserves_the_active_lease_and_reservation,inspect_sandbox_remains_available_when_diagnostics_are_saturatedandcleanup_does_not_stop_a_lease_replaced_while_it_waited_for_the_gate, which reaches them before its expectedadopt_stopping_leaserefusal. The twospaced_host_peer_*tests read pid links from temporary run directories, so they don't touch the host.run.rsactivate_runtime_supervisor_on_startactivates the supervisor's systemd unit at startup. It is not a client call.NetworkPool::open), so unit tests cannot call it. The production wiring is proven by the Kind runs below, not by a unit test.Host safety
/proc/meminfoand the fixed runtime paths.Follow-ups
service_for_unit_tests' doc that an admission refusal from the unscripted memory source names/proc/meminfowhatever path was read, becauseNodeMemoryCapacityError::Readhard-codes it. Tests that reach admission should injectMemoryAdmissionSources::new.Overlaps with open PRs
snapshots.rs,snapshots/tests.rs) and tests: keep three tests off host state they don't own #278 (loop_devices/tests.rs): no shared files. node-mgr: validate LoadSnapshot requests before staging host state #269's added tests use none of these dependencies. Its existing snapshot tests already inject their own supervisor.crates/libs/node/mgr/server/AGENTS.md. Its sentence "Unit-test services must override runtime paths, memory-admission sources, the lifecycle limiter and the supervisor client wherever a test reaches them; their defaults read the host, env or the production supervisor socket" becomes stale once this merges. Whichever lands second should say the defaults are test-owned and refuse.snapshots.rs: no shared files.Validation
Head
cebb5483360461ab7b4ead3d1641a0294178620c.cargo fmt --all -- --checkcargo clippy --workspace --locked --all-targets --all-features -- -D warningscheck-rust-error-policy.sh,generate-env-contract.sh --check,rustfmt --check scripts/generate-env-contract.rsscripts/source_guards/*.pyenv_test_hook_boundary,node_selector_extras_smoke,observability_vendor_boundarypass.thin_service_mainsfails oncrates/apps/vol/mgr/svc/src/main.rs, which this PR does not touch (pre-existing)check_no_inline_rust_test_modules.pycrates/libs/observabilityfiles this PR does not touch (pre-existing); nothing in node-mgrSPACE_CI_REQUIRE_TEST_DEPS=1 cargo nextest run --locked -p pplx-space-node-mgr --retries 0×3.config/nextest.tomlscopes the Redis setup topplx-space-api; node-mgr has no Redis-backed testsspace-nodemgr-svcsha25603db7458…d156matches across: the incremental artifact built after this commit, whose syncedcrates/andproto/are identical to this checkout; node-manager pods on all five clusters (all four pods on four of them; three of four on the first)ok, none pending, before the typed smokesmoke-kind-e2e.shsmoke-kind-typed.sh all, ECW passsmoke-kind-typed.sh all, native passnative 1/3,2/3,3/3; one fresh cluster each, as CI shards).native_contract.command_replay_credentialsis IGNORED by designECW pass detail:
SPACE_SMOKE_KIND_COLD_RECEIPTSand the admin token file set, as CI does): 49 passed, 0 failed, 27 inconclusive.No space left on device.local.ecw_template_build_from_oci_bootswas inconclusive both times. Build-manager's storage admission requires 52 GiB free (requires 55834574848 available bytes), and this host offered 15–34 GB. ENVIRONMENTAL.Native pass detail: inside
all, the pass could not start because harness bootstrap failed on the full disk (ENVIRONMENTAL). It was then rerun as the three shards above.🤖 Generated with Claude Code