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

test/apps-busy.test.ts runs in under 10 s, every test under 1 s, proving what it proves today #10224

Merged opened this

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

Merged

mason/forge:mason/10213-test-apps-busy-test-ts-runs-in-under-10 → main 1 commit fork 1 thread · 1 unresolved

  • Merged. mason/10213-test-apps-busy-test-ts-runs-in-under-10 was merged into main as dba5ca3.

Review

Sign in to review this change.
1 file changed +96 −49
test/apps-busy.test.ts +96 −49
@@ -22,26 +22,46 @@ import { serve, type Servers } from "../src/http/server.ts";
2222 import { readRunLog } from "../src/runs/log.ts";
2323
2424 /**
25 * The app under test: `/work?s=N` starts a child that works for N seconds
26 * and answers at once; `/stream` is an event stream that stays open until
27 * the browser goes away.
25 * The app under test: `/work` starts a child that works until `/done` ends
26 * it; `/slow` answers only once `/answer` is asked (`/waiting` counts
27 * those held); `/stream` is an event
28 * stream that stays open until the browser goes away. Nothing in it waits
29 * on the wall clock, so the suite never does either.
2830 */
2931 const BUSY_SERVER = `const port = Number(process.env.PORT ?? "0");
32 const children = new Set<ReturnType<typeof Bun.spawn>>();
33 const waiting: Array<() => void> = [];
3034 export const server = Bun.serve({
3135 port,
32 fetch(request) {
36 async fetch(request) {
3337 const url = new URL(request.url);
3438 if (url.pathname === "/work") {
35 Bun.spawn(["sleep", String(Number(url.searchParams.get("s") ?? "1"))], {
39 const child = Bun.spawn(["sleep", "300"], {
3640 stdio: ["ignore", "ignore", "ignore"],
3741 });
42 children.add(child);
3843 return new Response("working");
3944 }
45 if (url.pathname === "/done") {
46 for (const child of children) {
47 child.kill();
48 await child.exited;
49 }
50 children.clear();
51 return new Response("done");
52 }
4053 if (url.pathname === "/slow") {
4154 return new Promise((resolve) =>
42 setTimeout(() => resolve(new Response("late")), 2000),
55 waiting.push(() => resolve(new Response("late"))),
4356 );
4457 }
58 if (url.pathname === "/waiting") {
59 return new Response(String(waiting.length));
60 }
61 if (url.pathname === "/answer") {
62 for (const answer of waiting.splice(0)) answer();
63 return new Response("answered");
64 }
4565 if (url.pathname === "/stream") {
4666 let timer: ReturnType<typeof setInterval> | undefined;
4767 const body = new ReadableStream({
@@ -86,8 +106,14 @@ describe("apps that are busy", () => {
86106 let record: AppRecord;
87107 const DOMAIN = "apps.test";
88108 const HOST = `busy.${DOMAIN}`;
89 const WINDOW = 700;
90 const GRACE = 4_000;
109 // Windows of the sweep's own clock, which the suite runs ahead by hand:
110 // no real wait ever reaches them, so nothing sleeps unless a test says so.
111 const WINDOW = 60 * 60_000;
112 const GRACE = 10 * 60_000;
113 /** How far the sweep's clock runs ahead of the wall's. */
114 let ahead = 0;
115 /** How many sweeps have read the clock. */
116 let sweeps = 0;
91117
92118 const viaHost = (path = "/", init: RequestInit = {}) =>
93119 fetch(`${servers.url}${path}`, {
@@ -104,7 +130,7 @@ describe("apps that are busy", () => {
104130 while (Date.now() < deadline) {
105131 const value = read();
106132 if (value !== undefined) return value;
107 await Bun.sleep(50);
133 await Bun.sleep(5);
108134 }
109135 throw new Error("did not happen in time");
110136 }
@@ -114,14 +140,32 @@ describe("apps that are busy", () => {
114140 (r) => where(r) && (r.state === "ok" || r.state === "failed"),
115141 ),
116142 );
143 /**
144 * Run the sweep's clock to `ms` past the wall and wait for two sweeps to
145 * read it: the first decides synchronously after its read, so by the
146 * second every environment has been judged at that hour.
147 */
148 async function sweepAt(ms: number): Promise<void> {
149 ahead = ms;
150 const from = sweeps;
151 await until(() => (sweeps >= from + 2 ? true : undefined), 10_000);
152 }
117153 const asleep = () =>
118 until(() => (production().state === "asleep" ? true : undefined), 20_000);
154 until(() => (production().state === "asleep" ? true : undefined), 10_000);
119155 const log = (): string =>
120156 readRunLog(dataDir, production().runId!, 64 * 1024).lines.join("\n");
157 /** What the run log gained after `before`. */
158 const since = (before: string): string => log().slice(before.length);
121159
122 /** Wake production and wait for it to fall asleep, so each test starts rested. */
160 /** Put production to sleep and wake it, so each test starts rested. */
123161 async function freshlyAwake(): Promise<void> {
124 if (production().state !== "asleep") await asleep();
162 if (production().state !== "asleep") {
163 await viaHost("/done");
164 await viaHost("/answer");
165 await sweepAt(WINDOW + GRACE + 60_000);
166 await asleep();
167 }
168 ahead = 0;
125169 expect((await viaHost("/")).status).toBe(200);
126170 expect(production().state).toBe("running");
127171 }
@@ -135,9 +179,12 @@ describe("apps that are busy", () => {
135179 scheme: "http",
136180 publicPort: servers.port,
137181 forgeUrl: servers.url,
138 // Long enough for the scaffold and the busy app to deploy and settle.
139 sleepAfterMs: { production: 3_600_000, preview: 3_600_000 },
140 sweepEveryMs: 100,
182 sleepAfterMs: { production: WINDOW, preview: WINDOW },
183 clock: () => {
184 sweeps++;
185 return Date.now() + ahead;
186 },
187 sweepEveryMs: 10,
141188 busyGraceMs: GRACE,
142189 });
143190 await runtime.start();
@@ -176,18 +223,6 @@ describe("apps that are busy", () => {
176223 r.env === "production" && r.commitOid === committed.value.commitOid,
177224 );
178225 if (run.state !== "ok") throw new Error(`deploy failed: ${run.summary}`);
179 // The runtime the rest of the suite judges: the same data, a short window.
180 await runtime.stop();
181 runtime = new AppsRuntime(app, {
182 domain: DOMAIN,
183 scheme: "http",
184 publicPort: servers.port,
185 forgeUrl: servers.url,
186 sleepAfterMs: { production: WINDOW, preview: WINDOW },
187 sweepEveryMs: 100,
188 busyGraceMs: GRACE,
189 });
190 await runtime.start();
191226 await until(() => (production().state === "running" ? true : undefined));
192227 }, 180_000);
193228
@@ -199,24 +234,29 @@ describe("apps that are busy", () => {
199234 });
200235
201236 test("an idle app still sleeps, and says nothing about being busy", async () => {
237 // Invariant: idle past its window, an app with no work sleeps, and no busy is said.
202238 await freshlyAwake();
239 const before = log();
240 await sweepAt(WINDOW - 60_000);
241 expect(production().state).toBe("running");
242 await sweepAt(WINDOW);
203243 await asleep();
204244 expect(production().busy).toBeUndefined();
205 expect(log()).toContain("asleep: nothing asked for it");
206 }, 30_000);
245 expect(since(before)).toContain("asleep: nothing asked for it");
246 });
207247
208248 test.skipIf(noProc)(
209249 "an app whose child is still working survives the sweep, then sleeps once it is done",
210250 async () => {
251 // Invariant: a working child keeps it awake past the window, the pages say so, and it sleeps once the child is gone.
211252 await freshlyAwake();
212 // Five seconds of work: long enough that a loaded machine still checks
213 // while the child is busy (two seconds raced the check under load).
214 expect(await (await viaHost("/work?s=5")).text()).toBe("working");
253 const before = log();
254 expect(await (await viaHost("/work")).text()).toBe("working");
215255 // Well past the idle window, and still up: it was asked, and it is busy.
216 await Bun.sleep(WINDOW + 700);
256 await sweepAt(WINDOW + 60_000);
217257 expect(production().state).toBe("running");
218258 expect(production().busy).toContain("child processes");
219 expect(log()).toContain("kept awake: busy");
259 expect(since(before)).toContain("kept awake: busy");
220260
221261 // The app page and the Now card both say why it is up.
222262 const page = await (await fetch(`${servers.url}/alice/busy/app`)).text();
@@ -228,22 +268,30 @@ describe("apps that are busy", () => {
228268 expect(now).toContain("its child processes are still working");
229269
230270 // The child finishes; the next sweep finds nothing to wait for.
271 expect(await (await viaHost("/done")).text()).toBe("done");

Unbounded poll: use until(...) with a deadline so a miss gives a clear failure.

272 await sweepAt(WINDOW + 60_000);
231273 await asleep();
232274 expect(production().busy).toBeUndefined();
233275 },
234 30_000,
235276 );
236277
237278 test("a request still waiting on its answer keeps the app awake", async () => {
279 // Invariant: a request in flight keeps it awake past the window; once answered it sleeps.
238280 await freshlyAwake();
239 // The answer takes three windows; the sweep must not stop the app first.
240 const response = await viaHost("/slow");
241 expect(await response.text()).toBe("late");
281 const response = viaHost("/slow");
282 // The app holds it, so the proxy is holding it too.
283 while ((await (await viaHost("/waiting")).text()) !== "1") await Bun.sleep(5);
284 await sweepAt(WINDOW + 60_000);
242285 expect(production().state).toBe("running");
286 expect(production().busy).toContain("a request is still waiting");
287 await viaHost("/answer");
288 expect(await (await response).text()).toBe("late");
289 await sweepAt(WINDOW + 60_000);
243290 await asleep();
244 }, 30_000);
291 });
245292
246293 test("an open stream keeps the app awake until it closes", async () => {
294 // Invariant: an open stream keeps it awake past the window; once closed it sleeps.
247295 await freshlyAwake();
248296 const abort = new AbortController();
249297 const response = await viaHost("/stream", { signal: abort.signal });
@@ -251,29 +299,28 @@ describe("apps that are busy", () => {
251299 const reader = response.body!.getReader();
252300 const first = await reader.read();
253301 expect(new TextDecoder().decode(first.value)).toContain("data: hello");
254 await Bun.sleep(WINDOW + 700);
302 await sweepAt(WINDOW + 60_000);
255303 expect(production().state).toBe("running");
256304 expect(production().busy).toContain("a stream to it is still open");
257305
258306 abort.abort();
259307 await reader.cancel().catch(() => {});
260308 await asleep();
261 }, 30_000);
309 });
262310
263311 test.skipIf(noProc)(
264312 "an app that stays busy sleeps anyway once the grace is spent",
265313 async () => {
314 // Invariant: busy spares an app for one grace past its window, never longer.
266315 await freshlyAwake();
267 const started = Date.now();
268 // Works far longer than the window and the grace together.
269 await viaHost("/work?s=12");
270 await Bun.sleep(WINDOW + 700);
316 const before = log();
317 await viaHost("/work");
318 await sweepAt(WINDOW + GRACE - 60_000);
271319 expect(production().state).toBe("running");
320 expect(production().busy).toContain("child processes");
321 await sweepAt(WINDOW + GRACE);
272322 await asleep();
273 expect(Date.now() - started).toBeGreaterThanOrEqual(WINDOW + GRACE);
274 expect(Date.now() - started).toBeLessThan(10_000);
275 expect(log()).toContain("asleep: still busy");
323 expect(since(before)).toContain("asleep: still busy");
276324 },
277 30_000,
278325 );
279326 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs