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

The queue:check sentence says when its checks are held by a red main, not waiting behind the lane #3125

Merged opened this

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

Merged

scout/forge:scout/3120-the-queue-check-sentence-says-when-its-c → main 2 commits fork 3 threads · 3 unresolved

  • Merged. scout/3120-the-queue-check-sentence-says-when-its-c was merged into main as de3c42b.

Review

Sign in to review this change.
4 files changed +86 −14
src/checks/red_main.ts +21
@@ -162,6 +162,27 @@ export function heldChecks(db: Database): { red: RedMain; held: number }[] {
162162 return out;
163163 }
164164
165 /**
166 * The queued checks no red main holds (startable now): how many, and the
167 * oldest one's queue time. A held check's age says nothing about the lane.
168 */
169 export function freeChecks(
170 db: Database,
171 redRepoIds: readonly string[],
172 now: number,
173 ): { n: number; oldest: string | null } {
174 const row = db
175 .query<{ n: number; oldest: string | null }, [string, string]>(
176 `SELECT COUNT(*) AS n, MIN(created_at) AS oldest FROM run
177 WHERE state = 'queued' AND kind = 'check'
178 AND (not_before IS NULL OR not_before <= ?1)
179 AND NOT (issue_id IS NOT NULL AND reason <> '${ASKED}'
180 AND repo_id IN (SELECT value FROM json_each(?2)))`,
181 )
182 .get(new Date(now).toISOString(), JSON.stringify(redRepoIds));
183 return row ?? { n: 0, oldest: null };

Add a created_at > now - RED_HOLD_MS condition to the NOT(...) clause so checks past the hold count as free, matching heldByRed.

184 }
185
165186 /** The card's sentence: what is red, by which test, and what waits. */
166187 export function redSentence(red: RedMain, held: number): string {
167188 const first = red.failing[0];
src/kernel/vitals.ts +22 −12
@@ -32,7 +32,7 @@ import { behindStanding } from "../release/behind.ts";
3232 import { newestDrills } from "../core/domain_drills.ts";
3333 import { DOMAINS } from "./domains.ts";
3434 import { domainDrillNeeds } from "./domain_drill.ts";
35 import { heldChecks, redSentence } from "../checks/red_main.ts";
35 import { freeChecks, heldChecks, redSentence } from "../checks/red_main.ts";
3636 import { flakeHref, sharedCause, sharedSentence, sharedTests } from "../checks/shared_red.ts";
3737
3838 /** Free disk under this share of the volume is something to say. */
@@ -249,16 +249,18 @@ export function laneSentence(
249249 oldest: string,
250250 lane: Lane,
251251 now: number,
252 held = 0,
252253 ): string {
253254 const since = new Date(oldest).toISOString().slice(11, 16);
254255 const head = `${queued} check${queued === 1 ? "" : "s"} ${queued === 1 ? "waits" : "wait"} behind one lane; the oldest since ${since}`;
255 if (lane.running === null) return `${head}; the lane runs nothing.`;
256 const more = held > 0 ? `; ${held} more held by a red main` : "";
257 if (lane.running === null) return `${head}; the lane runs nothing${more}.`;
256258 const age = Math.max(
257259 0,
258260 Math.round((now - new Date(lane.running.since).getTime()) / 60_000),
259261 );
260262 const what = lane.running.pull === null ? "a check" : `#${lane.running.pull}`;
261 return `${head}; the lane runs ${what} for ${age} min (limit ${BUDGETS.gate / 60_000}).`;
263 return `${head}; the lane runs ${what} for ${age} min (limit ${BUDGETS.gate / 60_000})${more}.`;
262264 }
263265
264266 export function vitals(input: VitalsInput): Vitals {
@@ -307,17 +309,25 @@ export function vitals(input: VitalsInput): Vitals {
307309 const running = listRunning(input.db);
308310 const lane = readLane(input.db, running, now);
309311 const queue: Record<string, number> = {};
312 const reds = heldChecks(input.db);
313 const held = reds.reduce((n, r) => n + r.held, 0);
314 const free = freeChecks(input.db, reds.map((r) => r.red.repoId), now);
310315 for (const row of queueDepth(input.db, now)) {
311316 queue[row.kind] = row.queued;
317 if (row.kind === "check") {
318 // Checks a red main holds are not waiting for the lane: the red: card names them.
319 // Age and "since" come from the free checks alone.
outdated

free is a count only. waited and row.oldest still come from the whole queue, so held checks decide when this fires and what the sentence says. Take the oldest from non-held checks.

320 if (free.oldest !== null && now - new Date(free.oldest).getTime() > BUDGETS.laneWait)
321 need(
322 "queue:check",
323 laneSentence(free.n, free.oldest, lane, now, Math.min(held, row.queued)),
324 new Date(free.oldest).toISOString(),
325 "/settings/capacity",
326 );
327 continue;
328 }
312329 const waited = now - new Date(row.oldest).getTime();
313 if (row.kind === "check" && waited > BUDGETS.laneWait)
314 need(
315 "queue:check",
316 laneSentence(row.queued, row.oldest, lane, now),
317 new Date(row.oldest).toISOString(),
318 "/settings/capacity",
319 );
320 else if (row.kind !== "check" && waited > QUEUE_WAIT_MAX_MS)
330 if (waited > QUEUE_WAIT_MAX_MS)
321331 need(
322332 `queue:${row.kind}`,
323333 `The ${row.kind} queue has waited over ${QUEUE_WAIT_MAX_MS / 60_000} minutes.`,
@@ -364,7 +374,7 @@ export function vitals(input: VitalsInput): Vitals {
364374 );
365375
366376 // A red main holding its pulls' checks is one card, naming the test.
367 for (const { red, held } of heldChecks(input.db))
377 for (const { red, held } of reds)
368378 need(
369379 `red:${red.repoId}`,
370380 redSentence(red, held),
test/red-main.test.ts +41
@@ -179,3 +179,44 @@ describe("one fix issue for a red main (#1971)", () => {
179179 expect(coveredByRedFix(db, "r1", dup)).toBe(false);
180180 });
181181 });
182
183 describe("the lane sentence and a red main", () => {
184 const old = (ids: string[]) =>
185 ids.forEach((id) =>
186 db.run("UPDATE run SET created_at = ?2 WHERE id = ?1", [id, new Date(Date.now() - 20 * 60_000).toISOString()]),
187 );
188 const read = () =>
189 vitals({
190 db,
191 signals: { errors: new ErrorLedger(), refused: new RefusedLedger() } as never,
192 dataDir: tmpdir(),
193 disk: () => ({ freeBytes: 95, totalBytes: 100 }),
194 }).degraded;
195
196 test("checks all held by a red main, with an idle lane, raise red: and not queue:check", () => {
197 main("b", "failed", ["x"]);
198 old(Array.from({ length: 10 }, (_, i) => pullCheck(String(i)).id));
199 const causes = read().map((d) => d.cause);
200 expect(causes).toContain("red:r1");
201 expect(causes).not.toContain("queue:check");
202 });
203
204 test("some held, some free: the sentence counts the free ones and names the held", () => {
205 main("b", "failed", ["x"]);
206 const held = [pullCheck("1"), pullCheck("2"), pullCheck("3")];
207 const asked = pullCheck("4", "asked");
208 old([...held, asked].map((r) => r.id));
209 const lane = read().find((d) => d.cause === "queue:check");

Here the free check is aged as well, so the test passes with the oldest-overall bug. Add a case where only the held checks are old and the free one is fresh.

210 expect(lane?.sentence).toStartWith("1 check waits behind one lane;");
211 expect(lane?.sentence).toEndWith("; 3 more held by a red main.");
212 });
213
214 test("old held checks and a fresh free one: the lane is not blamed", () => {
215 main("b", "failed", ["x"]);
216 old([pullCheck("1"), pullCheck("2"), pullCheck("3")].map((r) => r.id));
217 createRun(db, { repoId: "r1", kind: "check", env: "pull-7", issueId: pull, commitOid: OID("4"), reason: "asked" });
218 const causes = read().map((d) => d.cause);
219 expect(causes).toContain("red:r1");
220 expect(causes).not.toContain("queue:check");
221 });
222 });
test/vitals.test.ts +2 −2
@@ -167,8 +167,8 @@ test("the number of queries does not grow with runs", () => {
167167 if (i % 2) app.db.query("UPDATE run SET state='running' WHERE id=?1").run(r.id);
168168 }
169169 expect(count()).toBe(before);
170 // 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.
171 expect(before).toBe(8);
170 // 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, and the checks no red main holds.
171 expect(before).toBe(9);
172172 });
173173
174174 test("errors are counted, bounded, redacted, and a burst is said", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs