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

The restore drill's budget follows the snapshot's size #2773

Merged opened this

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

Merged

The restore drill's budget is now 10 minutes plus 1 second per MB of snapshot, up to 45 minutes. Every get and its retries inside the drill now has to finish within whatever is left of that budget.

shipped production is serving it deploy c1f25a9

opened this

The restore drill's budget is now 10 minutes plus 1 second per MB of snapshot, up to 45 minutes. Every get and its retries inside the drill now has to finish within whatever is left of that budget.

Budget (src/snapshot/drill.ts): drillBudgetMs used to give 10 min + 60 s per GiB, capped at 45 min. It now gives 10 min + 1 s per MB (10^6 bytes), with the same 45 min ceiling. So 400 MB gets 16 min 40 s, and anything from 2.1 GB up gets 45 min. The issue asked for the per-MB rate to come from what d1 measured, but no measured numbers were available. The rate assumes 8 Mbit/s for download, decompress and clone together, about a nineteenth of the forge's shaped 150 Mbit/s downlink. The comment there gives this reasoning.

Gets fit inside the budget (src/snapshot/store.ts): the Store interface gains an optional bounded(leftMs), and withRetries implements it.

  • Each attempt's deadline is cut to an even share of what's left after the waits between retries still to come, and no attempt starts once nothing is left. So deadlines × attempts plus waits never exceed what remains.
  • runDrill hands this bounded store to both the snapshot listing and the restore. Before, a stalled store could keep five 180 s attempts running after the drill had already gone red.
  • A directory store doesn't retry, so it has no bounded and is used as it is.

Card (src/snapshot/verdict.ts, src/web/pages/backups.ts): the red card now ends "It had 16.7 min." The Backups line shows the time spent against the budget in minutes even when no restore phases were recorded. Drills recorded before budgets were kept say nothing about a budget.

Tests:

  • test/drill-budget-size.test.ts runs at 1000× speed. A 400 MB snapshot through a store throttled to 1 MB/s, retried the way the S3 store is, passes inside its budget. A store that stalls at the 31st object fails within the budget, and the error names the object it stalled on. With the bound removed, the stall test fails: the get gave up at 1179 ms against a 1000 ms budget.
  • test/snapshot-store.test.ts: a stalled get with 300 ms left fails within 300 ms, not after 5 × 10 s. With nothing left, no attempt is made.
  • Card and page wording are covered in test/drill-slow.test.ts and test/snapshot-drill-phases.test.ts.
  • In test/snapshot-drill.test.ts, the budget tests had hard-coded the old per-GiB numbers. I changed those expected numbers to the new rule; no code was wrong there.

The type check is clean and the drill, restore and store suites pass locally. I did not run the full check. The forge runs it on the push, and any failure there in an unrelated test such as following across forges > the whole path is not caused by this change.

I left the per-get deadline that follows object size to #2670, which is still open. This change only bounds the gets from above by the drill's time; docs/RECOVERY.md and the README describe the new budget.

To try it: bun test test/drill-budget-size.test.ts test/snapshot-store.test.ts test/snapshot-drill.test.ts

🤖 Generated with Claude Code

Closes #2744

commented

The restore drill's budget is now 10 minutes plus 1 second per MB of snapshot, up to 45 minutes. Every get and its retries inside the drill now has to finish within whatever is left of that budget.

Budget (src/snapshot/drill.ts): drillBudgetMs used to give 10 min + 60 s per GiB, capped at 45 min. It now gives 10 min + 1 s per MB (10^6 bytes), with the same 45 min ceiling. So 400 MB gets 16 min 40 s, and anything from 2.1 GB up gets 45 min. The issue asked for the per-MB rate to come from what d1 measured, but no measured numbers were available. The rate assumes 8 Mbit/s for download, decompress and clone together, about a nineteenth of the forge's shaped 150 Mbit/s downlink. The comment there gives this reasoning.

Gets fit inside the budget (src/snapshot/store.ts): the Store interface gains an optional bounded(leftMs), and withRetries implements it.

  • Each attempt's deadline is cut to an even share of what's left after the waits between retries still to come, and no attempt starts once nothing is left. So deadlines × attempts plus waits never exceed what remains.
  • runDrill hands this bounded store to both the snapshot listing and the restore. Before, a stalled store could keep five 180 s attempts running after the drill had already gone red.
  • A directory store doesn't retry, so it has no bounded and is used as it is.

Card (src/snapshot/verdict.ts, src/web/pages/backups.ts): the red card now ends "It had 16.7 min." The Backups line shows the time spent against the budget in minutes even when no restore phases were recorded. Drills recorded before budgets were kept say nothing about a budget.

Tests:

  • test/drill-budget-size.test.ts runs at 1000× speed. A 400 MB snapshot through a store throttled to 1 MB/s, retried the way the S3 store is, passes inside its budget. A store that stalls at the 31st object fails within the budget, and the error names the object it stalled on. With the bound removed, the stall test fails: the get gave up at 1179 ms against a 1000 ms budget.
  • test/snapshot-store.test.ts: a stalled get with 300 ms left fails within 300 ms, not after 5 × 10 s. With nothing left, no attempt is made.
  • Card and page wording are covered in test/drill-slow.test.ts and test/snapshot-drill-phases.test.ts.
  • In test/snapshot-drill.test.ts, the budget tests had hard-coded the old per-GiB numbers. I changed those expected numbers to the new rule; no code was wrong there.

The type check is clean and the drill, restore and store suites pass locally. I did not run the full check. The forge runs it on the push, and any failure there in an unrelated test such as following across forges > the whole path is not caused by this change.

I left the per-get deadline that follows object size to #2670, which is still open. This change only bounds the gets from above by the drill's time; docs/RECOVERY.md and the README describe the new budget.

To try it: bun test test/drill-budget-size.test.ts test/snapshot-store.test.ts test/snapshot-drill.test.ts

🤖 Generated with Claude Code

Approved

The budget is now 10 min + 1 s per MB (cap 45 min), and the drill hands the restore a store whose gets and retries stay inside what is left. I read the diff only and did not try the preview: the Backups page sits behind sign-in, so I never saw the new budget line. No new dependency, no security surface touched, and no terminal change is needed because the terminal has no backups view (the README says so). The tests would fail without the change: the 1000x-speed stall test, the store-level bound, and the card and page wording. All the budget tests were updated to the new rule.

Two things I would do next:

  1. Replace the assumed 8 Mbit/s rate with the number d1 measured. The issue asked for the measured rate, and the comment in drill.ts admits it is a guess.
  2. Loosen the wall-clock margins in test/drill-budget-size.test.ts (the 40 ms and 5 ms slack) or drive them from a fake clock. They can flake on a loaded check machine.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs