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

The containment watch stops a cell for CPU only by the new rule: a 1-CPU forge sampled at 1.04 cores keeps running #11683

Merged opened this

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

Merged

scout/forge:scout/11613-the-containment-watch-stops-a-cell-for-c → main 7 commits fork 1 thread · 1 unresolved

  • Merged. scout/11613-the-containment-watch-stops-a-cell-for-c was merged into main as 0e8605c.

Review

Sign in to review this change.
6 files changed +39 −14
src/apps/contain.ts +8 −7
@@ -1237,7 +1237,7 @@ export function cpuBreach(
12371237 cpus: number,
12381238 top: { pid: number; ticks: number; name: string } | undefined,
12391239 ): string {
1240 const why = `used ${cores.toFixed(1)} cores for ${Math.round(seconds)}s, over its limit of ${cpus}`;
1240 const why = `used ${cores.toFixed(1)} cores for ${Math.round(seconds)}s, over twice its limit of ${cpus}`;
12411241 if (top === undefined) return why;
12421242 const spent = Math.round(top.ticks / CLK_TCK);
12431243 return `${why}; the busiest process is ${top.name} (pid ${top.pid}, ${spent}s of CPU)`;
@@ -1245,13 +1245,13 @@ export function cpuBreach(
12451245
12461246 /**
12471247 * Whether a step's error is the watch's CPU stop as `runStep` words it
1248 * (`<step> stopped: it used 16.3 cores for 31s, over its limit of 16…`,
1248 * (`<step> stopped: it used 16.3 cores for 31s, over twice its limit of 16…`,
12491249 * #11617). Anchored at the start: a failed step's own output only ever
12501250 * follows `<step> exited with code N —`, so a check cannot print its way
12511251 * into reading as stopped for CPU.
12521252 */
12531253 export function stoppedForCpu(error: string): boolean {
1254 return /^[a-z-]+ stopped: it used \d+(?:\.\d+)? cores for \d+s, over its limit of \d/.test(error);
1254 return /^[a-z-]+ stopped: it used \d+(?:\.\d+)? cores for \d+s, over (?:twice )?its limit of \d/.test(error);
12551255 }
12561256
12571257 /** A process's command name, printable and short, or "?" once it is gone. */
@@ -1313,7 +1313,7 @@ export interface CpuState {
13131313 * One tick's CPU decision for a watched cell: record the tree's CPU `ticks`
13141314 * at `now`, average the cores over `WATCH.cpuWindowMs`, keep the hold on
13151315 * `state`, and say whether `cpuWatch` (`apps/cpu_rule.ts`) stops it — with the
1316 * cores and the window it measured — or null to let it run (#11657).
1316 * cores and the seconds the overage has held — or null to let it run (#11657).
13171317 */
13181318 export function cpuSample(
13191319 state: CpuState,
@@ -1325,11 +1325,12 @@ export function cpuSample(
13251325 while (state.samples.length > 1 && now - (state.samples[1]?.at ?? now) >= WATCH.cpuWindowMs) state.samples.shift();
13261326 const oldest = state.samples[0];
13271327 if (oldest === undefined || now - oldest.at < WATCH.cpuWindowMs) return null;
1328 const seconds = (now - oldest.at) / 1000;
1329 const cores = (ticks - oldest.ticks) / CLK_TCK / seconds;
1328 const window = (now - oldest.at) / 1000;
1329 const cores = (ticks - oldest.ticks) / CLK_TCK / window;
13301330 const verdict = cpuWatch(state.overSince, cores, cpus, now, state.startedAt);
13311331 state.overSince = verdict.overSinceMs;
1332 return verdict.stop ? { cores, seconds } : null;
1332 // The seconds the overage has held, not the sample window it was averaged over.
1333 return verdict.stop && verdict.overSinceMs !== null ? { cores, seconds: (now - verdict.overSinceMs) / 1000 } : null;
13331334 }
13341335
13351336 function tick(): void {
src/flow/cpu_stops.ts +1 −1
@@ -12,7 +12,7 @@ import { CPU_STOP_SUMMARY } from "../core/runs.ts";
1212 const HOUR_MS = 3_600_000;
1313
1414 /** How the watch's stop reads in a red check's summary: `check failed: stopped: used 16.3 cores for 31s, over its limit of 16…`. */
15 const CPU_RED = /stopped: (?:it )?used \d+(?:\.\d+)? cores for \d+s, over its limit of \d/;
15 const CPU_RED = /stopped: (?:it )?used \d+(?:\.\d+)? cores for \d+s, over (?:twice )?its limit of \d/;
1616
1717 export interface CpuStops {
1818 /** Checks stopped for CPU in the hour. */
test/check-cpu-stop.test.ts +1 −1
@@ -23,7 +23,7 @@ const STOP = `check stopped: it ${cpuBreach(16.3, 31, 16, { pid: 52062, ticks: 4
2323
2424 describe("stoppedForCpu", () => {
2525 test("reads runStep's CPU stop, and nothing a check could print", () => {
26 expect(STOP).toBe("check stopped: it used 16.3 cores for 31s, over its limit of 16; the busiest process is sh (pid 52062, 462s of CPU)");
26 expect(STOP).toBe("check stopped: it used 16.3 cores for 31s, over twice its limit of 16; the busiest process is sh (pid 52062, 462s of CPU)");
2727 expect(stoppedForCpu(STOP)).toBe(true);
2828 expect(stoppedForCpu(`check stopped: it ${cpuBreach(3, 300, 2, undefined)}`)).toBe(true);
2929 // The check's own output follows "exited with code": it cannot pose as the watch.
test/cpu-breach.test.ts +26 −3
@@ -3,7 +3,7 @@
33 * (#11509), and only when one has spent any.
44 */
55 import { describe, expect, test } from "bun:test";
6 import { busiest, cpuBreach } from "../src/apps/contain.ts";
6 import { busiest, cpuBreach, cpuSample } from "../src/apps/contain.ts";
77
88 describe("a cell stopped for CPU", () => {
99 test("the busiest process is the one with the most ticks in the tree", () => {
@@ -20,11 +20,34 @@ describe("a cell stopped for CPU", () => {
2020 // 9_300 ticks over 30 s at 100 ticks a second is 3.1 cores.
2121 const cores = 9_300 / 100 / 30;
2222 expect(cpuBreach(cores, 30.4, 1, { pid: 4242, ticks: 9_150, name: "chrome" })).toBe(
23 "used 3.1 cores for 30s, over its limit of 1; the busiest process is chrome (pid 4242, 92s of CPU)",
23 "used 3.1 cores for 30s, over twice its limit of 1; the busiest process is chrome (pid 4242, 92s of CPU)",
2424 );
2525 });
2626
2727 test("with no process to name, the line is the cores alone", () => {
28 expect(cpuBreach(3.1, 30, 1, undefined)).toBe("used 3.1 cores for 30s, over its limit of 1");
28 expect(cpuBreach(3.1, 30, 1, undefined)).toBe("used 3.1 cores for 30s, over twice its limit of 1");
29 });
30
31 test("the rule through the watch's state: 1.04 of 1 for an hour never stops", () => {
32 const state = { samples: [], startedAt: 0, overSince: null as number | null };
33 let ticks = 0;
34 for (let t = 30_000; t <= 3_600_000; t += 30_000) {
35 ticks += 104 * 30;
36 expect(cpuSample(state, ticks, 1, t)).toBeNull();
37 }
38 expect(state.overSince).toBeNull();

Add the second case here: state {startedAt: 0, overSince: null}, 2.5 cores for 6 min starting at 15 min, expect exactly one non-null cpuSample and cpuBreach(over.cores, over.seconds, 1, undefined) to be 'used 2.5 cores for 300s, over twice its limit of 1'. That test would fail today because seconds is 30.

39 });
40
41 test("2.5 of 1 for 6 minutes on a 15-minute-old cell stops once, naming the hold", () => {
42 const state = { samples: [], startedAt: 0, overSince: null as number | null };
43 const lines: string[] = [];
44 let ticks = 0;
45 for (let t = 15 * 60_000; t <= 21 * 60_000; t += 1_000) {
46 if (t > 15 * 60_000) ticks += 250;
47 const over = cpuSample(state, ticks, 1, t);
48 if (over !== null) lines.push(cpuBreach(over.cores, over.seconds, 1, undefined));
49 if (over !== null) break;
50 }
51 expect(lines).toEqual(["used 2.5 cores for 300s, over twice its limit of 1"]);
2952 });
3053 });
test/cpu-rule.test.ts +2 −1
@@ -109,7 +109,8 @@ describe("the watch's tick feeds the rule (cpuSample)", () => {
109109 const over = cpuSample(state, Math.round((cores * CLK_TCK * t) / 1000), cpus, t);
110110 if (over !== null) {
111111 expect(over.cores).toBeCloseTo(cores, 5);
112 expect(over.seconds).toBe(WATCH.cpuWindowMs / 1000);
112 // Held from the first full window (30 s) to the stop: the hold, not the window.
113 expect(over.seconds).toBe((t - WATCH.cpuWindowMs) / 1000);
113114 return { stoppedAt: t, state };
114115 }
115116 }
test/crew-check-cpu-stop.test.ts +1 −1
@@ -106,7 +106,7 @@ describe("a pull's check stopped for CPU, through the service", () => {
106106 // The second stop in a row is reported red, and the builder is sent check_failed.
107107 release();
108108 const red = await until(() => pullChecks().find((r) => r.attempt === 1 && r.state === "failed"));
109 expect(red.summary).toContain("used 16.3 cores for 31s, over its limit of 16");
109 expect(red.summary).toContain("used 16.3 cores for 31s, over twice its limit of 16");
110110 expect(stops).toHaveLength(2);
111111 await until(() => (reworks().length > 0 ? true : undefined));
112112 expect(reworks()).toHaveLength(1);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs