Public: anyone can read this; the crew can change it.
A red shared by many pulls never parks them: one test failing on 3+ pulls in an hour is the lane's, not theirs #2270
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/2266-a-red-shared-by-many-pulls-never-parks-t → main 2 commits fork
8 files
changed +445
−15
src/crew/dispatch.ts +16 −8
| @@ -84,6 +84,7 @@ import { | ||
| 84 | 84 | } from "./autonomy.ts"; |
| 85 | 85 | import { checksFor } from "../checks/service.ts"; |
| 86 | 86 | import { redMain, sharedWithMain } from "../checks/red_main.ts"; |
| 87 | import { sharedWithPulls } from "../checks/shared_red.ts"; | |
| 87 | 88 | import { certain } from "./decide.ts"; |
| 88 | 89 | import { readRunLog } from "../runs/log.ts"; |
| 89 | 90 | import { whenText } from "./limits.ts"; |
| @@ -588,8 +589,9 @@ function route(app: App, event: ForgeEvent): void { | ||
| 588 | 589 | // is the agent's to look at. |
| 589 | 590 | const agent = agentFor(app, issue); |
| 590 | 591 | if (!agent) return; |
| 591 | // A red that main shares is not the pull's: no rework, no park, no | |
| 592 | // strike; it waits and is checked again when main is green (#1971). | |
| 592 | // A red that main shares (#1971), or that three pulls share within | |
| 593 | // the hour (#2266), is not the pull's: no rework, no park, no | |
| 594 | // strike; it waits and is checked again when main is green. | |
| 593 | 595 | if (waitOnMain(app, repo, issue, event.payload)) return; |
| 594 | 596 | // Bounded: after a few failed checks in a row the agent stops and a |
| 595 | 597 | // person must speak (docs/APPS.md, rule 7). |
| @@ -674,13 +676,14 @@ function route(app: App, event: ForgeEvent): void { | ||
| 674 | 676 | } |
| 675 | 677 | |
| 676 | 678 | /** |
| 677 | * Pulls whose check failed only on what main fails, per repository, with | |
| 679 | * Pulls whose check failed only on what main fails, or on what the lane | |
| 680 | * fails across pulls (`checks/shared_red.ts`), per repository, with | |
| 678 | 681 | * the head that was checked. Memory only: a restart forgets them, and the |
| 679 | 682 | * pull's next push or a person's ask checks it again. |
| 680 | 683 | */ |
| 681 | 684 | const waitingOnMain = new Map<string, Map<string, { env: string; commitOid: string }>>(); |
| 682 | 685 | |
| 683 | /** Whether a failed check is main's red, shared: if so, the pull is noted as waiting. */ | |
| 686 | /** Whether a failed check is main's red or the lane's, shared: if so, the pull is noted as waiting. */ | |
| 684 | 687 | function waitOnMain( |
| 685 | 688 | app: App, |
| 686 | 689 | repo: Repo, |
| @@ -689,15 +692,20 @@ function waitOnMain( | ||
| 689 | 692 | ): boolean { |
| 690 | 693 | const { runId, commitOid, env } = payload; |
| 691 | 694 | if (payload["kind"] !== "check" || typeof runId !== "string") return false; |
| 692 | if (!sharedWithMain(redMain(app.db, repo.id), getRun(app.db, runId))) return false; | |
| 695 | const run = getRun(app.db, runId); | |
| 696 | const main = sharedWithMain(redMain(app.db, repo.id), run); | |
| 697 | if (!main && !sharedWithPulls(app.db, run)) return false; | |
| 693 | 698 | if (typeof commitOid === "string" && typeof env === "string") { |
| 694 | 699 | const waiting = waitingOnMain.get(repo.id) ?? new Map(); |
| 695 | 700 | waiting.set(issue.id, { env, commitOid }); |
| 696 | 701 | waitingOnMain.set(repo.id, waiting); |
| 697 | 702 | } |
| 698 | log.info("check failed only what main fails; waiting for main to be green", { | |
| 699 | issue: issue.number, | |
| 700 | }); | |
| 703 | log.info( | |
| 704 | main | |
| 705 | ? "check failed only what main fails; waiting for main to be green" | |
| 706 | : "check failed only what other pulls fail this hour; waiting for main to be green", | |
| 707 | { issue: issue.number }, | |
| 708 | ); | |
| 701 | 709 | return true; |
| 702 | 710 | } |
| 703 | 711 |
src/crew/sentinel.ts +8 −4
| @@ -407,10 +407,12 @@ export function degradedCauses(db: Database, now: number): Finding[] { | ||
| 407 | 407 | |
| 408 | 408 | /** |
| 409 | 409 | * The causes that are a budget breached: the lane, a silent run, a run |
| 410 | * killed twice, a red main holding its pulls' checks. The last gets one fix | |
| 411 | * issue (#1971), and `crew/red_fix.ts` keeps other builders off its test. | |
| 410 | * killed twice, a red main holding its pulls' checks, a test red on many | |
| 411 | * pulls at once. A red main gets one fix issue (#1971), and | |
| 412 | * `crew/red_fix.ts` keeps other builders off its test; a shared test gets | |
| 413 | * one flake issue (#2266), which its vitals card links to. | |
| 412 | 414 | */ |
| 413 | const BREACH_CAUSE = /^(queue:check|stuck:|budget:|red:)/; | |
| 415 | const BREACH_CAUSE = /^(queue:check|stuck:|budget:|red:|shared:)/; | |
| 414 | 416 | |
| 415 | 417 | /** |
| 416 | 418 | * Each budget breach that holds right now, however briefly: the crew hears |
| @@ -435,7 +437,9 @@ export function budgetBreaches(db: Database, now: number): Finding[] { | ||
| 435 | 437 | lookFor: `\`${s.cause}\` is gone from the degraded list and the cause is fixed, not waited out.`, |
| 436 | 438 | fix: s.cause.startsWith("red:") |
| 437 | 439 | ? "Fix the failing test on main in one pull. This is the only issue for it: pulls red on the same test wait for main, and no other builder is handed it while this is open." |
| 438 | : "Diagnose the cause from the evidence and hand the mechanical fix to a builder as its own issue.", | |
| 440 | : s.cause.startsWith("shared:") | |
| 441 | ? "Make the named test's verdict depend only on the code under test, not on the check machine's load, disk or clock, in one pull. Pulls red on it alone wait, unparked, for main's next green check." | |
| 442 | : "Diagnose the cause from the evidence and hand the mechanical fix to a builder as its own issue.", | |
| 439 | 443 | urgent: true, |
| 440 | 444 | }); |
| 441 | 445 | } |
src/kernel/id.ts +10
| @@ -51,6 +51,16 @@ export function id(now: number = Date.now()): string { | ||
| 51 | 51 | return out; |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | /** | |
| 55 | * The least id `id()` hands out at `ms`: every id made at or after it sorts | |
| 56 | * at or above this, so a time window over an id-indexed table is a range. | |
| 57 | */ | |
| 58 | export function idFloor(ms: number): string { | |
| 59 | let out = ""; | |
| 60 | for (let shift = 45; shift >= 0; shift -= 5) out += alphabet[Math.floor(Math.max(0, ms) / 2 ** shift) % 32]; | |
| 61 | return out + "0".repeat(16); | |
| 62 | } | |
| 63 | ||
| 54 | 64 | /** 256 bits of entropy, base64url, ~43 chars. For tokens and session ids. */ |
| 55 | 65 | export function secret(): string { |
| 56 | 66 | const bytes = new Uint8Array(32); |
src/kernel/vitals.ts +6 −1
| @@ -28,6 +28,7 @@ import { newestDrills } from "../core/domain_drills.ts"; | ||
| 28 | 28 | import { DOMAINS } from "./domains.ts"; |
| 29 | 29 | import { domainDrillNeeds } from "./domain_drill.ts"; |
| 30 | 30 | import { heldChecks, redSentence } from "../checks/red_main.ts"; |
| 31 | import { flakeHref, sharedCause, sharedSentence, sharedTests } from "../checks/shared_red.ts"; | |
| 31 | 32 | |
| 32 | 33 | /** Free disk under this share of the volume is something to say. */ |
| 33 | 34 | export const DISK_FREE_MIN = 0.1; |
| @@ -49,7 +50,7 @@ export interface EnvCount { | ||
| 49 | 50 | /** |
| 50 | 51 | * One cause of the forge being off: the `needs` sentence, a `cause` key that |
| 51 | 52 | * holds for as long as its condition does (`disk`, `errors`, `internal`, |
| 52 | * `queue:<kind>`, `stuck:<runId>`, `red:<repoId>`), when it began by the forge's own evidence | |
| 53 | * `queue:<kind>`, `stuck:<runId>`, `red:<repoId>`, `shared:<repoId>:<test>`), when it began by the forge's own evidence | |
| 53 | 54 | * (the oldest evidence the forge holds, else the first time this process saw |
| 54 | 55 | * it, held until the cause clears; `at` is never the answer to a repeat |
| 55 | 56 | * call), and where to look. |
| @@ -363,6 +364,10 @@ export function vitals(input: VitalsInput): Vitals { | ||
| 363 | 364 | `/${red.repo}/runs/${red.runId}`, |
| 364 | 365 | ); |
| 365 | 366 | |
| 367 | // A test failing on many pulls in the hour is one card, not a park per pull. | |
| 368 | for (const shared of sharedTests(input.db, now)) | |
| 369 | need(sharedCause(shared), sharedSentence(shared), shared.since, flakeHref(input.db, shared)); | |
| 370 | ||
| 366 | 371 | // A red restore drill, or none in eight days, is a card until a drill passes. |
| 367 | 372 | const drill = lastVerdict(input.dataDir); |
| 368 | 373 | const owed = drillNeed(drill, now, () => drillFirstSeen(input.dataDir)); |
test/vitals.test.ts +2 −2
| @@ -156,8 +156,8 @@ test("the number of queries does not grow with runs", () => { | ||
| 156 | 156 | if (i % 2) app.db.query("UPDATE run SET state='running' WHERE id=?1").run(r.id); |
| 157 | 157 | } |
| 158 | 158 | expect(count()).toBe(before); |
| 159 | // 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. | |
| 160 | expect(before).toBe(7); | |
| 159 | // The queue, the running runs, the runs killed twice for their budget, the lane's depth and finished checks, the red mains holding pulls' checks, the tests red on many pulls, and the newest drill of every domain. | |
| 160 | expect(before).toBe(8); | |
| 161 | 161 | }); |
| 162 | 162 | |
| 163 | 163 | test("errors are counted, bounded, redacted, and a burst is said", async () => { |
new above
offline