Public: anyone can read this; the crew can change it.
Vitals raises one need per domain whose last drill is red or eight days old, and the need says the exact command that runs the drill #2196
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2175-vitals-raises-one-need-per-domain-whose → main 1 commit fork
9 files
changed +206
−3
src/actions/health.ts +1
| @@ -30,6 +30,7 @@ export function readHealth( | ||
| 30 | 30 | dataDir: app.config.dataDir, |
| 31 | 31 | envs, |
| 32 | 32 | signals: app.signals, |
| 33 | forgeUrl: app.config.url ?? `http://localhost:${app.config.port}`, | |
| 33 | 34 | }); |
| 34 | 35 | return { ...v, ...forge, ok: forge.ok && v.ok }; |
| 35 | 36 | } |
src/core/domain_drills.ts +13 −1
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | import type { Database } from "bun:sqlite"; |
| 4 | 4 | import { id } from "../kernel/id.ts"; |
| 5 | import type { Observed, Verdict } from "../kernel/domain_drill.ts"; | |
| 5 | import type { NewestDrill, Observed, Verdict } from "../kernel/domain_drill.ts"; | |
| 6 | 6 | |
| 7 | 7 | export interface DrillRecord extends Verdict { |
| 8 | 8 | readonly id: string; |
| @@ -43,3 +43,15 @@ export function lastDrill(db: Database, domain: string): LastDrill | null { | ||
| 43 | 43 | .get(domain); |
| 44 | 44 | return r === null ? null : { green: r.green === 1, why: r.why, backSeconds: r.back_seconds, at: r.at }; |
| 45 | 45 | } |
| 46 | ||
| 47 | /** The newest drill of every domain, in one query. */ | |
| 48 | export function newestDrills(db: Database): NewestDrill[] { | |
| 49 | return db | |
| 50 | .query<{ domain: string; green: number; why: string; at: string }, []>( | |
| 51 | `SELECT d.domain, d.green, d.why, d.at FROM domain_drill d | |
| 52 | WHERE d.rowid = (SELECT x.rowid FROM domain_drill x WHERE x.domain = d.domain | |
| 53 | ORDER BY x.at DESC, x.rowid DESC LIMIT 1)`, | |
| 54 | ) | |
| 55 | .all() | |
| 56 | .map((r) => ({ domain: r.domain, green: r.green === 1, why: r.why, at: r.at })); | |
| 57 | } |
src/kernel/domain_drill.ts +86
| @@ -37,3 +37,89 @@ export function drillVerdict( | ||
| 37 | 37 | } |
| 38 | 38 | return { green: true, why: `every phrase seen; back in ${backSeconds}s of ${limit}s` }; |
| 39 | 39 | } |
| 40 | ||
| 41 | /** A domain drill with no green in this long, or whose newest is red, is a need. */ | |
| 42 | export const DOMAIN_DRILL_STALE_MS = 8 * 24 * 3_600_000; | |
| 43 | ||
| 44 | /** The newest drill kept for one domain. */ | |
| 45 | export interface NewestDrill { | |
| 46 | readonly domain: string; | |
| 47 | readonly green: boolean; | |
| 48 | readonly why: string; | |
| 49 | readonly at: string; | |
| 50 | } | |
| 51 | ||
| 52 | export interface DomainDrillNeed { | |
| 53 | /** `drill:<domain-key>`, or `drill:never` for every domain not yet drilled. */ | |
| 54 | readonly cause: string; | |
| 55 | readonly sentence: string; | |
| 56 | readonly since: string; | |
| 57 | } | |
| 58 | ||
| 59 | /** A word safe in a shell as it is; anything else is single-quoted. */ | |
| 60 | function shellWord(s: string): string { | |
| 61 | return /^[A-Za-z0-9_.:\/@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`; | |
| 62 | } | |
| 63 | ||
| 64 | /** The exact command that runs one domain's drill against this forge. */ | |
| 65 | export function drillCommand(key: string, forgeUrl: string): string { | |
| 66 | return `bun scripts/drills/run.ts ${shellWord(key)} --forge ${shellWord(forgeUrl)}`; | |
| 67 | } | |
| 68 | ||
| 69 | const WHY_MAX = 160; | |
| 70 | ||
| 71 | /** The failing phrase: the quoted words of the verdict, else the verdict itself. */ | |
| 72 | function failing(why: string): string { | |
| 73 | const quoted = /"([^"]+)"/.exec(why); | |
| 74 | const text = (quoted ? quoted[1]! : why).replace(/[\r\n]+/g, " "); | |
| 75 | return text.length > WHY_MAX ? `${text.slice(0, WHY_MAX)}…` : text; | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * One need per drillable domain whose newest drill is red, or whose newest | |
| 80 | * green is over eight days old; and one for all the drillable domains never | |
| 81 | * drilled, once the forge itself (`forgeSince`) is over eight days old. A | |
| 82 | * newer green drill is the newest, so it clears its domain. | |
| 83 | */ | |
| 84 | export function domainDrillNeeds( | |
| 85 | newest: readonly NewestDrill[], | |
| 86 | now: number, | |
| 87 | forgeSince: string | undefined, | |
| 88 | forgeUrl: string, | |
| 89 | domains: readonly Domain[], | |
| 90 | ): DomainDrillNeed[] { | |
| 91 | const byDomain = new Map(newest.map((d) => [d.domain, d])); | |
| 92 | const days = Math.floor(DOMAIN_DRILL_STALE_MS / 86_400_000); | |
| 93 | const needs: DomainDrillNeed[] = []; | |
| 94 | const never: Domain[] = []; | |
| 95 | for (const d of domains) { | |
| 96 | if (d.drill.name === null) continue; | |
| 97 | const last = byDomain.get(d.key); | |
| 98 | if (last === undefined) { | |
| 99 | never.push(d); | |
| 100 | continue; | |
| 101 | } | |
| 102 | const at = Date.parse(last.at); | |
| 103 | if (!last.green) { | |
| 104 | needs.push({ | |
| 105 | cause: `drill:${d.key}`, | |
| 106 | sentence: `The ${d.key} drill failed: ${failing(last.why)}. Run it again: ${drillCommand(d.key, forgeUrl)}`, | |
| 107 | since: last.at, | |
| 108 | }); | |
| 109 | } else if (now - at > DOMAIN_DRILL_STALE_MS) { | |
| 110 | needs.push({ | |
| 111 | cause: `drill:${d.key}`, | |
| 112 | sentence: `No ${d.key} drill in ${days} days; the last one passed on ${last.at.slice(0, 10)}. Run it: ${drillCommand(d.key, forgeUrl)}`, | |
| 113 | since: new Date(at + DOMAIN_DRILL_STALE_MS).toISOString(), | |
| 114 | }); | |
| 115 | } | |
| 116 | } | |
| 117 | const began = Date.parse(forgeSince ?? ""); | |
| 118 | if (never.length > 0 && now - began > DOMAIN_DRILL_STALE_MS) | |
| 119 | needs.push({ | |
| 120 | cause: "drill:never", | |
| 121 | sentence: `No drill has ever run for ${never.map((d) => d.key).join(", ")}. Run each: ${never.map((d) => drillCommand(d.key, forgeUrl)).join("; ")}`, | |
| 122 | since: new Date(began + DOMAIN_DRILL_STALE_MS).toISOString(), | |
| 123 | }); | |
| 124 | return needs; | |
| 125 | } |
src/kernel/vitals.ts +15
| @@ -23,6 +23,9 @@ import { STALE_AFTER_MS } from "../runs/lease.ts"; | ||
| 23 | 23 | import { BUDGETS } from "./budgets.ts"; |
| 24 | 24 | import { lastMirror, mirrorNeed } from "../snapshot/mirror.ts"; |
| 25 | 25 | import { drillNeed, firstSeen as drillFirstSeen, lastVerdict } from "../snapshot/verdict.ts"; |
| 26 | import { newestDrills } from "../core/domain_drills.ts"; | |
| 27 | import { DOMAINS } from "./domains.ts"; | |
| 28 | import { domainDrillNeeds } from "./domain_drill.ts"; | |
| 26 | 29 | import { heldChecks, redSentence } from "../checks/red_main.ts"; |
| 27 | 30 | |
| 28 | 31 | /** Free disk under this share of the volume is something to say. */ |
| @@ -95,6 +98,8 @@ export interface VitalsInput { | ||
| 95 | 98 | /** Every environment the runtime holds, when one is up. */ |
| 96 | 99 | readonly envs?: readonly EnvCount[]; |
| 97 | 100 | readonly now?: number; |
| 101 | /** The address people reach this forge at, named in the drill commands. */ | |
| 102 | readonly forgeUrl?: string; | |
| 98 | 103 | /** The free-disk share below which the disk is a need; `DISK_FREE_MIN` unless a test says. */ |
| 99 | 104 | readonly diskFreeMin?: number; |
| 100 | 105 | /** One forge's own signals (`App.signals`). */ |
| @@ -319,6 +324,16 @@ export function vitals(input: VitalsInput): Vitals { | ||
| 319 | 324 | if (owed !== undefined) |
| 320 | 325 | need("drill", owed.sentence, owed.since, "/settings/backups"); |
| 321 | 326 | |
| 327 | // A domain whose drill is red or eight days old is a card with the command that runs it. | |
| 328 | for (const owed of domainDrillNeeds( | |
| 329 | newestDrills(input.db), | |
| 330 | now, | |
| 331 | drillFirstSeen(input.dataDir), | |
| 332 | input.forgeUrl ?? "http://localhost", | |
| 333 | DOMAINS, | |
| 334 | )) | |
| 335 | need(owed.cause, owed.sentence, owed.since, "/"); | |
| 336 | ||
| 322 | 337 | // A snapshot that did not land off the box, or a mirror a day behind, is a card until it does. |
| 323 | 338 | const behind = mirrorNeed(lastMirror(input.dataDir), now); |
| 324 | 339 | if (behind !== undefined) need("mirror", behind.sentence, behind.since, "/settings/backups"); |
src/main.ts +1
| @@ -446,6 +446,7 @@ const tickAsks = async (): Promise<void> => { | ||
| 446 | 446 | db: app.db, |
| 447 | 447 | dataDir: app.config.dataDir, |
| 448 | 448 | signals: app.signals, |
| 449 | forgeUrl, | |
| 449 | 450 | }).degraded, |
| 450 | 451 | Date.now(), |
| 451 | 452 | app.log, |
src/web/pages/board.ts +1
| @@ -334,6 +334,7 @@ function forgeStrip(ctx: Ctx): Html | null { | ||
| 334 | 334 | db: app.db, |
| 335 | 335 | dataDir: app.config.dataDir, |
| 336 | 336 | signals: app.signals, |
| 337 | forgeUrl: app.config.url ?? `http://localhost:${app.config.port}`, | |
| 337 | 338 | envs: runtime |
| 338 | 339 | ? listApps(app.db).flatMap((record) => runtime.status(record)) |
| 339 | 340 | : [], |
src/web/studio/asks.ts +1
| @@ -1346,6 +1346,7 @@ export function forgeNeedsFor(ctx: Ctx, user: User | undefined): Degraded[] { | ||
| 1346 | 1346 | db: ctx.app.db, |
| 1347 | 1347 | dataDir: ctx.app.config.dataDir, |
| 1348 | 1348 | signals: ctx.app.signals, |
| 1349 | forgeUrl: ctx.app.config.url ?? `http://localhost:${ctx.app.config.port}`, | |
| 1349 | 1350 | envs, |
| 1350 | 1351 | }).degraded; |
| 1351 | 1352 | } |
test/vitals-domain-drill.test.ts added +86
| @@ -0,0 +1,86 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { mkdtempSync } from "node:fs"; | |
| 3 | import { tmpdir } from "node:os"; | |
| 4 | import { join } from "node:path"; | |
| 5 | import { drillCommand, domainDrillNeeds, type NewestDrill } from "../src/kernel/domain_drill.ts"; | |
| 6 | import { DOMAINS } from "../src/kernel/domains.ts"; | |
| 7 | ||
| 8 | const NOW = Date.parse("2026-10-16T12:00:00Z"); | |
| 9 | const DAY = 86_400_000; | |
| 10 | const URL_ = "https://forge.example"; | |
| 11 | const iso = (ms: number) => new Date(ms).toISOString(); | |
| 12 | const drilled = DOMAINS.filter((d) => d.drill.name !== null).map((d) => d.key); | |
| 13 | const green = (domain: string, at: number): NewestDrill => ({ domain, green: true, why: "ok", at: iso(at) }); | |
| 14 | const needs = (newest: NewestDrill[], since = iso(NOW - 30 * DAY)) => | |
| 15 | domainDrillNeeds(newest, NOW, since, URL_, DOMAINS); | |
| 16 | ||
| 17 | test("a red drill raises its domain's need, naming the phrase and the command", () => { | |
| 18 | const rest = drilled.filter((k) => k !== "snapshots").map((k) => green(k, NOW - DAY)); | |
| 19 | const red: NewestDrill = { domain: "snapshots", green: false, why: 'the forge never said "The restore drill failed at"', at: iso(NOW - DAY) }; | |
| 20 | const n = needs([...rest, red]); | |
| 21 | expect(n.map((x) => x.cause)).toEqual(["drill:snapshots"]); | |
| 22 | expect(n[0]!.sentence).toContain('The restore drill failed at'); | |
| 23 | expect(n[0]!.sentence).toContain(`bun scripts/drills/run.ts snapshots --forge ${URL_}`); | |
| 24 | }); | |
| 25 | ||
| 26 | test("a newer green drill clears the need", () => { | |
| 27 | const all = drilled.map((k) => green(k, NOW - DAY)); | |
| 28 | expect(needs(all)).toEqual([]); | |
| 29 | }); | |
| 30 | ||
| 31 | test("eight days is the edge: exactly eight is quiet, a millisecond over speaks", () => { | |
| 32 | const rest = drilled.slice(1).map((k) => green(k, NOW - DAY)); | |
| 33 | const key = drilled[0]!; | |
| 34 | expect(needs([green(key, NOW - 8 * DAY), ...rest])).toEqual([]); | |
| 35 | const n = needs([green(key, NOW - 8 * DAY - 1), ...rest]); | |
| 36 | expect(n.map((x) => x.cause)).toEqual([`drill:${key}`]); | |
| 37 | }); | |
| 38 | ||
| 39 | test("domains never drilled are one need, once the forge is over eight days old", () => { | |
| 40 | const n = needs([]); | |
| 41 | expect(n.length).toBe(1); | |
| 42 | expect(n[0]!.cause).toBe("drill:never"); | |
| 43 | for (const k of drilled) expect(n[0]!.sentence).toContain(drillCommand(k, URL_)); | |
| 44 | expect(needs([], iso(NOW - 8 * DAY))).toEqual([]); | |
| 45 | expect(needs([], iso(NOW - 8 * DAY - 1)).length).toBe(1); | |
| 46 | expect(domainDrillNeeds([], NOW, undefined, URL_, DOMAINS)).toEqual([]); | |
| 47 | }); | |
| 48 | ||
| 49 | test("domains that cannot be drilled are never asked for", () => { | |
| 50 | expect(needs([]).map((x) => x.sentence).join(" ")).not.toContain("scripts/drills/run.ts edge"); | |
| 51 | }); | |
| 52 | ||
| 53 | test("the command is the real key and URL, shell-escaped", () => { | |
| 54 | expect(drillCommand("compute", "https://f.example:8443")).toBe( | |
| 55 | "bun scripts/drills/run.ts compute --forge https://f.example:8443", | |
| 56 | ); | |
| 57 | expect(drillCommand("a b", "http://x/?q=1&r='2'")).toBe( | |
| 58 | `bun scripts/drills/run.ts 'a b' --forge 'http://x/?q=1&r='\\''2'\\'''`, | |
| 59 | ); | |
| 60 | expect(drillCommand("x; rm -rf /", URL_)).toContain("'x; rm -rf /'"); | |
| 61 | }); | |
| 62 | ||
| 63 | test("a verdict that is not a quoted phrase is quoted whole, and bounded", () => { | |
| 64 | const rest = drilled.filter((k) => k !== "runner").map((k) => green(k, NOW - DAY)); | |
| 65 | const why = "back after 900s, " + "x".repeat(500); | |
| 66 | const n = needs([...rest, { domain: "runner", green: false, why, at: iso(NOW) }]); | |
| 67 | expect(n[0]!.sentence).toContain("back after 900s"); | |
| 68 | expect(n[0]!.sentence.length).toBeLessThan(400); | |
| 69 | }); | |
| 70 | ||
| 71 | test("vitals raises them from the newest drill of each domain, in one query", async () => { | |
| 72 | const { vitals } = await import("../src/kernel/vitals.ts"); | |
| 73 | const { newestDrills, recordDrill } = await import("../src/core/domain_drills.ts"); | |
| 74 | const { createApp } = await import("../src/app.ts"); | |
| 75 | const dir = mkdtempSync(join(tmpdir(), "vdd-")); | |
| 76 | const app = createApp({ dataDir: dir, port: 0 }); | |
| 77 | const seen = { health: "", board: "" }; | |
| 78 | recordDrill(app.db, "u", "compute", seen, 1, { green: false, why: 'the forge never said "x"' }); | |
| 79 | const sentence = () => | |
| 80 | vitals({ db: app.db, signals: app.signals, dataDir: dir, forgeUrl: URL_ }).needs.find((s) => s.includes("compute drill")); | |
| 81 | expect(sentence()).toContain(`bun scripts/drills/run.ts compute --forge ${URL_}`); | |
| 82 | app.db.query("UPDATE domain_drill SET at = '2020-01-01T00:00:00.000Z'").run(); | |
| 83 | recordDrill(app.db, "u", "compute", seen, 1, { green: true, why: "ok" }); | |
| 84 | expect(sentence()).toBeUndefined(); | |
| 85 | expect(newestDrills(app.db).length).toBe(1); | |
| 86 | }); |
test/vitals.test.ts +2 −2
| @@ -154,8 +154,8 @@ test("the number of queries does not grow with runs", () => { | ||
| 154 | 154 | if (i % 2) app.db.query("UPDATE run SET state='running' WHERE id=?1").run(r.id); |
| 155 | 155 | } |
| 156 | 156 | expect(count()).toBe(before); |
| 157 | // The queue, the running runs, the runs killed twice for their budget, the lane's depth and finished checks, and the red mains holding pulls' checks. | |
| 158 | expect(before).toBe(6); | |
| 157 | // The queue, the running runs, the runs killed twice for their budget, the lane's depth and finished checks, and the red mains holding pulls' checks. and the newest drill of every domain. | |
| 158 | expect(before).toBe(7); | |
| 159 | 159 | }); |
| 160 | 160 | |
| 161 | 161 | test("errors are counted, bounded, redacted, and a burst is said", async () => { |
new above
offline