Skip to content

fix(ci): reclaim the session e2e's state dir so the soak stops filling the runner - #1021

Merged
norrietaylor merged 1 commit into
mainfrom
fix/soak-e2e-state-dir-leak
Jul 29, 2026
Merged

fix(ci): reclaim the session e2e's state dir so the soak stops filling the runner#1021
norrietaylor merged 1 commit into
mainfrom
fix/soak-e2e-state-dir-leak

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 29, 2026

Copy link
Copy Markdown
Member

The failure

session-e2e-soak has been red on nightly-tests since the 2026-07-25 run (last green: 30073487997, 07-24). Every failure since has the identical shape:

Run Job duration Step 15 (Soak the session e2e (x10)) Logs
30419924349 46 min / 90 in_progress log not found
30244995348 22 min / 90 never recorded none
30191843749 14 min / 90 in_progress none
30182780159 63 min / 90 in_progress none
30180328655 66 min / 90 in_progress none

A step stuck at in_progress while the job reports failure, zero retrievable log lines, no uploaded artifacts, and the runner dead well inside its budget — that is the runner agent being killed, not a test assertion failing. It is the signature of ENOSPC on the runner. The single-session ci-linux-kvm lane, which runs the same e2e once, stays green throughout.

For scale: the last green soak ran all ten iterations plus the bulk proof in 3m25s. The failing runs die inside that same step.

The cause

scripts/session-e2e.sh never removed its $WORK state dir.

Its two sibling harnesses both do. bulk-upload-e2e.sh is explicit about why — "Leave nothing behind: a canary that strands a session (or a VM) per run is worse than no canary" — and stress-session-e2e.sh does the same. session-e2e.sh was the outlier, and it is the one soak-session-e2e.sh runs ten times back-to-back on a single runner.

That dir is not just metadata. On a VM lane it holds the provider's per-VM writable data volume, minimal/providers/local-minvmd0/data-vol.raw — a sparse image (DEFAULT_VOLUME_BYTES = 256 GiB nominal) whose host allocation is everything the guest wrote into it: its package cache, the session rootfs, the workspace. $WORK is minted fresh per iteration, so nothing is shared between them; each iteration re-pays that allocation, ten times, with nothing reclaiming any of it.

The fix

scripts/session-e2e.shrm -rf "$WORK" on teardown, matching the siblings. The min bug diagnostic bundle's fallback moves from $WORK to /tmp so it survives that teardown (the same fallback, for the same reason, that bulk-upload-e2e.sh already uses). Every diagnostic fail() collects is gathered before teardown runs, so nothing is lost.

scripts/soak-session-e2e.sh — measure free space on the filesystem holding those state dirs, print it with a per-check delta on every iteration, and stop with a real ::error:: if it falls under a floor (4096 MiB, override with SOAK_MIN_FREE_MIB).

The floor is the part that matters beyond this one bug: a genuine ENOSPC leaves nothing to read, which is why five nights of failures produced no diagnosis. This fails the step while a runner is still alive to say why, and the per-iteration deltas make it immediately visible if reclamation ever regresses again.

No workflow files touched — .github/workflows/ is frozen, and both changes land in the scripts/ extension point CI already schedules over.

Verification

  • scripts/lint-shell.sh — 24 scripts pass shellcheck (the gate crates/common/tests/shell_lint.rs runs in CI).
  • Leak, before/after. Stubbed min drives the harness through its fail()teardown() path — the same path every soak iteration exits by:
    BEFORE-HEAD    (exit 1): state dirs left behind = 1   /private/tmp/mnl-e2e.SjgiKq
    AFTER-patched  (exit 1): state dirs left behind = 0
    
  • Headroom check, exercised from an isolated copy (the live dev stack on this host would otherwise be reaped):
    • floor above actual free space → aborts before running anything (exit 1, zero iterations run), emitting the ::error::
    • floor of 0 → all iterations plus the bulk proof run, deltas reported
    • non-numeric floor → rejected, exit 2

The real proof is the next nightly (or a workflow_dispatch of nightly-tests): the soak step should complete and report a flat per-iteration disk delta.

Note: the diff is shell-only and touches no Rust, so the just ci Rust gates cover nothing here; just lint-shell is the gate for it and passes. I deliberately did not run just ci on this host — cargo clippy --all-targets -p minvmd would relink target/debug/minvmd and drop the hypervisor codesign out from under the running dev stack.

🤖 Generated with Claude Code

Note

Reclaim session e2e state directory on teardown to stop soak runner disk fill

  • session-e2e.sh teardown now deletes the working/state directory ($WORK), reclaiming provider-managed data volumes after each run.
  • Diagnostic bundles from mnl bug now default to /tmp instead of the state directory when MINVMD_BOOT_LOG is unset, avoiding writes under the path being deleted.
  • soak-session-e2e.sh adds a check_free guard before each iteration and before the bulk upload proof, aborting early with a GitHub Actions error if free space falls below a configurable SOAK_MIN_FREE_MIB floor.
  • A free_mib helper measures available disk space in MiB for a given path using portable df and awk, with platform-specific filesystem selection.

Macroscope summarized 0e060ac.

…g the runner

`scripts/session-e2e.sh` never removed its `$WORK` state dir. Its two sibling
harnesses do — `bulk-upload-e2e.sh` ("leave nothing behind: a canary that
strands a session per run is worse than no canary") and `stress-session-e2e.sh`
both `rm -rf "$WORK"` on teardown. This one was the outlier, and it is the one
`soak-session-e2e.sh` runs ten times back-to-back on a single runner.

The dir is not just metadata. On a VM lane it holds the provider's per-VM
writable data volume, `minimal/providers/local-minvmd0/data-vol.raw` — a sparse
image whose HOST allocation is everything the guest wrote into it: its package
cache, the session rootfs, the workspace. `$WORK` is minted fresh per run, so
nothing is shared between iterations and each one pays that allocation again,
ten times over, with nothing reclaiming any of it.

That matches how the nightly fails. `session-e2e-soak` has been red since the
2026-07-25 run (last green 07-24) and every failure has the same shape: the
soak step stuck at `in_progress`, the job reporting `failure`, zero retrievable
log lines, no uploaded artifacts, and the runner dead well inside the 90-minute
budget. That is the runner AGENT being killed, not a test assertion failing —
the signature of ENOSPC on the runner. The single-session KVM lane, which runs
the same e2e once, stays green.

Two changes:

- `session-e2e.sh` removes `$WORK` on teardown, and writes its `min bug`
  diagnostic bundle to /tmp rather than into `$WORK` when no boot-log dir is
  set, so the bundle survives that teardown (the fallback `bulk-upload-e2e.sh`
  already uses, for the same reason). Every diagnostic `fail` collects is
  gathered before teardown runs.

- `soak-session-e2e.sh` measures free space on the filesystem holding those
  state dirs, prints it with a per-check delta on every iteration, and stops
  with a real `::error::` if it falls under a floor (4096 MiB, overridable via
  `SOAK_MIN_FREE_MIB`). A genuine ENOSPC leaves nothing to read; this fails the
  step while a runner is still alive to say why, and the deltas make it
  obvious if reclamation ever regresses again.

Verified: `scripts/lint-shell.sh` passes (24 scripts). Before/after against a
stubbed `min` that drives the harness through its `fail` -> `teardown` path:
HEAD leaves a `/tmp/mnl-e2e.*` state dir behind, the patched script leaves
none. The headroom check was exercised from an isolated copy — floor above
actual free space aborts before running any iteration (exit 1, zero iterations
run), floor of 0 runs all iterations plus the bulk proof and reports the
deltas, a non-numeric floor is rejected with exit 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce995601-0cbb-409c-9731-c29a8ee2786d

📥 Commits

Reviewing files that changed from the base of the PR and between df22d71 and 0e060ac.

📒 Files selected for processing (2)
  • scripts/session-e2e.sh
  • scripts/soak-session-e2e.sh

Comment @coderabbitai help to get the list of available commands.

@norrietaylor
norrietaylor merged commit c1d466c into main Jul 29, 2026
29 checks passed
@norrietaylor
norrietaylor deleted the fix/soak-e2e-state-dir-leak branch July 29, 2026 14:44
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.

2 participants