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

Merged opened this

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

Merged

scout/forge:scout/12168-the-restore-drill-need-is-not-raised-unt → main 2 commits fork

  • Merged. scout/12168-the-restore-drill-need-is-not-raised-unt was merged into main as 1018b7a.

Review

Sign in to review this change.
3 files changed +85 −11
src/kernel/vitals.ts +8 −5
@@ -35,7 +35,7 @@ import { backupVerdict, isWatched } from "../snapshot/bucket.ts";
3535 import { lastDrill } from "../snapshot/drills.ts";
3636 import { lastMirror, mirrorNeed } from "../snapshot/mirror.ts";
3737 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";
3939 import { proofNeed, proofNotice } from "../release/prove.ts";
4040 import { running as runningVersion } from "./version.ts";
4141 import { rollbackNeed } from "../release/launch.ts";
@@ -562,17 +562,20 @@ export function vitals(input: VitalsInput): Vitals {
562562 const full = diskStillFull ? diskReds(input.db, now) : undefined;
563563 if (full !== undefined) need("broken", "disk-red", diskRedSentence(full), full.since, "/settings/capacity");
564564
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.
566568 const drill = lastVerdict(input.dataDir);
567 const owed = drillNeed(drill, now, () => drillFirstSeen(input.dataDir));
569 const owed = drillNeed(drill, now);
568570 if (owed !== undefined)
569571 need("todo", "drill", owed.sentence, owed.since, "/settings/backups");
570572
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.
572575 for (const owed of domainDrillNeeds(
573576 newestDrills(input.db),
574577 now,
575 drillFirstSeen(input.dataDir),
578 undefined,
576579 input.forgeUrl ?? "http://localhost",
577580 DOMAINS,
578581 ))
test/drill-board-line.test.ts +6 −6
@@ -61,13 +61,11 @@ describe("the drill's board line", () => {
6161 await rm(root, { recursive: true, force: true });
6262 });
6363
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);
6866 });
6967
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 () => {
7169 expect(line(T0 - 30 * DAY + DAY)).toHaveLength(0);
7270 const fresh = mkdtempSync(join(tmpdir(), "forge-drill-fresh-"));
7371 const input = { db: app.db, dataDir: fresh, signals: app.signals };
@@ -79,7 +77,9 @@ describe("the drill's board line", () => {
7977 seedFirstSeen(fresh, T0);
8078 seedFirstSeen(fresh, T0 + 5 * DAY);
8179 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);
8383 await rm(fresh, { recursive: true, force: true });
8484 });
8585
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs