Public: anyone can read this; the crew can change it.
The restore-drill need is not raised until the forge runs a drill by itself #12384
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/12168-the-restore-drill-need-is-not-raised-unt → main 2 commits fork
3 files
changed +85
−11
src/kernel/vitals.ts +8 −5
| @@ -35,7 +35,7 @@ import { backupVerdict, isWatched } from "../snapshot/bucket.ts"; | ||
| 35 | 35 | import { lastDrill } from "../snapshot/drills.ts"; |
| 36 | 36 | import { lastMirror, mirrorNeed } from "../snapshot/mirror.ts"; |
| 37 | 37 | import { lastSnapshotOutcome, snapshotNeed } from "../snapshot/outcome.ts"; |
| 38 | import { drillNeed, firstSeen as drillFirstSeen, lastVerdict } from "../snapshot/verdict.ts"; | |
| 38 | import { drillNeed, lastVerdict } from "../snapshot/verdict.ts"; | |
| 39 | 39 | import { proofNeed, proofNotice } from "../release/prove.ts"; |
| 40 | 40 | import { running as runningVersion } from "./version.ts"; |
| 41 | 41 | import { rollbackNeed } from "../release/launch.ts"; |
| @@ -562,17 +562,20 @@ export function vitals(input: VitalsInput): Vitals { | ||
| 562 | 562 | const full = diskStillFull ? diskReds(input.db, now) : undefined; |
| 563 | 563 | if (full !== undefined) need("broken", "disk-red", diskRedSentence(full), full.since, "/settings/capacity"); |
| 564 | 564 | |
| 565 | // A red restore drill, or none in eight days, is a card until a drill passes. | |
| 565 | // A red restore drill, or a green one eight days old, is a card until a drill passes. | |
| 566 | // A forge where no drill has ever run is no card (#12168): it cannot meet the need by | |
| 567 | // itself, so the forge's first-seen time is not passed and "never" is not raised. | |
| 566 | 568 | const drill = lastVerdict(input.dataDir); |
| 567 | const owed = drillNeed(drill, now, () => drillFirstSeen(input.dataDir)); | |
| 569 | const owed = drillNeed(drill, now); | |
| 568 | 570 | if (owed !== undefined) |
| 569 | 571 | need("todo", "drill", owed.sentence, owed.since, "/settings/backups"); |
| 570 | 572 | |
| 571 | // A domain whose drill is red or eight days old is a card with the command that runs it. | |
| 573 | // A domain whose drill is red or eight days old is a card with the command that runs it; | |
| 574 | // domains never drilled are no card, for the same reason. | |
| 572 | 575 | for (const owed of domainDrillNeeds( |
| 573 | 576 | newestDrills(input.db), |
| 574 | 577 | now, |
| 575 | drillFirstSeen(input.dataDir), | |
| 578 | undefined, | |
| 576 | 579 | input.forgeUrl ?? "http://localhost", |
| 577 | 580 | DOMAINS, |
| 578 | 581 | )) |
test/drill-board-line.test.ts +6 −6
| @@ -61,13 +61,11 @@ describe("the drill's board line", () => { | ||
| 61 | 61 | await rm(root, { recursive: true, force: true }); |
| 62 | 62 | }); |
| 63 | 63 | |
| 64 | test("none ever recorded, on a forge older than eight days: one line", () => { | |
| 65 | const l = line(T0); | |
| 66 | expect(l).toHaveLength(1); | |
| 67 | expect(l[0]!.sentence).toContain("No restore drill"); | |
| 64 | test("none ever recorded, even on a forge older than eight days: no line (#12168)", () => { | |
| 65 | expect(line(T0)).toHaveLength(0); | |
| 68 | 66 | }); |
| 69 | 67 | |
| 70 | test("a forge younger than eight days has missed nothing", async () => { | |
| 68 | test("a forge with no drill has missed nothing, young or old", async () => { | |
| 71 | 69 | expect(line(T0 - 30 * DAY + DAY)).toHaveLength(0); |
| 72 | 70 | const fresh = mkdtempSync(join(tmpdir(), "forge-drill-fresh-")); |
| 73 | 71 | const input = { db: app.db, dataDir: fresh, signals: app.signals }; |
| @@ -79,7 +77,9 @@ describe("the drill's board line", () => { | ||
| 79 | 77 | seedFirstSeen(fresh, T0); |
| 80 | 78 | seedFirstSeen(fresh, T0 + 5 * DAY); |
| 81 | 79 | expect(flagged(T0)).toBe(false); |
| 82 | expect(flagged(T0 + 9 * DAY)).toBe(true); | |
| 80 | // Seeded and nine days old, still no drill ever: the forge cannot meet that need | |
| 81 | // by itself (#12168). | |
| 82 | expect(flagged(T0 + 9 * DAY)).toBe(false); | |
| 83 | 83 | await rm(fresh, { recursive: true, force: true }); |
| 84 | 84 | }); |
| 85 | 85 |
test/vitals-drill-never.test.ts added +71
| @@ -0,0 +1,71 @@ | ||
| 1 | /** | |
| 2 | * A forge where no drill has ever run raises no drill need (#12168): it | |
| 3 | * cannot run those drills by itself, so the need could never be met. A drill | |
| 4 | * that ran and went red is still a `todo` need. | |
| 5 | */ | |
| 6 | ||
| 7 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 8 | import { mkdirSync, writeFileSync } from "node:fs"; | |
| 9 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 10 | import { tmpdir } from "node:os"; | |
| 11 | import { join } from "node:path"; | |
| 12 | import { createApp, type App } from "../src/app.ts"; | |
| 13 | import { recordDrill } from "../src/core/domain_drills.ts"; | |
| 14 | import { vitals } from "../src/kernel/vitals.ts"; | |
| 15 | import { recordVerdict } from "../src/snapshot/verdict.ts"; | |
| 16 | ||
| 17 | const DAY = 86_400_000; | |
| 18 | const roomy = () => ({ freeBytes: 95, totalBytes: 100 }); | |
| 19 | ||
| 20 | let dataDir: string; | |
| 21 | let app: App; | |
| 22 | ||
| 23 | beforeAll(async () => { | |
| 24 | dataDir = await mkdtemp(join(tmpdir(), "forge-drill-never-")); | |
| 25 | app = createApp({ dataDir, port: 0, disk: roomy }); | |
| 26 | // The forge was first seen a month ago: long past the eight days that used to raise "never". | |
| 27 | mkdirSync(join(dataDir, "drill"), { recursive: true }); | |
| 28 | writeFileSync(join(dataDir, "drill", "first-seen"), new Date(Date.now() - 30 * DAY).toISOString()); | |
| 29 | }); | |
| 30 | ||
| 31 | afterAll(async () => { | |
| 32 | app.close(); | |
| 33 | await rm(dataDir, { recursive: true, force: true }); | |
| 34 | }); | |
| 35 | ||
| 36 | const drillCauses = () => | |
| 37 | vitals({ db: app.db, signals: app.signals, dataDir, disk: roomy }).degraded.filter( | |
| 38 | (d) => d.cause === "drill" || d.cause.startsWith("drill:"), | |
| 39 | ); | |
| 40 | ||
| 41 | test("a month-old data dir with no drill raises no drill need", () => { | |
| 42 | expect(drillCauses()).toEqual([]); | |
| 43 | const v = vitals({ db: app.db, signals: app.signals, dataDir, disk: roomy }); | |
| 44 | expect(v.needs.some((n) => n.includes("No drill has ever run"))).toBe(false); | |
| 45 | expect(v.needs.some((n) => n.includes("none has ever been recorded"))).toBe(false); | |
| 46 | }); | |
| 47 | ||
| 48 | test("a red restore drill and a red domain drill each raise a todo need", () => { | |
| 49 | const at = new Date(Date.now() - DAY).toISOString(); | |
| 50 | recordVerdict(dataDir, { | |
| 51 | at, | |
| 52 | snapshotId: null, | |
| 53 | ms: 1000, | |
| 54 | counts: { repositories: 0, issues: 0, users: 0, pulls: 0, apps: 0 }, | |
| 55 | health: "unreached", | |
| 56 | verdict: "red", | |
| 57 | failedStep: "fetch", | |
| 58 | phases: [], | |
| 59 | budgetMs: 0, | |
| 60 | reason: "no snapshot", | |
| 61 | }); | |
| 62 | recordDrill(app.db, "alice", "compute", { health: "", board: "" }, 10, { green: false, why: 'the forge never said "down"' }); | |
| 63 | ||
| 64 | const v = vitals({ db: app.db, signals: app.signals, dataDir, disk: roomy }); | |
| 65 | const causes = drillCauses().map((d) => d.cause).sort(); | |
| 66 | expect(causes).toEqual(["drill", "drill:compute"]); | |
| 67 | for (const d of drillCauses()) expect(v.todo).toContain(d.sentence); | |
| 68 | expect(v.broken.some((s) => drillCauses().some((d) => d.sentence === s))).toBe(false); | |
| 69 | // The domains still never drilled stay quiet. | |
| 70 | expect(v.needs.some((n) => n.includes("No drill has ever run"))).toBe(false); | |
| 71 | }); |
new above
offline