Repository navigation
test(runtime-core): size release-check scans for load and pin the drop, kill-anchor and retired-format guards - #501
Merged
Conversation
…p, kill-anchor and retired-format guards - Unit-test builds give the stop paths' whole-host lsof scan a named LOADED_HOST_SCAN_WAIT (60 s) instead of the 5 s production budget, which expired under load and failed release-check fixtures as provider_timeout_uncertain before their result under test. - Name Reservation::drop's lease wait (RELEASE_LEASE_WAIT), document the dead-owner recovery fallback, and prove a receipt left by an expired wait is unchanged and recoverable. - A claim writer that panics inside the lease is reported as that panic, not as a lease overlap by the writers after it. - Pin the once-only SIGKILL grace anchor and make the upgraded-node fixture differ from its live child only in boot format. - Cover the IPv6 branch of the peer probe's address rewrite. Refs HACK-1457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hang guard A 2 s wait for a spawned /bin/sh to exit or print its marker expired at load average ~230 during the HACK-1457 load runs. It is a hang guard, not a correctness bound, so it becomes FIXTURE_SHELL_HANG_GUARD (60 s) with a closed E_TEST_FIXTURE_SHELL_HANG message. Refs HACK-1457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntime-core-load-and-guards-r2
…ds and pick unallocated ports The second round of HACK-1457 load fixes. The CPU counter test now spins for 40 ms of thread CPU and brackets the native read between an inner and an outer process-clock window, so parallel CPU use cannot fail it. HTTPS recovery and discovery fixtures take a port below the kernel's ephemeral range, which no port-0 bind or outgoing connection can take before the fixture needs it. host_endpoint, relay_loop and https_recovery tests replace their fixed 1-6 s waits with LOADED_EVENT_HANG_GUARD, MAX_CONNECT_BUDGET or ISOLATED_HANG_GUARD; expiry tests still force an expired deadline. A new test covers the Reservation Drop wiring. Refs HACK-1457 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…per bound (HACK-1457) The buffered hello test used a 20 ms deadline that could expire before admit_hello ran under load. It now passes the largest accepted budget, asserts that exact deadline survives admission and handshake progress, and expires a deadline that the test sets itself. The CPU counter test could not fail on a doubled conversion, because parallel tests inflated the whole-process upper bound. It now measures in an isolated single-thread child, where process CPU is about thread CPU, so cpu_nanoseconds x2 and raw Mach ticks both fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admit with a 4 s budget that differs from the reactor's 5 s connect timeout and bracket the observed deadline against that budget, so an admit that ignored its budget now fails. The deadline is still expired by hand, so a loaded host cannot race it (HACK-1457). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
November 2, 2026 22:47
roodboi
added a commit
that referenced
this pull request
Nov 3, 2026
…an injectable probe (HACK-1328) (#553) ## Why `down`, `recover` and host-filesystem inspection prove the provider released its disks by running `lsof` over `storage.raw` and `overlay.raw`. That scan was bounded at 5 s (`STOP_HANDLE_WAIT`). `lsof` walks every process's descriptors, and on a loaded host it takes longer than that. `down` then ended as `provider_timeout_uncertain`, wrote no clean-stop marker, and the next restart fell back to the fresh disk budget. The start path's release check already used 30 s. Neither bound had a test, because `lsof` is a fixed host path. ## What changed - **New `lifecycle/disk_handles.rs`.** The scan now goes through an injectable `HandleProbe` and a `Clock`. Production uses the host `lsof` and the monotonic clock. The answer is interpreted the same way for every probe: only exit 1 with no stdout and no stderr proves the disks closed. The ownership checks are unchanged (VM lock, `lsof`, sockets, disk identity). - **Stop bound raised from 5 s to 30 s**, the same as `RELEASE_HANDLE_WAIT`. Both constants now live together with their measurements documented. 30 s is more than four times the slowest scan measured. With `down`'s other bounded waits (15 s guest shutdown acknowledgement, 10 s provider exit), a stop still ends within about a minute. - **`process::capture` split** into `spawn`, `Pending::poll`/`expire` and `wait_within(pending, timeout, clock)`. The bounded wait can now run on a fake clock. Production behavior is the same: the bound covers exit plus stream close; on timeout only the direct child is killed and reaped; a helper that keeps output open is reported as `provider_output_uncertain`. - **Deterministic `lsof` tests.** In test builds only, real `lsof` scans take a per-user `flock`, so every test process (isolated children included) runs one whole-host scan at a time. Each scan's bound starts once it holds the lock. Concurrent scans under load are what timed these tests out. The lock is released explicitly, so a forked copy of the descriptor cannot keep it held. - **`scripts/measure-lsof-scan.ts`** times the exact scan (sequential and concurrent rounds, optional CPU burners) and prints JSON with no paths or hostnames. - **README:** one sentence on the bound next to the clean-stop marker description. ## Measurements These are from a Linux host with 20 CPUs (`bun scripts/measure-lsof-scan.ts`). Every scan returned the clean no-holder answer. | processes | load (1 min) | round | median | p95 | max | |---|---|---|---|---|---| | ~790 | 57 | sequential | 0.38 s | 0.46 s | 0.51 s | | ~810 | 62 | 8 concurrent | 2.41 s | 2.86 s | 2.87 s | | ~840 | 84 | sequential, 40 burners | 0.66 s | 1.82 s | 4.88 s | | ~890 | 121 | 16 concurrent, 80 burners | 4.16 s | 4.99 s | **5.34 s** (above the old 5 s bound) | The earlier macOS figure, 5.4–6.5 s at about 1,850 processes and load 30–80, came from the previous `RELEASE_HANDLE_WAIT` comment. **No new macOS timings have been taken yet.** To take them, run `bun scripts/measure-lsof-scan.ts --runs 32 --concurrency 8` (add `--burn N` for CPU contention) on a loaded Mac. ## Tests `disk_handles::tests` are not platform-gated, so they run on Linux and macOS: - `both_bounds_outlast_a_slow_loaded_host_scan`: a scan four times the slowest measured one (26 s on the fake clock) passes under both bounds. - `both_bounds_expire_a_hung_scan_as_uncertain_at_the_bound`: a scan that never finishes is expired at the bound as `provider_timeout_uncertain`. The test also checks that each bound is at most 45 s. - `a_scan_finishing_after_the_bound_is_uncertain`: a "no holder" answer that arrives late is not accepted. - `only_a_silent_no_holder_answer_proves_the_disks_closed`: a listed PID, a diagnostic on stderr, exit 0 and exit 2 all refuse as `stop_uncertain`. - `host_lsof_finds_a_held_disk` and `host_lsof_proves_closed_disks_closed` (isolated child): the same checks against real `lsof`. They skip on Linux hosts without `lsof`; macOS must have it. `process::tests` add exit status and both streams, kill and reap on timeout, and a helper that retains output reported as `provider_output_uncertain`. **Mutation testing.** All 11 mutations were killed: - stop bound set to 5 s, and to 600 s - release bound set to 5 s, and to 600 s - lsof exit status ignored; lsof stderr ignored - wait doubled at the call site - wait never expires - expiry returns without signalling the child - no kill on timeout - output retention reported as a timeout ## Where it ran - Linux (this host): `cargo fmt --check`, `cargo clippy --all-targets -D warnings`, the same with `--all-features`, and the focused tests (`provider::lifecycle::disk_handles`, `provider::process`) all pass. The full `cargo test` run was still queued behind other work on this machine's shared job limiter when I pushed; hosted CI is the full-suite signal. - macOS: I only type-checked, via `cargo clippy --target aarch64-apple-darwin --all-targets` (default and all features, clean), cross-compiled from Linux. The macOS-only lifecycle tests (`admission_pool`, `short_home`, `host_filesystem`, `interrupted`) were **not run locally**. The CI `Candidate core (macos-latest)` job runs them. ## Remaining gaps - No macOS scan timings measured yet (see above). - Call-site wiring (`lock_absent_disks` → stop bound, `released` → release bound) is covered by the macOS lifecycle tests only. Both bounds are 30 s, so swapping them would change nothing. Linear: HACK-1328 ## Review fixes (after fee8ad8) - `process::wait_within` reads the clock after every poll. A result first seen at or after the bound is refused as `provider_timeout_uncertain`, so a waiter that oversleeps past the deadline can no longer accept a late scan. New fake-clock tests oversleep across the bound (refused) and within it (accepted). Restoring the old order makes the first test fail. - `scripts/measure-lsof-scan.ts` owns every child it starts: - Burners and scans are registered as they are spawned, inside the cleanup scope. - Each scan is bounded across its exit and both streams (`--scan-timeout`, default 120 s). - Abort and failure cancel and await in-flight scans, then kill and await every child. - An already-aborted signal starts nothing. `--lsof` lets you time a different `lsof`. - `tests/measure-lsof-scan.test.ts` has 8 tests: a clean run, a hung scan, a held pipe, a partial launch failure, abort, an already-aborted signal, a concurrent spawn failure, and SIGINT to the CLI with a held pipe. - The merge of `next` keeps #501's test-only 60 s `LOADED_HOST_SCAN_WAIT` for the `lock_absent_disks` fixtures, as `STOP_SCAN_WAIT`. Production stays at the 30 s `disk_handles::STOP_HANDLE_WAIT`. - Independent review (GPT-6-Astra, codex-dio): CLEAN at `fe29abef`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- codesmith:footer --> --- <a href="https://backend.blacksmith.sh/track/run-proof?expires=1796251618&installation_model_id=17053&pr_number=553&ref=codesmith_pr_footer&repository=hack-dance%2Fhack&return_to=https%3A%2F%2Fgithub.com%2Fhack-dance%2Fhack%2Fpull%2F553&variant=run_proof_on_this_pr&signature=a23dd4dcb33a18be918cc3a6da9f0769b547c6c66b809b0fd840cf3af4469270"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/proof-run-proof-on-this-pr-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/proof-run-proof-on-this-pr-light.svg"><img alt="Run Proof on this PR" src="https://pr-comments-assets.blacksmith.sh/codesmith/proof-run-proof-on-this-pr-dark-3x.png" width="168" height="28"></picture></a> <sup><a href="https://app.blacksmith.sh/hack-dance/codesmith/hack/pr/553?autoLogin=true&ref=codesmith_pr_footer">Open in [code]smith</a> · Need help on this PR? Tag <code>@codesmith-bot</code> with what you need.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer --> --------- Co-authored-by: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Current state (head
67400987)Head
6740098790762bed610b990f21ee14c9e27caa22mergesorigin/nextat8621d907and fixes the should-fix from the independent review of619fb8e1. Sections below this one describe older heads.Change:
partial_hello_has_fixed_deadline_and_never_connectsnow admits with a 4 s budget, asserts that this is shorter than the fixture's 5 s connect timeout, and brackets the observed deadline against the 4 s budget. It still expires the deadline by hand. Test-only; no production code changed.Evidence (local, macOS, Rust 1.97.1, Zig 0.15.2, on the final test source unless noted):
RelayLoop::admitusingself.limits.connect_timeoutinstead of its budget, 5 of 5 runs failed withE_TEST_HANDSHAKE_DEADLINE_NOT_FROM_BUDGET. Reverted.cargo fmt --checkandcargo clippy --all-targets --all-features -- -D warningspass.619fb8e1..67400987: no leaks.Not verified: hosted CI at this head, the integration test binaries and the full suite, and the areas the review did not read (including whether the supervisor also signals the single process as well as the group).
Current state (head
619fb8e1)Head
619fb8e1641b1b7971babc7b5091a3de3c70e1d0mergesorigin/nextatae032e79and reworks the two defects from the failed re-verification of6508480b. Everything below this section is earlier history and describes older heads.Changes in this rework
buffered_hello_keeps_original_deadline_and_wrong_proof_never_connects: replaced the fixed 20 ms deadline withMAX_CONNECT_BUDGET. It still asserts that the exact deadline survives admission. It now also asserts that handshake progress does not renew a deadline, and it expires a deadline that the test sets itself.cpu_counter_advances_in_nanoseconds_under_work: now measures in an isolated single-thread child (cpu_counter_child), so the upper bound is tight. The bounds are 2/3 and 3/2, and the false comment is corrected.Verified locally on macOS at load average about 11-22, with no load generators, using
cargo +1.97.1inpackages/runtime-corecpu_nanosecondsx2E_TEST_CPU_COUNTER_UNITS)E_TEST_CPU_COUNTER_UNITS)admit_hellorenews the deadlinecargo fmt --checkcargo clippy --all-targets --all-features -- -D warnings(Zig 0.15.2)gitleaksover6508480b..619fb8e1All mutations were reverted, confirmed by file hash.
Not verified
619fb8e1had not finished when this section was written.bun run test:localwere not run.Remaining limit:
admit_hellorefuses a deadline more than 5 s away, so a stall of 5 s or more between computing the deadline and that call would still fail the test.Follow-ups from the #484, #485 and #486 verifiers. This changes test code, with one exception:
Reservation::dropis refactored behind a named budget, and its behavior is unchanged. Refs HACK-1457Root cause and fix
1. Load-sensitive release-check tests
Cause. Every failing test reaches
lifecycle::lock_absent_disks, which runs a whole-hostlsofover the owned disks with the productionSTOP_HANDLE_WAITof 5 s.lsofwalks every process on the host. Under load, and with other tests' isolated children scanning at the same time, one scan can take longer than 5 s. The fixture then fails asprovider_timeout_uncertainbefore it reaches its result under test.I reproduced this on
next(246f1b4) by adding 20 CPU spinners and 20/bin/sh -c truespawn loops (load ~105-120). Run 3 of 3 failed 7 tests, all withprovider_timeout_uncertainor the isolated parent of such a child:interrupted::unadopted_unclean_stop_child(interrupted.rs:300, the one in the issue)admission_pool::uncertain_stop_rewrite_childhost_filesystem::device_migration_childandsubstituted_lock_childshort_home::explicit_recovery_child,vm_lock_release_childandactive_socket_retry_childThe G105 verifier logs show the same set at load 78-137.
Fix. The unit-test build gets a named budget instead of the fixed 5 s.
STOP_HANDLE_WAITistest_executable::LOADED_HOST_SCAN_WAIT(60 s) undercfg(test). The production value (5 s) is unchanged. These fixtures prove what the scan reports (status 1 and no output), not how fast it reports it, so what they prove is unchanged. Tests of the timeout path itself already pass their own bound tolock_absent_disks_within. 60 s is still well insideISOLATED_HANG_GUARD(120 s), so a scan that hangs is still reported.The loaded runs found one more fixed bound, a 2 s wait for a spawned
/bin/shinprivate_childtests. It is a hang guard, so it is nowFIXTURE_SHELL_HANG_GUARD(60 s), with a closedE_TEST_FIXTURE_SHELL_HANGmessage.I also tightened
unadopted_unclean_stop_child's second check. It used to accept any error (is_err()), so a scan timeout also satisfied it. It now requires the identification error itself (provider_state) and requires that no disk was recorded.2.
Reservation::drop's unnamed 5 sThe wait is now
RELEASE_LEASE_WAIT, and the drop logic moved intoReservation::release_within(wait) -> Release. The fallback is documented. If the lease stays busy, or the lease, home or receipt does not verify, the receipt is left as written and its slots stay reserved. It is then released byrecover_orphan(never-admitted runs) or by dead-owner graph cleanup (recover_cleaned). Both require the recorded owner to be dead. The behavior is unchanged.New test
a_release_that_outlasts_a_busy_lease_leaves_a_recoverable_receiptchecks four things:LeaseBusyonly after the wait has elapsedrecover_record, the shared core of both recovery paths, then releases the receipt under the lease, and a late retry does not touch the receipt again3. Claim-writer failure attribution
Cause. A writer that panicked inside the lease never decremented the shared "inside" counter. Every later writer then failed with
E_TEST_CLAIM_LEASE_OVERLAP, andjoin().unwrap()hid the original message.Fix. An
InsideLeasedrop guard is declared after the lease guard, so it drops first and leaves the count before the lease is released. Joins now collect every writer's panic message in writer order (E_TEST_CLAIM_WRITER_PANICKED (writer N: …)). New testa_writer_panic_inside_the_lease_names_itself_not_an_overlapinjects a panic in writer 0 and requires a failure that names only that panic.4. #486 gaps (
src/node/)repeated_kills_of_a_child_that_never_exits_keep_the_first_kill_anchor. The child is not a group leader, but its recorded identity says it is, so every group signal is authorized and finds no group (ESRCH, tolerated). The child outlives each SIGKILL and is re-signalled on every later pass. The test requiresChild termination deadline exceeded., at least three signal checks, and the child still alive afterwards. A named 60 s hang guard turns a regression into a failure instead of a hang.observe(pid)) with onlybootin the retiredseconds:microsform. The old fixture also hadstart: 1. Note thatschedulenever callsverify: the "never signals" result comes from the structure. So the test now checks the only signal authority directly.verify(legacy, live)andverify(legacy, legacy)must both refuse with the retired-format message.5. IPv6 probe branch
New test
peer probe fetches an IPv6-only answer through a bracketed host. The stubbedBun.dns.lookupreturns only{address: "::1", family: 6}, and the listener is on::1. The test requires exit 0 and that both requests arrive withHost: [::1]:port.runStubbedPeerProbegained an optionalanswerfor this.Mutation proofs (each reverted)
provider_timeout_uncertainE_TEST_RECEIPT_CHANGEDE_TEST_RELEASE_WAIT_NOT_HONOREDE_TEST_RELEASE_NOT_REMOVEDInsideLeaseskips its decrement while panicking (old behavior)E_TEST_CLAIM_FAILURE_MISATTRIBUTED; the message lists writers 1-7 asE_TEST_CLAIM_LEASE_OVERLAPkilled.get_or_insert_with(Instant::now)changed tokilled = Some(Instant::now())E_TEST_KILL_GRACE_REANCHOREDafter the 60 s guard; the 7 existing supervisor tests still passownership::verifyE_TEST_RETIRED_FORMAT_NOT_REFUSEDtarget.hostname=x.address(no brackets)rejected)Evidence
private_child::independently_absent_guest_allows_reaping_failed_or_live_outer_transportwaited 2 s for a spawned/bin/shto exit. Commit 8d591db turns that wait and its sibling intoFIXTURE_SHELL_HANG_GUARD(60 s).https_recovery::full_legacy_command_child, which gothttps_recovery_refusedafter 1.3 s. That is not a wall-clock bound; see Gaps.provider_timeout_uncertainappeared. The base commit under the same generator had failed 7 such tests in 1 of 3 runs.private_childtests pass: 8.dependency_slots::tests,node::andinterruptedtests pass: 75 passed.tests/native-networks-fixture.test.tspasses: 67 tests.bun run check:local(fmt and clippy-D warnings, cargo 1.97.1) passes.--all-targets --all-features, with Zig 0.15.2) passes.bun run check(with privacy:check) andbun run --cwd packages/cli typecheckpass.origin/next..HEADfinds no leaks.bun run test:local(cargo 1.97.1, no added load) at 8d591db: all 21 test binaries passed, including the lib (1,068 passed). Hosted CI at 8d591db is all green: test shards 1-4, Candidate core (ubuntu and macos), docker-e2e shards 1-4, Config compiler on 4 platforms, Runtime state models, linux-process-lifetime, runtime-images and secret-scan.Gaps
https_recovery::full_legacy_command_childfailed once in 30 loaded runs, withhttps_recovery_refusedafter 1.3 s. The fixture binds an ephemeral port, drops it, and later requiresport_absenton that port, so another socket on the host can take the port in between. This is a port-reservation race, not a timing bound, so it is reported as a finding and not changed here. The refusal is closed (no stage is reported), so I have not confirmed which check refused.host_endpoint,relay_loopandhttps_recoverytests use 1-3 s waits). None of them failed in the 60 loaded runs, so they were left as they are.STOP_HANDLE_WAIT(5 s) is unchanged.admission_pool.rsdocumentslsoftaking 5.4-6.5 s at load 30-80, sodownandrecoveron a loaded host can still fail asprovider_timeout_uncertain. This is reported as a finding and not changed here, because it changes product behavior.network_update::stoppedstill has a fixed, unnamed 5 slsofbudget. No lib test reaches it after the scan, so it is not load-sensitive in the suite. It is left as is.🤖 Generated with Claude Code