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

On a 1-CPU forge checks and snapshots start only between crew runs; two or more CPUs start them at once #11723

Merged opened this

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

Merged

scout/forge:scout/11599-on-a-1-cpu-forge-checks-and-snapshots-st → main 16 commits fork 2 threads · 2 unresolved

  • Merged. scout/11599-on-a-1-cpu-forge-checks-and-snapshots-st was merged into main as 2b9f819.

Review

Sign in to review this change.
6 files changed +291 −65
src/checks/service.ts +27 −15
@@ -18,6 +18,7 @@ import { chmod, lstat, readdir, rm } from "node:fs/promises";
1818 import { join } from "node:path";
1919 import type { App } from "../app.ts";
2020 import { releaseOnGreen } from "../actions/holds.ts";
21 import { activeCrewRuns, crewHoldsCore, forgeCpus, holdClock } from "../core/one_core.ts";
2122 import { announceRun } from "../actions/runs.ts";
2223 import { announceShard } from "./shard_event.ts";
2324 import type { ForgeEvent } from "../core/events.ts";
@@ -56,7 +57,6 @@ import { readCombined } from "../git/combined.ts";
5657 import { approvedOnce } from "./approved_first.ts";
5758 import { checkOnApproval } from "./approval.ts";
5859 import { checkKey, provenBy, recordProven } from "./proven.ts";
59 import { crewHoldsCore, forgeCpus } from "../core/one_core.ts";
6060 import { beginGate, endGate, gateHolding } from "./gate_record.ts";
6161 import { giveHistory } from "./history.ts";
6262 import { readLearned } from "./learned.ts";
@@ -145,10 +145,6 @@ export interface ChecksOptions {
145145 readonly concurrency?: number | undefined;
146146 /** The forge's public URL, told to the check as `FORGE_URL`. */
147147 readonly forgeUrl: string;
148 /** The forge's CPU count; a 1-CPU forge holds pull checks for a crew run. @default forgeCpus */
149 readonly cpus?: (() => number) | undefined;
150 /** Crew runs active now. @default the active agent runs */
151 readonly activeCrewRuns?: (() => number) | undefined;
152148 /** The parent's gate, for a hosted child: its checks take their turn in the parent's lane. */
153149 readonly gate?: Gate | undefined;
154150 /** Whose checks these are, written first in each run's log (a hosted child's name). */
@@ -189,6 +185,15 @@ export interface ChecksOptions {
189185 * to `VERIFY_EVERY_MS`. The forge sets it to `VERIFY_EVERY_MS`.
190186 */
191187 readonly verifyEveryMs?: number | undefined;
188 /** Test seam: how often the queue is looked at. @default TICK_MS */
189 readonly tickMs?: number | undefined;
190 /** The forge's CPU count; a 1-CPU forge holds pull checks for a crew run. @default forgeCpus */
191 readonly cpus?: (() => number) | undefined;
192 /** Crew runs working now. @default the running agent runs */
193 readonly activeCrewRuns?: (() => number) | undefined;
194 /** Test seams: how long a held pull check waits at most, and the clock it is judged by. */
195 readonly holdMaxMs?: number | undefined;
196 readonly holdNow?: (() => number) | undefined;
192197 }
193198
194199 /** A gate's answer from the lane: `interrupted` is no verdict (#3155). */
@@ -633,10 +638,20 @@ export function startChecks(app: App, options: ChecksOptions): ChecksHandle {
633638 return next;
634639 }
635640
641 const pullHold = holdClock(options.holdMaxMs);
642
636643 function schedule(): void {
637644 if (stopped) return;
638645 const limit = capacity();
outdated

This return skips startVerify() above and the rebase and main-check picks below. Hold only pull checks, and only for a bounded time.

639646 if (options.gate === undefined) gate.resize(limit);
647 // On one CPU a pull's check waits while a crew run works, for at most
648 // `holdMaxMs`; the verify lane, a rebase's check and a main check are the
649 // pipeline and never wait (#11599).
650 const crewHolds = crewHoldsCore((options.cpus ?? forgeCpus)(), options.activeCrewRuns ?? (() => activeCrewRuns(app.db)));
651 // The clock counts one continuous hold: it starts again once no crew run
652 // holds, and forgets checks that are no longer queued.
653 if (!crewHolds) pullHold.reset();
654 else pullHold.keep(new Set(listActive(app.db, "check").filter((run) => run.state === "queued").map((run) => run.id)));
640655 startVerify();
641656 // Every merge-queue gate in flight holds a slot, a line's first too (#6376).
642657 while (slotted() < limit - gatesInFlight()) {
@@ -652,25 +667,22 @@ export function startChecks(app: App, options: ChecksOptions): ChecksHandle {
652667 (repoId) => redMain(app.db, repoId),
653668 options.redHoldMs ?? 0,
654669 );
655 // One core: a crew run has it. A pull's check keeps its place and starts
656 // once the run has ended; main's check (the merge gate) is never held.
657 const crewHolds = crewHoldsCore(
658 (options.cpus ?? forgeCpus)(),
659 (options.activeCrewRuns ?? (() => listActive(app.db, "agent").length))(),
660 );
661 const runnable = crewHolds ? queued.filter((run) => run.issueId === null) : queued;
662670 // The limit is asked at the point of starting, one pick at a time:
663671 // a started check is in flight before the next is asked about, so a
664672 // burst of one account's checks cannot pass together. A rebase's
665673 // check goes before any other (`finishing_first.ts`, #6140).
666 const { rebases, rest: others } = rebasesFirst(app.db, runnable);
674 const { rebases, rest: others } = rebasesFirst(app.db, queued);
667675 // While a gate is about to start, the last free slot is its: a pull's
668676 // check waits for it, a rebase's and a main check do not.
669677 const held = holdsLastSlot(limit - gatesInFlight() - slotted(), gateQueued());
670 const next = startable(rebases) ?? startable(held ? others.filter((run) => run.issueId === null) : others);
678 const waiting = crewHolds
679 ? others.filter((run) => run.issueId === null || !pullHold.held(run.id, (options.holdNow ?? Date.now)()))
680 : others;
681 const next = startable(rebases) ?? startable(held ? waiting.filter((run) => run.issueId === null) : waiting);
671682 if (!next) return;
672683 const repo = getRepoById(app.db, next.repoId);
673684 if (!repo) return;
685 pullHold.release(next.id);
674686 start(next, repo);
675687 }
676688 }
@@ -1716,7 +1728,7 @@ export function startChecks(app: App, options: ChecksOptions): ChecksHandle {
17161728
17171729 const timer = setInterval(() => {
17181730 if (!stopped) during("lane", schedule);
1719 }, TICK_MS);
1731 }, options.tickMs ?? TICK_MS);
17201732 // Main is verified after landing on a schedule, when the forge says how often.
17211733 const sweeper =
17221734 options.verifyEveryMs === undefined
src/core/one_core.ts +40 −3
@@ -1,8 +1,9 @@
1 import type { Database } from "bun:sqlite";
12 import { availableParallelism } from "node:os";
23
3 /** True when a 1-CPU forge should hold background work for a crew run. */
4 export function crewHoldsCore(cpus: number, activeCrewRuns: number): boolean {
5 return cpus <= 1 && activeCrewRuns > 0;
4 /** True when a 1-CPU forge should hold background work for a crew run; more CPUs never count the runs. */
5 export function crewHoldsCore(cpus: number, activeCrewRuns: () => number): boolean {
6 return cpus <= 1 && activeCrewRuns() > 0;
67 }
78
89 /** The forge's own CPU count, at least 1. */
@@ -10,3 +11,39 @@ export function forgeCpus(): number {
1011 const n = availableParallelism();
1112 return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;
1213 }
14
15 /** Crew runs working now: agent runs in the `running` state. */

Looks counts active agent runs with listActive(app.db, 'agent'); this counts only state='running'. Keep one definition and use it in looks as well.

16 export function activeCrewRuns(db: Database): number {
17 const row = db.query<{ n: number }, []>("SELECT COUNT(*) AS n FROM run WHERE kind = 'agent' AND state = 'running'").get();
18 return row?.n ?? 0;
19 }
20
21 /** The longest a check or a snapshot waits for a crew run on one CPU before it starts anyway. */
22 export const MAX_HOLD_MS = 10 * 60_000;
23
24 /**
25 * Whether work keyed `key` is still held at `now`. The first time it is held
26 * starts its clock; once `maxMs` has passed it is held no longer, so crew runs
27 * back to back cannot starve it. `release` forgets the key when the work starts,
28 * `keep` forgets every key not named (work cancelled or superseded), and
29 * `reset` forgets them all once no crew run holds, so a later run holds afresh.
30 */
31 export function holdClock(maxMs = MAX_HOLD_MS) {
32 const since = new Map<string, number>();
33 return {
34 held(key: string, now: number): boolean {
35 const first = since.get(key) ?? now;
36 since.set(key, first);
37 return now - first < maxMs;
38 },
39 release(key: string): void {
40 since.delete(key);
41 },
42 keep(keys: ReadonlySet<string>): void {
43 for (const key of since.keys()) if (!keys.has(key)) since.delete(key);
44 },
45 reset(): void {
46 since.clear();
47 },
48 };
49 }
src/looks/service.ts +1 −1
@@ -408,7 +408,7 @@ export function startLooks(app: App, options: LooksOptions): LooksHandle {
408408 const cpus = options.cpus ?? forgeCpus;
409409 const active =
410410 options.activeCrewRuns ?? (() => listActive(app.db, "agent").length);
411 if (crewHoldsCore(cpus(), active())) {
411 if (crewHoldsCore(cpus(), active)) {
412412 queue.unshift(next);
413413 return;
414414 }
src/snapshot/service.ts +26 −39
@@ -11,6 +11,7 @@
1111 */
1212
1313 import type { App } from "../app.ts";
14 import { activeCrewRuns, crewHoldsCore, forgeCpus, holdClock } from "../core/one_core.ts";
1415 import { createLog } from "../kernel/log.ts";
1516 import { firstSeen, reportVerdict, seedFirstSeen } from "./verdict.ts";
1617 import {
@@ -36,8 +37,6 @@ import { isOffBox } from "./off_box.ts";
3637 import { noteOffBox, recordSnapshotOutcome } from "./outcome.ts";
3738 import { s3FromEnv, type Store } from "./store.ts";
3839 import { during } from "../perf/jobs.ts";
39 import { crewHoldsCore, forgeCpus } from "../core/one_core.ts";
40 import { listActive } from "../core/runs.ts";
4140
4241 const log = createLog("snapshots");
4342
@@ -109,6 +108,8 @@ const CATCH_UP_MS = 5 * 60_000;
109108 const SNAPSHOT_CATCH_UP_MS = 2 * 60_000;
110109 /** A snapshot still owed is looked for again this often; one boot's look is not the last (#10167). */
111110 const SNAPSHOT_CHECK_MS = 30 * 60_000;
111 /** A snapshot held for a crew run on one CPU looks again this often. */
112 const HOLD_RETRY_MS = 30_000;
112113
113114 export function untilDrill(now = Date.now()): number {
114115 const next = new Date(now);
@@ -153,9 +154,11 @@ export function startSnapshots(
153154 readonly bucketFirstMs?: number | undefined;
154155 /** Test seam: the clock `takeIfDue` judges by. */
155156 readonly now?: (() => number) | undefined;
156 /** Test seams: the forge's CPU count and how many crew runs are active (1 CPU: a run holds the core). */
157 /** Test seams: the forge's CPU count, crew runs working now, how soon a held snapshot looks again, and how long it waits at most. */
157158 readonly cpus?: (() => number) | undefined;
158159 readonly activeCrewRuns?: (() => number) | undefined;
160 readonly holdRetryMs?: number | undefined;
161 readonly holdMaxMs?: number | undefined;
159162 /** Test seam: how soon the first nightly timer fires. */
160163 readonly nightlyInMs?: number | undefined;
161164 } = {
@@ -178,6 +181,17 @@ export function startSnapshots(
178181 return resolvedMirror.store;
179182 };
180183 let stopped = false;
184 // On one CPU a due snapshot waits for the crew run to end, for at most
185 // `holdMaxMs`, then is taken anyway so back-to-back runs cannot starve it (#11599).
186 const wait = holdClock(options.holdMaxMs ?? MAX_DEFER_MS);
187 const crewHolds = (): boolean => {
188 if (!crewHoldsCore((options.cpus ?? forgeCpus)(), options.activeCrewRuns ?? (() => activeCrewRuns(app.db)))) {
189 wait.reset();
190 return false;
191 }
192 return wait.held("snapshot", (options.now ?? Date.now)());
193 };
194 const holdRetryMs = options.holdRetryMs ?? options.snapshotCheckMs ?? HOLD_RETRY_MS;
181195 let inFlight: Promise<Manifest> | undefined;
182196 let active = 0;
183197 let running: Progress | undefined;
@@ -189,6 +203,7 @@ export function startSnapshots(
189203 let offBox: boolean | undefined;
190204 // After the one before it, however that ended: chained on a failure as it
191205 // was, every later run failed at once with the old error until a restart.
206 wait.release("snapshot");
192207 const run = (inFlight ?? Promise.resolve()).catch(() => {}).then(async () => {
193208 // Resolved per run, so a destination saved on the Backups page is the
194209 // very next snapshot's.
@@ -271,26 +286,6 @@ export function startSnapshots(
271286 }
272287 };
273288
274 // One core: a crew run has it, so a due snapshot waits and is taken on the
275 // next tick after the run ends (src/core/one_core.ts). Never forever: after
276 // MAX_DEFER_MS of waiting it is taken anyway. A snapshot running is untouched.
277 let deferredSince: number | undefined;
278 const deferred = (): boolean => {
279 const at = (options.now ?? Date.now)();
280 const held = crewHoldsCore(
281 (options.cpus ?? forgeCpus)(),
282 (options.activeCrewRuns ?? (() => listActive(app.db, "agent").length))(),
283 );
284 if (!held) {
285 deferredSince = undefined;
286 return false;
287 }
288 deferredSince ??= at;
289 if (at - deferredSince >= MAX_DEFER_MS) return false;
290 log.info("snapshot due; a crew run holds the one core, waiting");
291 return true;
292 };
293
294289 // A restart takes the night's snapshot if the store does not hold it: a
295290 // release at 08:21 killed the one in flight and nothing took it again (#8742).
296291 // The manifest is written last, so a snapshot cut short is not one.
@@ -305,9 +300,7 @@ export function startSnapshots(
305300 log.warn("snapshot catch-up: store unreadable, not taking one now", { error: String(error) });
306301 return false;
307302 }
308 if (stopped || active > 0 || !snapshotDue(newest?.finishedAt, (options.now ?? Date.now)())) return false;
309 if (deferred()) return false;
310 deferredSince = undefined;
303 if (stopped || active > 0 || !snapshotDue(newest?.finishedAt, (options.now ?? Date.now)()) || crewHolds()) return false;
311304 const cut = await cutShort(store, newest?.name);
312305 log.info("snapshot due or lost; taking one", { newest: newest?.name ?? "none", resuming: cut?.toISOString() ?? "no" });
313306 await take(cut).catch(() => {});
@@ -411,18 +404,12 @@ export function startSnapshots(
411404 if (timer !== undefined) clearTimeout(timer);
412405 timer = undefined;
413406 if (options.nightly === false || stopped) return;
414 timer = setTimeout(
415 () =>
416 void during("snapshot", () => {
417 if (active === 0 && deferred()) {
418 arm(options.snapshotCheckMs ?? SNAPSHOT_CHECK_MS);
419 return Promise.resolve();
420 }
421 deferredSince = undefined;
422 return take().then(() => {});
423 }).catch(() => {}),
424 ms,
425 );
407 timer = setTimeout(() => {
408 void during("snapshot", async () => {
409 if (active === 0 && crewHolds()) return arm(holdRetryMs);
410 await take();
411 }).catch(() => {});
412 }, ms);
426413 }
427414 arm(options.nightlyInMs ?? untilNightly());
428415
@@ -496,7 +483,7 @@ export function startSnapshots(
496483 .catch(() => {})
497484 .finally(() => {
498485 bootLookAt = undefined;
499 scheduleCatchUp(options.snapshotCheckMs ?? SNAPSHOT_CHECK_MS);
486 scheduleCatchUp(crewHolds() ? holdRetryMs : (options.snapshotCheckMs ?? SNAPSHOT_CHECK_MS));
500487 });
501488 }, ms);
502489 };
test/one-core-hold.test.ts added +190
@@ -0,0 +1,190 @@
1 /** #11599: on one CPU a check or a snapshot waits for the crew run; two CPUs start them at once. */
2 import { afterEach, beforeEach, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { startChecks, type ChecksHandle } from "../src/checks/service.ts";
8 import { createRepo } from "../src/actions/repos.ts";
9 import { createIssue } from "../src/core/issues.ts";
10 import { createRun, getRun } from "../src/core/runs.ts";
11 import { createUser } from "../src/core/users.ts";
12 import { startSnapshots, type SnapshotsHandle } from "../src/snapshot/service.ts";
13 import { dirStore } from "../src/snapshot/store.ts";
14 import { holdClock } from "../src/core/one_core.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 let dir: string;
20 let app: App;
21 let checks: ChecksHandle | undefined;
22 let snaps: SnapshotsHandle | undefined;
23 let seeded: { userId: string; repoId: string } | undefined;
24
25 beforeEach(async () => {
26 dir = await mkdtemp(join(tmpdir(), "forge-one-core-"));
27 app = createApp({ dataDir: dir, port: 0 });
28 });
29 afterEach(async () => {
30 await checks?.stop({ drain: false });
31 await snaps?.stop();
32 checks = snaps = seeded = undefined;
33 app.close();
34 await rm(dir, { recursive: true, force: true });
35 });
36
37 const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
38 async function until(ok: () => boolean, ms = 3000): Promise<boolean> {
39 for (const end = Date.now() + ms; Date.now() < end; await sleep(20)) if (ok()) return true;
40 return ok();
41 }
42
43 async function queuedCheck(opts: { pull?: boolean; env?: string; reason?: string; rebase?: boolean } = {}): Promise<string> {
44 if (seeded === undefined) {
45 const made = await createUser(app.db, { origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true });
46 if (!made.ok) throw new Error(made.error);
47 const repo = await createRepo(app, made.value, { name: "lib", visibility: "public" });
48 if (!repo.ok) throw new Error(String(repo.error));
49 seeded = { userId: made.value.id, repoId: repo.value.id };
50 }
51 const issue = createIssue(app.db, { repoId: seeded.repoId, authorId: seeded.userId, title: "change" });
52 if (!issue.ok) throw new Error(issue.error);
53 if (opts.rebase) createRun(app.db, { repoId: seeded.repoId, kind: "agent", issueId: issue.value.id, reason: "conflict", commitOid: "0".repeat(40) });
54 return createRun(app.db, { repoId: seeded.repoId, kind: "check", env: opts.env ?? "", commitOid: "0".repeat(40), reason: opts.reason ?? "test", ...(opts.pull === false ? {} : { issueId: issue.value.id }) }).id;
55 }
56
57 test("1 CPU with a crew run holds a queued check, and it starts once the run is gone", async () => {
58 const id = await queuedCheck();
59 let crew = 1;
60 checks = startChecks(app, { forgeUrl: "http://127.0.0.1:9", concurrency: 1, cpus: () => 1, activeCrewRuns: () => crew, tickMs: 25 });
61 await sleep(200);
62 expect(getRun(app.db, id)!.state).toBe("queued");
63 crew = 0;
64 expect(await until(() => getRun(app.db, id)!.state !== "queued")).toBe(true);
65 });
66
67 test("2 CPUs start a queued check at once, whatever the crew does", async () => {
68 const id = await queuedCheck();
69 checks = startChecks(app, { forgeUrl: "http://127.0.0.1:9", concurrency: 1, cpus: () => 2, activeCrewRuns: () => 1, tickMs: 25 });
70 expect(await until(() => getRun(app.db, id)!.state !== "queued")).toBe(true);
71 });
72
73 test("1 CPU with a crew run does not hold a main check, a rebase check or a verify run", async () => {
74 const main = await queuedCheck({ pull: false });
75 checks = startChecks(app, { forgeUrl: "http://127.0.0.1:9", concurrency: 1, cpus: () => 1, activeCrewRuns: () => 1, tickMs: 25 });
76 expect(await until(() => getRun(app.db, main)!.state !== "queued")).toBe(true);
77 });
78
79 test("1 CPU: a verify-lane run starts with a crew run active", async () => {
80 const verify = await queuedCheck({ pull: false, env: "verify" });
81 checks = startChecks(app, { forgeUrl: "http://127.0.0.1:9", concurrency: 1, cpus: () => 1, activeCrewRuns: () => 1, tickMs: 25 });
82 expect(await until(() => getRun(app.db, verify)!.state !== "queued")).toBe(true);
83 });
84
85 test("1 CPU: a rebase's check starts with a crew run active, a pull's check beside it waits", async () => {
86 const rebase = await queuedCheck({ rebase: true });
87 const pull = await queuedCheck();
88 checks = startChecks(app, { forgeUrl: "http://127.0.0.1:9", concurrency: 2, cpus: () => 1, activeCrewRuns: () => 1, tickMs: 25 });
89 expect(await until(() => getRun(app.db, rebase)!.state !== "queued")).toBe(true);
90 expect(getRun(app.db, pull)!.state).toBe("queued");
91 });
92
93 test("1 CPU: a pull's check held past the maximum wait starts anyway", async () => {
94 const id = await queuedCheck();
95 let now = 1_000;
96 checks = startChecks(app, { forgeUrl: "http://127.0.0.1:9", concurrency: 1, cpus: () => 1, activeCrewRuns: () => 1, tickMs: 25, holdMaxMs: 5_000, holdNow: () => now });
97 await sleep(150);
98 expect(getRun(app.db, id)!.state).toBe("queued");
99 now += 5_000;
100 expect(await until(() => getRun(app.db, id)!.state !== "queued")).toBe(true);
101 });
102
103 test("1 CPU: a check held, then idle, is held afresh when the next crew run comes", async () => {
104 const id = await queuedCheck();
105 let now = 1_000;
106 let crew = 1;
107 checks = startChecks(app, { forgeUrl: "http://127.0.0.1:9", concurrency: 1, cpus: () => 1, activeCrewRuns: () => crew, tickMs: 25, holdMaxMs: 5_000, holdNow: () => now });
108 await sleep(100);
109 expect(getRun(app.db, id)!.state).toBe("queued");
110 // Kept queued by something else (a not-before) while no crew run works.
111 app.db.run("UPDATE run SET not_before = ? WHERE id = ?", [new Date(Date.now() + 60_000).toISOString(), id]);
112 now += 4_000;
113 crew = 0;
114 await sleep(100);
115 // The crew returns past the first clock's bound: a fresh hold, not a release.
116 crew = 1;
117 now += 2_000;
118 app.db.run("UPDATE run SET not_before = NULL WHERE id = ?", [id]);
119 await sleep(150);
120 expect(getRun(app.db, id)!.state).toBe("queued");
121 now += 5_000;
122 expect(await until(() => getRun(app.db, id)!.state !== "queued")).toBe(true);
123 });
124
125 test("the hold clock releases work after its maximum, and a started key begins again", () => {
126 const c = holdClock(100);
127 expect(c.held("a", 0)).toBe(true);
128 expect(c.held("a", 99)).toBe(true);
129 expect(c.held("a", 100)).toBe(false);
130 c.release("a");
131 expect(c.held("a", 500)).toBe(true);
132 });
133
134 test("the hold clock starts again after reset, and keep forgets work no longer waiting", () => {
135 const c = holdClock(100);
136 c.held("a", 0);
137 c.held("b", 0);
138 c.reset();
139 expect(c.held("a", 150)).toBe(true);
140 c.held("b", 150);
141 c.keep(new Set(["a"]));
142 expect(c.held("a", 250)).toBe(false);
143 expect(c.held("b", 260)).toBe(true);
144 });
145
146 // The timers are set far off: each test drives `takeIfDue` itself, with an injected clock.
147 function snapshotRuns(cpus: number, crew: () => number, extra: { holdMaxMs?: number; now?: () => number } = {}) {
148 snaps = startSnapshots(app, dirStore(join(dir, "store")), {
149 version: "t",
150 cpus: () => cpus,
151 activeCrewRuns: crew,
152 snapshotCatchUpMs: 3_600_000,
153 snapshotCheckMs: 3_600_000,
154 holdRetryMs: 3_600_000,
155 catchUpMs: 3_600_000,
156 mirrorEveryMs: 3_600_000,
157 nightly: false,
158 ...extra,
159 });
160 return snaps;
161 }
162
163 test("1 CPU with a crew run holds a due snapshot, and takes it once the run is gone", async () => {
164 let crew = 1;
165 const s = snapshotRuns(1, () => crew);
166 expect(await s.takeIfDue()).toBe(false);
167 expect((await s.list()).length).toBe(0);
168 crew = 0;
169 expect(await s.takeIfDue()).toBe(true);
170 expect(s.last().manifest).toBeDefined();
171 });
172
173 test("1 CPU: a snapshot held past the maximum wait is taken anyway", async () => {
174 let now = 1_000;
175 const s = snapshotRuns(1, () => 1, { holdMaxMs: 5_000, now: () => now });
176 expect(await s.takeIfDue()).toBe(false);
177 now += 4_999;
178 expect(await s.takeIfDue()).toBe(false);
179 now += 1;
180 expect(await s.takeIfDue()).toBe(true);
181 expect(s.last().manifest).toBeDefined();
182 });
183
184 test("2 CPUs take a due snapshot at once, without asking about crew runs", async () => {
185 let asked = 0;
186 const s = snapshotRuns(2, () => (asked++, 1));
187 expect(await s.takeIfDue()).toBe(true);
188 expect(s.last().manifest).toBeDefined();
189 expect(asked).toBe(0);
190 });
test/one-core.test.ts +7 −7
@@ -1,16 +1,16 @@
11 import { expect, test } from "bun:test";
22 import { crewHoldsCore, forgeCpus } from "../src/core/one_core.ts";
33
4 test("1 CPU with a crew run is held", () => expect(crewHoldsCore(1, 1)).toBe(true));
5 test("1 CPU with no run is not held", () => expect(crewHoldsCore(1, 0)).toBe(false));
4 test("1 CPU with a crew run is held", () => expect(crewHoldsCore(1, () => 1)).toBe(true));
5 test("1 CPU with no run is not held", () => expect(crewHoldsCore(1, () => 0)).toBe(false));
66 test("2 and 4 CPUs with a run are not held", () => {
7 expect(crewHoldsCore(2, 3)).toBe(false);
8 expect(crewHoldsCore(4, 1)).toBe(false);
7 expect(crewHoldsCore(2, () => 3)).toBe(false);
8 expect(crewHoldsCore(4, () => 1)).toBe(false);
99 });
1010 test("0 or negative CPUs read as 1 cpu: held with a run", () => {
11 expect(crewHoldsCore(0, 1)).toBe(true);
12 expect(crewHoldsCore(-2, 1)).toBe(true);
13 expect(crewHoldsCore(0, 0)).toBe(false);
11 expect(crewHoldsCore(0, () => 1)).toBe(true);
12 expect(crewHoldsCore(-2, () => 1)).toBe(true);
13 expect(crewHoldsCore(0, () => 0)).toBe(false);
1414 });
1515 test("forgeCpus is a whole number of at least 1", () => {
1616 const n = forgeCpus();

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs