Skip to content

test(runtime-core): size release-check scans for load and pin the drop, kill-anchor and retired-format guards - #501

Merged
roodboi merged 8 commits into
nextfrom
wf/g107-runtime-core-load-and-guards
Nov 2, 2026
Merged

roodboi merged 8 commits into
nextfrom
wf/g107-runtime-core-load-and-guards

Conversation

@roodboi

@roodboi roodboi commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Current state (head 67400987)

Head 6740098790762bed610b990f21ee14c9e27caa22 merges origin/next at 8621d907 and fixes the should-fix from the independent review of 619fb8e1. Sections below this one describe older heads.

Change: partial_hello_has_fixed_deadline_and_never_connects now 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):

  • Changed test: 12 of 12 runs passed on the final source, each confirmed to run the test. A further 35 passed before a formatting-only edit.
  • Mutation: with RelayLoop::admit using self.limits.connect_timeout instead of its budget, 5 of 5 runs failed with E_TEST_HANDSHAKE_DEADLINE_NOT_FROM_BUDGET. Reverted.
  • cargo fmt --check and cargo clippy --all-targets --all-features -- -D warnings pass.
  • Lib test binary, run once before the formatting-only edit: 1071 passed, 0 failed, 103 ignored.
  • Gitleaks over 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 619fb8e1641b1b7971babc7b5091a3de3c70e1d0 merges origin/next at ae032e79 and reworks the two defects from the failed re-verification of 6508480b. 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 with MAX_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.
  • The other fixed bounds that the verdict listed as non-blocking are unchanged.

Verified locally on macOS at load average about 11-22, with no load generators, using cargo +1.97.1 in packages/runtime-core

Check Result
Buffered hello test, repeated 20/20 passed
CPU counter test, repeated 20/20 passed
Child ratios over 30 runs native/inner 1.0002-1.0009, native/outer 0.9998-0.9999
Mutation: cpu_nanoseconds x2 CPU test failed 10/10 (E_TEST_CPU_COUNTER_UNITS)
Mutation: raw Mach ticks CPU test failed 10/10 (E_TEST_CPU_COUNTER_UNITS)
Mutation: admit_hello renews the deadline Buffered hello test failed 5/5
Mutation: handshake progress renews the deadline Buffered hello test failed 5/5
Lib test binary, one run 1070 passed, 0 failed, 102 ignored
cargo fmt --check Passed
cargo clippy --all-targets --all-features -- -D warnings (Zig 0.15.2) Passed
gitleaks over 6508480b..619fb8e1 No leaks found

All mutations were reverted, confirmed by file hash.

Not verified

  • Hosted CI at 619fb8e1 had not finished when this section was written.
  • These tests have not been re-run under the heavy load that exposed the original failure. No loaded lib runs were done at this head, and an independent re-verification is still open.
  • The full test suite and bun run test:local were not run.

Remaining limit: admit_hello refuses 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::drop is refactored behind a named budget, and its behavior is unchanged. Refs HACK-1457

Root cause and fix

1. Load-sensitive release-check tests

Cause. Every failing test reaches lifecycle::lock_absent_disks, which runs a whole-host lsof over the owned disks with the production STOP_HANDLE_WAIT of 5 s. lsof walks 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 as provider_timeout_uncertain before it reaches its result under test.

I reproduced this on next (246f1b4) by adding 20 CPU spinners and 20 /bin/sh -c true spawn loops (load ~105-120). Run 3 of 3 failed 7 tests, all with provider_timeout_uncertain or 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_child
  • host_filesystem::device_migration_child and substituted_lock_child
  • short_home::explicit_recovery_child, vm_lock_release_child and active_socket_retry_child

The 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_WAIT is test_executable::LOADED_HOST_SCAN_WAIT (60 s) under cfg(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 to lock_absent_disks_within. 60 s is still well inside ISOLATED_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/sh in private_child tests. It is a hang guard, so it is now FIXTURE_SHELL_HANG_GUARD (60 s), with a closed E_TEST_FIXTURE_SHELL_HANG message.

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 s

The wait is now RELEASE_LEASE_WAIT, and the drop logic moved into Reservation::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 by recover_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_receipt checks four things:

  • a free lease removes the receipt
  • a held lease returns LeaseBusy only after the wait has elapsed
  • the receipt bytes and its reserved slot are unchanged after that
  • recover_record, the shared core of both recovery paths, then releases the receipt under the lease, and a late retry does not touch the receipt again

3. 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, and join().unwrap() hid the original message.

Fix. An InsideLease drop 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 test a_writer_panic_inside_the_lease_names_itself_not_an_overlap injects a panic in writer 0 and requires a failure that names only that panic.

4. #486 gaps (src/node/)

  • Kill anchor. New test 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 requires Child 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.
  • Retired-format fixture. The upgraded-node fixture is now the live child's own identity (observe(pid)) with only boot in the retired seconds:micros form. The old fixture also had start: 1. Note that schedule never calls verify: the "never signals" result comes from the structure. So the test now checks the only signal authority directly. verify(legacy, live) and verify(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 stubbed Bun.dns.lookup returns only {address: "::1", family: 6}, and the listener is on ::1. The test requires exit 0 and that both requests arrive with Host: [::1]:port. runStubbedPeerProbe gained an optional answer for this.

Mutation proofs (each reverted)

# Mutation Result
M1 Base binary (5 s scan budget) under the same churn load 1 of 3 full runs failed, 7 tests, all provider_timeout_uncertain
M2a Remove the receipt when the lease wait expires E_TEST_RECEIPT_CHANGED
M2b Stop waiting for the lease (single attempt) E_TEST_RELEASE_WAIT_NOT_HONORED
M2c Never remove the receipt under a free lease E_TEST_RELEASE_NOT_REMOVED
M3 InsideLease skips its decrement while panicking (old behavior) E_TEST_CLAIM_FAILURE_MISATTRIBUTED; the message lists writers 1-7 as E_TEST_CLAIM_LEASE_OVERLAP
M4a killed.get_or_insert_with(Instant::now) changed to killed = Some(Instant::now()) E_TEST_KILL_GRACE_REANCHORED after the 60 s guard; the 7 existing supervisor tests still pass
M4b Remove the retired-format guard in ownership::verify E_TEST_RETIRED_FORMAT_NOT_REFUSED
M5 target.hostname=x.address (no brackets) IPv6 test fails, exit 26 (rejected)

Evidence

  • Under load. I ran full lib runs (1,068 tests) with 20 CPU spinners and 20 spawn-churn loops, plus whatever else was running on the host. A trap stops the generator, and none was left running.
    • Batch 1 (commit a3b87e8, load 101-242): 29 of 30 passed. The one failure, at load 228, was a different fixed bound: private_child::independently_absent_guest_allows_reaping_failed_or_live_outer_transport waited 2 s for a spawned /bin/sh to exit. Commit 8d591db turns that wait and its sibling into FIXTURE_SHELL_HANG_GUARD (60 s).
    • Batch 2 (final head 8d591db, load 75-249): 29 of 30 passed. The one failure was https_recovery::full_legacy_command_child, which got https_recovery_refused after 1.3 s. That is not a wall-clock bound; see Gaps.
    • Across all 60 runs, no release-check test failed and no provider_timeout_uncertain appeared. The base commit under the same generator had failed 7 such tests in 1 of 3 runs.
  • Focused. private_child tests pass: 8. dependency_slots::tests, node:: and interrupted tests pass: 75 passed. tests/native-networks-fixture.test.ts passes: 67 tests.
  • Gates.
    • bun run check:local (fmt and clippy -D warnings, cargo 1.97.1) passes.
    • All-feature clippy (--all-targets --all-features, with Zig 0.15.2) passes.
    • Root bun run check (with privacy:check) and bun run --cwd packages/cli typecheck pass.
    • gitleaks 8.30.1 over origin/next..HEAD finds no leaks.
  • Full suite. 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_child failed once in 30 loaded runs, with https_recovery_refused after 1.3 s. The fixture binds an ephemeral port, drops it, and later requires port_absent on 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.
  • Other short fixed deadlines remain in the lib tests (host_endpoint, relay_loop and https_recovery tests use 1-3 s waits). None of them failed in the 60 loaded runs, so they were left as they are.
  • The production STOP_HANDLE_WAIT (5 s) is unchanged. admission_pool.rs documents lsof taking 5.4-6.5 s at load 30-80, so down and recover on a loaded host can still fail as provider_timeout_uncertain. This is reported as a finding and not changed here, because it changes product behavior.
  • network_update::stopped still has a fixed, unnamed 5 s lsof budget. No lib test reaches it after the scan, so it is not load-sensitive in the suite. It is left as is.
  • The kill-anchor test models a SIGKILL that never takes effect with a non-leader child and an injected leader identity. It does not use a real unkillable process.
  • The Docker e2e suite was not run locally, per policy.

🤖 Generated with Claude Code

…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>
@linear-code

linear-code Bot commented Oct 24, 2026

Copy link
Copy Markdown

HACK-1457

roodboi and others added 7 commits October 24, 2026 18:31
…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>
…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
roodboi marked this pull request as ready for review November 2, 2026 22:47
@roodboi
roodboi merged commit 9057298 into next Nov 2, 2026
23 checks passed
@roodboi
roodboi deleted the wf/g107-runtime-core-load-and-guards branch 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant