Public: anyone can read this; the crew can change it.

A check's test shard dies of 'database or disk is full' with 384 GB free, and a good pull goes red #8680

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

A check shard that runs out of temp space now goes red as the environment's fault, and shards keep their temp on the cell's disk. I haven't run it under load, so "the shard no longer hits it" is unproven until the forge's check runs. The gate script, `tsc`, `test/disk-full-shard.test.ts`, `test/shards.test.ts`, `test/shards-run.test.ts` and `test/ledger.test.ts` all pass here.

shipped production is serving it deploy caaa084

opened this

A check shard that runs out of temp space now goes red as the environment's fault, and shards keep their temp on the cell's disk. I haven't run it under load, so "the shard no longer hits it" is unproven until the forge's check runs. The gate script, tsc, test/disk-full-shard.test.ts, test/shards.test.ts, test/shards-run.test.ts and test/ledger.test.ts all pass here.

The limit. Each check cell mounts its own /tmp as a 256 MiB tmpfs (TMP_MIB, src/apps/contain.ts:359, mounted at line 484), whatever the host disk has free. scripts/test-shards.ts made every shard's TMPDIR under it, so shards running side by side put their sqlite databases, journals, WAL files, repo clones and sockets there. SQLite reports a full tmpfs as database or disk is full, which matches the 384 GB-free puzzle. The tail of the shard then fails as (unnamed) hook errors behind it. I read this from the code and did not reproduce it.

What changed

  • Shard temp space: shard temp and data directories now live under FORGE_APP_DATA, a bound directory on disk, when the cell sets one (shardBase in src/checks/disk_full.ts). Otherwise they stay under the system temp directory.
  • Environment verdict: a log containing database or disk is full, SQLITE_FULL, ENOSPC or No space left on device now gets one summary line. It starts "the check's temp space ran out", names the first failing test after the error, and says it is the environment's fault. The (unnamed) fallout is not listed (src/checks/summary.ts).
  • Not charged as a try: judgeFailedCheck (src/crew/dispatch.ts) returns cause: "environment" for that summary before asking the decider, so the existing once-per-commit environment re-run applies. That re-run does not spend the agent's tries.
  • Tests: test/disk-full-shard.test.ts covers the summary and shardBase. A new case in test/ledger.test.ts checks that the judgement is environment even when the decider would say stop.

Not done

  • The "run under load" proof is left to the forge's check, because the cell's tmpfs can't be reproduced in my sandbox.
  • I did not enlarge the tmpfs, since it counts against the cell's memory.
  • For the first-failure item, I only made the disk-full summary name the first failure. In other cases parseBunTestFailure already lists failures in output order, so I left it unchanged. Shards are written to the log in the order they finish, so the first name in a multi-shard summary may not be the earliest failure in time.

Closes #8679

commented

A check shard that runs out of temp space now goes red as the environment's fault, and shards keep their temp on the cell's disk. I haven't run it under load, so "the shard no longer hits it" is unproven until the forge's check runs. The gate script, tsc, test/disk-full-shard.test.ts, test/shards.test.ts, test/shards-run.test.ts and test/ledger.test.ts all pass here.

The limit. Each check cell mounts its own /tmp as a 256 MiB tmpfs (TMP_MIB, src/apps/contain.ts:359, mounted at line 484), whatever the host disk has free. scripts/test-shards.ts made every shard's TMPDIR under it, so shards running side by side put their sqlite databases, journals, WAL files, repo clones and sockets there. SQLite reports a full tmpfs as database or disk is full, which matches the 384 GB-free puzzle. The tail of the shard then fails as (unnamed) hook errors behind it. I read this from the code and did not reproduce it.

What changed

  • Shard temp space: shard temp and data directories now live under FORGE_APP_DATA, a bound directory on disk, when the cell sets one (shardBase in src/checks/disk_full.ts). Otherwise they stay under the system temp directory.
  • Environment verdict: a log containing database or disk is full, SQLITE_FULL, ENOSPC or No space left on device now gets one summary line. It starts "the check's temp space ran out", names the first failing test after the error, and says it is the environment's fault. The (unnamed) fallout is not listed (src/checks/summary.ts).
  • Not charged as a try: judgeFailedCheck (src/crew/dispatch.ts) returns cause: "environment" for that summary before asking the decider, so the existing once-per-commit environment re-run applies. That re-run does not spend the agent's tries.
  • Tests: test/disk-full-shard.test.ts covers the summary and shardBase. A new case in test/ledger.test.ts checks that the judgement is environment even when the decider would say stop.

Not done

  • The "run under load" proof is left to the forge's check, because the cell's tmpfs can't be reproduced in my sandbox.
  • I did not enlarge the tmpfs, since it counts against the cell's memory.
  • For the first-failure item, I only made the disk-full summary name the first failure. In other cases parseBunTestFailure already lists failures in output order, so I left it unchanged. Shards are written to the log in the order they finish, so the first name in a multi-shard summary may not be the earliest failure in time.
commented

I merged main into the branch and pushed it. The only conflict was in src/crew/dispatch.ts, where both sides had added an import at the top. I kept both: OUT_OF_ROOM from my change and freshPullFor from main.

bun x tsc --noEmit is clean, and test/disk-full-shard.test.ts and test/ledger.test.ts pass. I did not run the rest of the suite here; the forge's check will run it on this push.

commented

The first fix did nothing inside the check cell, so the shards were still filling its 256 MiB /tmp. I've made a second fix and pushed it. It is untested under load, so the forge's check on this push is the first real test of it.

Why the check failed. The check cell sets no FORGE_APP_DATA, and only its checkout and its home are writable on disk. shardBase therefore fell back to tmpdir(), which is the tmpfs. Four shards ran out of room mid-run (44 failures this time, 92 on the earlier attempt at the same commit), and the later beforeAlls died as (unnamed) with undefined is not an object. The failed shards' own output isn't in the log the API returns. So "ran out of room" is my inference from those symptoms and the 17% disk-free the run recorded, not something I read in the log.

What changed

  • src/checks/service.ts now passes FORGE_CHECK_SCRATCH (the cell's bound home) to the check when it runs in a cell.
  • shardBase in src/checks/disk_full.ts prefers FORGE_CHECK_SCRATCH, then FORGE_APP_DATA, then the system temp directory.
  • scripts/test-shards.ts creates that base directory before making each shard's directory.
  • test/disk-full-shard.test.ts covers the new precedence.

Checks run. The gate script and tsc are clean. test/disk-full-shard.test.ts, test/shards.test.ts and test/shards-run.test.ts pass (85 tests).

What I left alone. The "ran out of room" verdict in src/checks/summary.ts is unchanged. The failed shards' output didn't reach the log, so that verdict never fired on this run.

Changes requested — before the latest push

Direction fit: pipeline flake fix, in scope. No UI change, so no phone/keyboard findings; no new README/TUI surface needed. Changes needed, all in one round:

  1. The verdict is too broad (src/checks/disk_full.ts FULL). Any line in the log matching ENOSPC or "No space left on device" turns the whole failure into "environment, retry free", and it replaces the real parsed failures. A pull whose own tests print those strings (this pull's test file does, and bun echoes source/error text) is retried without spending tries and its genuine failures are hidden. Require the message to come from a SQLiteError/error line, not any line, and keep the real failure list in the summary when other non-fallout tests failed.
  2. Coupling by string: dispatch.ts decides cause by run.summary.startsWith(OUT_OF_ROOM). Store the cause structurally (or at least test the real summariseCheckFailure output flowing into judgeFailedCheck); the ledger test fabricates the summary by hand, so it passes even if summary.ts stops producing it.
  3. No test covers the actual fix: service.ts passing FORGE_CHECK_SCRATCH only in a cell, and runShard using shardBase/mkdirSync. Revert either line and nothing fails. Add a test on each (the env builder, and runShard base).
  4. Unbounded retry: confirm the environment re-run is once per commit as claimed; add a test that a second full-disk failure on the same commit is not retried forever.
  5. The author says the cause is inferred, not read from a log. Prefer a cheap guard that proves it: have the check log free space of the scratch and /tmp when a shard fails, so the next red names its cause.

Fix 1 first.

commented

I've addressed all five points and pushed. I haven't seen a check run on this push yet. Beyond the checks below, I ran 214 tests across the files that touch check summaries and details, and they pass.

I ran bun scripts/gate.ts, tsc, design-lint and test/disk-full-shard.test.ts, test/shards-run.test.ts, test/shards.test.ts and test/ledger.test.ts. scripts/fast.ts couldn't name a smaller set, because scripts/test-shards.ts changed.

  1. Verdict too broad. fullShard now needs an error line (error: … or SomethingError: …) that carries SQLITE_FULL, ENOSPC, database or disk is full or No space left on device. A test that only prints those words no longer counts. It also needs at most one distinct named failing test besides the (unnamed) hooks that fall over behind it. With two or more named failures the verdict is off and the real list stays. The summary always keeps the parsed failure list; when the verdict does apply, it only gets a prefix.
  2. Coupling by string. The cause now sits on the run's detail as CheckDetail.environment = "temp_full". judgeFailedCheck reads that field, not the summary text. The ledger test now feeds the real summariseCheckFailure output through finishRun and judgeFailedCheck.
  3. The actual fix.

- scratchEnv and shardHome are extracted into src/checks/disk_full.ts and unit-tested, including that the scratch directory need not exist yet. - A new end-to-end test in test/shards-run.test.ts runs scripts/test-shards.ts with FORGE_CHECK_SCRATCH set and asserts the shard's TMPDIR is under it. - Nothing yet tests that service.ts actually calls scratchEnv; reverting that call line would still pass. I'd need a cell-level check test to cover it, which I judged too heavy here.

  1. Retry once per commit. The ledger test now runs three full-temp reds on one commit through answerFailedCheck. The answers are reran, reworked, reworked.
  2. Prove the cause. A red shard now logs a line like test-shards: space after a red shard: scratch <dir>: N MiB free of M MiB; tmp <dir>: …, so the next red names its cause.

The named-failure cap means a tmpfs that fills badly enough to fail several named tests is no longer marked as the environment's fault. That is a deliberate trade for point 1. The scratch move is what should stop the filling.

Approved

All five earlier asks are met. (1) The verdict is narrowed: fullShard needs an error line carrying SQLITE_FULL/ENOSPC, and at most one named failure besides the (unnamed) hooks; the test covers a test that merely prints the words and the two-named-failures case. (2) The cause is a typed field, CheckDetail.environment, not summary text, and the ledger test feeds the real summariseCheckFailure output through judgeFailedCheck. (3) The scratch move is extracted (scratchEnv, shardHome), unit-tested, and run end to end through test-shards.ts. (4) Retry once per commit is tested: reran, reworked, reworked. (5) A red shard logs the free space of each place it writes, so the next red names its cause. No dependency, no README addition, and this is infrastructure with no page or terminal screen, so the both-doors rule does not apply.

Two things I would do next, both Non-blocking:

  1. Non-blocking: nothing tests that src/checks/service.ts calls scratchEnv; deleting that spread line would leave every test green and put the shards back on the 256 MiB tmpfs. A small test that extracts the cell env builder, or asserts on the env handed to the cell, would close it. Whether the fix works under load is shown only by the forge check on this push.
  2. Non-blocking: the two-named-failure cap means a tmpfs that fills badly enough to fail several named tests is not marked as the environment fault. If the new space line shows that happening in practice, raise the cap or key on the count of SQLITE_FULL lines instead.
commented

Left #8679 open: 4 unticked items remain (The limit is named in the pull, with what fills it during a run.; The shard no longer hits it, and a run under load shows it.; When a shard does run out, the check says so as the environment's fault, once, and the failure is not charged as a try on the pull (CheckJudgement.cause).; …).

Approved

Earlier asks, all met in the diff since 1c655facc73e. The verdict now needs an error line, not just the words. It is off when two or more named tests fail. The cause rides on CheckDetail.environment, and judgeFailedCheck reads that field, not the summary text. The ledger test feeds real summariseCheckFailure output through finishRun and judgeFailedCheck. The once-per-commit re-run is tested (reran, reworked, reworked). A red shard logs its free space. shardHome and scratchEnv are unit-tested, and an end-to-end test shows TMPDIR under FORGE_CHECK_SCRATCH. No README change, no new dependency.

Two things I would do next, both non-blocking.

  1. Non-blocking: nothing fails if the scratchEnv(...) call in src/checks/service.ts is removed. That call is the actual fix. Add a small test that the env the service builds for a cell carries FORGE_CHECK_SCRATCH.
  2. Non-blocking: once a real red shard has logged the new space line, check it against the cell numbers. That will confirm the 256 MiB tmpfs cause, which is still read from the code and not seen.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs