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

The release load run is one command a person can take on a copy: both passes, both result files with a verdict, production refused #9598

Merged opened this

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

Merged

torque/forge:torque/9596-the-release-load-run-is-one-command-a-pe → main 2 commits fork

  • Merged. torque/9596-the-release-load-run-is-one-command-a-pe was merged into main as b21bf49.

Review

Sign in to review this change.
8 files changed +273 −31
bench/load.ts +17 −4
@@ -39,7 +39,8 @@
3939 */
4040
4141 import { readFileSync, writeFileSync } from "node:fs";
42 import { fell, missed, summarize, type LoadResult } from "../src/perf/load_verdict.ts";
42 import { fell, missed, summarize, TARGET, type LoadResult } from "../src/perf/load_verdict.ts";
43 import { refuseProduction, systemResolve } from "../src/perf/production_guard.ts";
4344
4445 const arg = (name: string, fallback?: string): string => {
4546 const at = process.argv.indexOf(`--${name}`);
@@ -49,6 +50,12 @@ const arg = (name: string, fallback?: string): string => {
4950 };
5051
5152 const base = arg("url").replace(/\/$/, "");
53 // Never at production (#9596): decided from the URL, before the first request.
54 const notHere = await refuseProduction(base, systemResolve);
55 if (notHere !== undefined) {
56 console.error(`load: refused: ${notHere}`);
57 process.exit(2);
58 }
5259 const repo = arg("repo");
5360 const rate = Number(arg("rate", "100"));
5461 const seconds = Number(arg("seconds", "10"));
@@ -180,11 +187,17 @@ counting = true;
180187 await flood(seconds);
181188 await Promise.all(inflight);
182189
183 const result: LoadResult = { ...summarize(version, rate, seconds, latencies, refused), cache: bypass ? "bypass" : "edge", refusedBy };
190 const warmTotals = warmPerSecond.reduce(([s, r], [a, b]) => [s + a, r + b], [0, 0]);
191 const result: LoadResult = {
192 ...summarize(version, rate, seconds, latencies, refused),
193 cache: bypass ? "bypass" : "edge",
194 refusedBy,
195 warm: { seconds: warm, served: warmTotals[0]!, refused: warmTotals[1]! },
196 };
184197 console.log(JSON.stringify(result, null, 2));
185198 console.log(`per second, served/refused: ${perSecond.map(([a, b]) => `${a}/${b}`).join(" ")}`);
186199 const short = missed(result);
187 console.log(short === undefined ? "target met: none refused, p95 under 100 ms" : `target missed: ${short}`);
200 console.log(short === undefined ? `target met: ${TARGET.rate}/s, none refused, p95 under ${TARGET.p95UnderMs} ms` : `target missed: ${short}`);
188201 if (cookie !== undefined) {
189202 const s = [...signedIn].sort((a, b) => a - b);
190203 const at = (p: number) => (s.length === 0 ? null : Math.round(s[Math.min(s.length - 1, Math.floor(p * s.length))]! * 10) / 10);
@@ -192,7 +205,7 @@ if (cookie !== undefined) {
192205 }
193206 const out = process.argv.includes("--out") ? arg("out") : undefined;
194207 // The file says itself whether the run met the goal, so a kept result can be read without rerunning it.
195 if (out !== undefined) writeFileSync(out, JSON.stringify({ ...result, verdict: short ?? "target met" }, null, 2) + "\n");
208 if (out !== undefined) writeFileSync(out, JSON.stringify({ ...result, target: TARGET, verdict: short ?? "target met" }, null, 2) + "\n");
196209 const last = process.argv.includes("--compare") ? (JSON.parse(readFileSync(arg("compare"), "utf8")) as LoadResult) : undefined;
197210 const why = last === undefined ? undefined : fell(last, result);
198211 if (why !== undefined) {
docs/features/load-test.md +24 −8
@@ -15,9 +15,9 @@ Media (`/media/…`, the route a PDF or image is drawn from) is **not covered**:
1515
1616 The bypasses are tested (`test/anonymous-deep.test.ts`): a signed-in reader, a repository made private, `?fragment=1` and a branch-name ref never get a page kept for a stranger. `--compare` exiting 1 is tested (`test/load-bench.test.ts`).
1717
18 **Release step:** `bun scripts/release-load.ts <version> --url <copy> --repo <owner/name>` runs the load twice, through the edge and with it bypassed, at 500/s for 20 s after a 20 s warm-up, writes `bench/results/<version>.json` and `<version>.bypass.json`, and compares each with the newest older kept file of its kind (`src/perf/baseline.ts`); exit 1 from either is the gate. With no older file it only writes the first; commit it. `scripts/cut-release.ts` prints this command as its last step; it does not run it (it needs a copy with data), and no baseline is kept yet — the first release run writes it.
18 **Release step:** `bun scripts/release-load.ts <version> --url <copy> --repo <owner/name>` runs the load twice, through the edge and with it bypassed, at 500/s for 20 s after a 20 s warm-up, writes `bench/results/<version>.json` and `<version>.bypass.json` (`--results DIR` writes them elsewhere), and compares each with the newest older kept file of its kind (`src/perf/baseline.ts`); exit 1 from either is the gate. With no older file it only writes the first; commit it. `scripts/cut-release.ts` prints this command as its last step; it does not run it (it needs a copy with data). See [the person's run](#the-persons-release-run-9596) below for the steps.
1919
20 No release baseline (`<version>.json`) is kept in `bench/results/` yet: the pull's preview holds no repository to read, and the load must not be aimed at production. The first files are written by the first run against a copy with data. The two `9514-*.json` files there are the in-process measurements below; `lastBaseline` ignores them.
20 No release baseline (`<version>.json`) is kept in `bench/results/` yet: the pull's preview holds no repository to read, and the load must not be aimed at production. The first files are written by the first run against a copy with data. The two `9514-*.json` files there are the in-process measurements below; `lastBaseline` ignores them. The crew writes no result file it did not measure.
2121
2222 Follow-ups: profile #9503, baseline #9504 (the file itself, still to be written from a copy with data), media #9505.
2323
@@ -96,12 +96,28 @@ What the caches hold (entries, bytes beside the cap, hits and misses) is on `/sy
9696
9797 A bigger git pool (`FORGE_GIT_CONCURRENCY=32`) changed nothing before item 5: the main thread was the limit, not git.
9898
99 ### Not done: the run through the edge, on a copy with production's data (#9565)
99 ### The run through the edge, on a copy with production's data (#9565, #9596)
100100
101 This sandbox reaches no copy holding production's repositories and no edge in front of one. The load must not be aimed at production, and a pull's preview holds no repository. The numbers above are the best case on a copy of one repository: no edge, the client on the same machine, and a warm start. They are not the `Done` of #9514. To take them, run from a machine that is not the server:
101 A crew sandbox reaches no copy holding production's repositories and no edge in front of one, so the numbers above are the best case: one repository, no edge, the client on the same machine, a warm start. They are not the `Done` of #9514. The run below is a person's, from a machine that is not the server.
102102
103 ```sh
104 bun scripts/release-load.ts <version> --url <copy behind the edge> --repo trevato/forge
105 ```
103 ## The person's release run (#9596)
104
105 **Never at production.** `scripts/release-load.ts` and `bench/load.ts` both refuse, with exit 2 and before any request goes out, a `--url` whose host is production's (`forge.open-platform.sh`), any name under it, or an address production's name resolves to (`src/perf/production_guard.ts`). A different name on the same edge (a hosted child, `<name>--forge.open-platform.sh`) is a different forge and is not refused. Load it only if it is yours.
106
107 1. **Restore a copy of production's data** on a box that is not production: `bun scripts/restore-drill.ts /srv/forge-copy` restores the newest snapshot into that empty directory (the `S3_*` variables, or `FORGE_SNAPSHOT_DIR`, and `FORGE_SNAPSHOT_PASSPHRASE`, as on the restore card), boots it once on a side port and checks it against production's public counts. The directory stays.
108 2. **Boot the copy as a forge**: `FORGE_DATA=/srv/forge-copy PORT=3100 bun run src/main.ts` from a clone at the release's tag. It runs the code you are releasing on production's repositories.
109 3. **Put the edge in front of it**, the same way production has it: a Cloudflare tunnel (or the cluster ingress) to `localhost:3100` under a name of your own, with the cache rule production has: cache HTML that is `public` and `s-maxage`, **bypass any request with a Cookie or Authorization header** (`src/http/addressed.ts`). Without that rule, the bypass pass reads cached pages and the two files show the same thing. Behind the tunnel, set `FORGE_TRUSTED_PROXIES` and `FORGE_CLIENT_IP_HEADER=cf-connecting-ip` ([OPERATING](../OPERATING.md)) so strangers are counted per address, not as one.
110 4. **Check the copy answers** at its name: `curl -sI https://<copy>/trevato/forge` is 200.
111 5. **Run it from a third machine** (not the copy's box, not production):
112
113 ```sh
114 bun install
115 bun scripts/release-load.ts <version> --url https://<copy> --repo trevato/forge
116 ```
117
118 It takes about 80 s: per pass, a 20 s warm-up that is not counted (its per-second `served/refused` line is printed), then 20 s at 500/s. Add `--crawl 3000` to spread the load over the repository rather than its top folder.
119 6. **Read the verdicts.** It ends with one line per file. Each of `bench/results/<version>.json` (through the edge) and `bench/results/<version>.bypass.json` (past it) holds `asked` (the rate), `servedPerSecond`, `refused` and `refusedBy`, `p50` and `p95` in ms, `warm` (`seconds`, `served`, `refused`), `target` (`{"rate": 500, "refused": 0, "p95UnderMs": 100}`) and `verdict`: `"target met"`, or how far it missed, e.g. `"12 refused of 10000; p95 131.4 ms, 31.4 ms over the 100 ms target"` or `"served 400/s, 100/s under the 500/s target"`. A missed target is written, not an error; exit 1 means the served rate fell more than 20% from the last kept file of the same kind.
120 7. **Post both files** as a comment on #9514, with the version, the machine the load ran from and the copy's box, and commit them in a pull to `bench/results/`. They become the first baseline the next release compares with.
121
122 Tests: `test/release-load-run.test.ts` runs the whole command against a local fake forge, one that keeps up (both files `"target met"`) and one that answers in 150 ms (both files say how far p95 missed), and checks that production is refused by name and by address before any request.
106123
107 This writes both kinds and compares each with its last kept result.
scripts/release-load.ts +38 −13
@@ -1,22 +1,41 @@
11 /**
2 * `bun scripts/release-load.ts <version> --url <copy> --repo <owner/name> [--rate N] [--seconds N]`:
3 * the release step's load check (#9504, #9514). Runs bench/load.ts against a
4 * copy with data twice, through the edge cache and with it bypassed, writes
5 * bench/results/<version>.json and <version>.bypass.json, and compares each
6 * with the newest kept older result of its kind; exit 1 is a fall of more
7 * than 20% in either. With no older result of a kind it only writes the
8 * first. Commit the files it writes.
2 * `bun scripts/release-load.ts <version> --url <copy> --repo <owner/name> [--rate N] [--seconds N] [--warm N] [--results DIR]`:
3 * the release step's load check (#9504, #9514, #9596). Runs bench/load.ts
4 * against a copy with data twice, through the edge cache and with it
5 * bypassed, writes `<version>.json` and `<version>.bypass.json` into
6 * bench/results (or `--results DIR`), each with its warm-up, the target and
7 * `"verdict"` ("target met" or how far it missed), and compares each with
8 * the newest kept older result of its kind; exit 1 is a fall of more than
9 * 20% in either. With no older result of a kind it only writes the first.
10 * A `--url` that is production is refused (exit 2) before any request goes
11 * out (`src/perf/production_guard.ts`). Commit the files it writes.
912 */
1013
11 import { existsSync, mkdirSync, readdirSync } from "node:fs";
12 import { CACHE_MODES, lastBaseline, loadArgs } from "../src/perf/baseline.ts";
14 import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
15 import { resolve } from "node:path";
16 import { CACHE_MODES, lastBaseline, loadArgs, resultFile } from "../src/perf/baseline.ts";
17 import { refuseProduction, systemResolve } from "../src/perf/production_guard.ts";
1318
14 const [version, ...rest] = process.argv.slice(2);
15 if (version === undefined || !/^\d+\.\d+\.\d+$/.test(version) || !rest.includes("--url") || !rest.includes("--repo")) {
16 console.error("usage: bun scripts/release-load.ts <major.minor.patch> --url <copy> --repo <owner/name> [--rate N] [--seconds N]");
19 const USAGE = "usage: bun scripts/release-load.ts <major.minor.patch> --url <copy> --repo <owner/name> [--rate N] [--seconds N] [--warm N] [--results DIR]";
20 const [version, ...args] = process.argv.slice(2);
21 const valueOf = (name: string): string | undefined => {
22 const at = args.indexOf(`--${name}`);
23 return at >= 0 ? args[at + 1] : undefined;
24 };
25 const url = valueOf("url");
26 if (version === undefined || !/^\d+\.\d+\.\d+$/.test(version) || url === undefined || valueOf("repo") === undefined) {
27 console.error(USAGE);
1728 process.exit(2);
1829 }
19 const dir = `${import.meta.dir}/../bench/results`;
30 const notHere = await refuseProduction(url, systemResolve);
31 if (notHere !== undefined) {
32 console.error(`release-load: refused: ${notHere}`);
33 process.exit(2);
34 }
35 // `--results` is this script's own; bench/load.ts is handed the rest.
36 const resultsAt = args.indexOf("--results");
37 const rest = resultsAt >= 0 ? [...args.slice(0, resultsAt), ...args.slice(resultsAt + 2)] : args;
38 const dir = resolve(valueOf("results") ?? `${import.meta.dir}/../bench/results`);
2039 mkdirSync(dir, { recursive: true });
2140 // Through the edge, then with it bypassed (#9514): both run, and either falling is the gate.
2241 let failed = 0;
@@ -27,4 +46,10 @@ for (const cache of CACHE_MODES) {
2746 const code = await Bun.spawn(cmd, { stdout: "inherit", stderr: "inherit", env: { BUN_CONFIG_MAX_HTTP_REQUESTS: "65000", ...process.env } }).exited;
2847 if (code !== 0) failed = code;
2948 }
49 // Both verdicts together at the end, read back from the files that will be committed.
50 for (const cache of CACHE_MODES) {
51 const file = `${dir}/${resultFile(version, cache)}`;
52 const verdict = existsSync(file) ? (JSON.parse(readFileSync(file, "utf8")) as { verdict?: string }).verdict : "not written";
53 console.error(`release-load (${cache}): ${file}: ${verdict}`);
54 }
3055 process.exit(failed);
src/boards/guide.ts +1 −2
@@ -227,8 +227,7 @@ export function parseSentence(text: string, now: number): Record<string, string>
227227 else if (/\byesterday\b/.test(t)) {
228228 out.from = since(Date.parse(startOfToday(now)), 1);
229229 out.to = startOfToday(now);
230 }
231 else if (/\b(today|so far today)\b/.test(t)) out.from = startOfToday(now);
230 } else if (/\b(today|so far today)\b/.test(t)) out.from = startOfToday(now);
232231 else if (/\b(a week|weekly)\b/.test(t)) days = 7;
233232 }
234233 if (days !== undefined) out.from = since(now, days);
src/perf/load_verdict.ts +24 −3
@@ -26,17 +26,38 @@ export interface LoadResult {
2626 readonly refusedBy?: Readonly<Record<string, number>>;
2727 /** `"target met"`, or `missed()`'s words, as the run that wrote it judged (#9548). Absent in results kept before. */
2828 readonly verdict?: string;
29 /** The uncounted warm-up before the measured window (#9596): its seconds and what it got. Absent in results kept before. */
30 readonly warm?: { readonly seconds: number; readonly served: number; readonly refused: number };
2931 }
3032
3133 /** The goal a release is held to (#9514): at the asked rate, no refusal and p95 under this, ms. */
3234 export const TARGET_P95_MS = 100;
35 /** The rate the goal is held at, requests per second (#9514). */
36 export const TARGET_RATE = 500;
37 /**
38 * The share of the asked rate a run must serve. The open loop sends
39 * floor(elapsed × rate), so a full run lands a request or two under; 95% is
40 * the slack for that, not for a forge that cannot keep up (refusals count apart).
41 */
42 export const SERVED_SHARE = 0.95;
3343
34 /** Words for how a run fell short of the goal, or undefined when it refused none and its p95 was under `TARGET_P95_MS`. */
44 /** What the goal is, written into each result file so it reads on its own (#9596). */
45 export const TARGET = { rate: TARGET_RATE, refused: 0, p95UnderMs: TARGET_P95_MS } as const;
46
47 const tenth = (n: number) => Math.round(n * 10) / 10;
48
49 /**
50 * Words for how far a run fell short of the goal (500/s asked and served, none
51 * refused, p95 under `TARGET_P95_MS`), or undefined when it met it.
52 */
3553 export function missed(r: LoadResult): string | undefined {
3654 const why: string[] = [];
37 if (r.refused > 0) why.push(`${r.refused} refused`);
55 if (r.asked < TARGET_RATE) why.push(`asked ${r.asked}/s, ${tenth(TARGET_RATE - r.asked)}/s under the ${TARGET_RATE}/s target`);
56 else if (r.served > 0 && r.servedPerSecond < TARGET_RATE * SERVED_SHARE)
57 why.push(`served ${r.servedPerSecond}/s, ${tenth(TARGET_RATE - r.servedPerSecond)}/s under the ${TARGET_RATE}/s target`);
58 if (r.refused > 0) why.push(`${r.refused} refused of ${r.served + r.refused}`);
3859 if (r.served === 0) why.push("none served");
39 else if (r.p95 >= TARGET_P95_MS) why.push(`p95 ${r.p95} ms, not under ${TARGET_P95_MS} ms`);
60 else if (r.p95 >= TARGET_P95_MS) why.push(`p95 ${r.p95} ms, ${tenth(r.p95 - TARGET_P95_MS)} ms over the ${TARGET_P95_MS} ms target`);
4061 return why.length === 0 ? undefined : why.join("; ");
4162 }
4263
src/perf/production_guard.ts added +49
@@ -0,0 +1,49 @@
1 /**
2 * The load is never aimed at production (#9596). Decides from the URL alone,
3 * before any request goes out, whether a `--url` would land on the forge's
4 * own public host: its name, any name under it, or an address literal that
5 * production's name resolves to. A different name on the same edge is a
6 * different forge (the edge routes by Host), so it is not refused here.
7 *
8 * Plain data in, words out: undefined when the URL may be loaded, else why not.
9 */
10
11 import { isIP } from "node:net";
12
13 /** The forge's own public host: production (as in `scripts/restore-drill.ts`). */
14 export const PRODUCTION_HOST = "forge.open-platform.sh";
15
16 /** Addresses a name resolves to; injected so tests ask no resolver. */
17 export type Resolve = (host: string) => Promise<readonly string[]>;
18
19 const bare = (host: string): string => host.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
20
21 /** A loopback or unspecified address: production is never one. */
22 const local = (ip: string): boolean => /^127\./.test(ip) || ip === "::1" || ip === "0.0.0.0" || ip === "::";
23
24 export async function refuseProduction(url: string, resolve: Resolve, production: string = PRODUCTION_HOST): Promise<string | undefined> {
25 let parsed: URL;
26 try {
27 parsed = new URL(url);
28 } catch {
29 return `--url ${JSON.stringify(url)} is not a URL`;
30 }
31 if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return `--url must be http or https, not ${parsed.protocol}`;
32 const host = bare(parsed.hostname);
33 const prod = bare(production);
34 if (host === prod || host.endsWith(`.${prod}`)) return `--url ${parsed.origin} is production (${prod}); run the load only at a copy you stand up`;
35 if (isIP(host) === 0 || local(host)) return undefined;
36 let addresses: readonly string[];
37 try {
38 addresses = (await resolve(prod)).map(bare);
39 } catch {
40 return `--url ${parsed.origin} is an address and ${prod} could not be resolved to rule out that it is production; use the copy's name`;
41 }
42 return addresses.includes(host) ? `--url ${parsed.origin} is an address of production (${prod}); run the load only at a copy you stand up` : undefined;
43 }
44
45 /** The system resolver, every address of a name. */
46 export const systemResolve: Resolve = async (host) => {
47 const { lookup } = await import("node:dns/promises");
48 return (await lookup(host, { all: true })).map((a) => a.address);
49 };
test/load-bench.test.ts +2 −1
@@ -67,7 +67,8 @@ test("--bypass sends every request with a cookie the edge must pass, --warm is n
6767 expect(withCookie).toBeGreaterThan(result.served);
6868 expect(result.served).toBeLessThanOrEqual(41);
6969 expect(out).toContain("warm-up, per second");
70 expect(out).toContain("target met");
70 // 40/s is under the 500/s the goal is held at, so the verdict says by how much (#9596).
71 expect(out).toContain("target missed: asked 40/s, 460/s under the 500/s target");
7172 } finally {
7273 server.stop(true);
7374 }
test/release-load-run.test.ts added +118
@@ -0,0 +1,118 @@
1 import { expect, test } from "bun:test";
2 import { existsSync } from "node:fs";
3 import { mkdtemp, readFile, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { missed } from "../src/perf/load_verdict.ts";
7 import { PRODUCTION_HOST, refuseProduction } from "../src/perf/production_guard.ts";
8
9 // The person's release run (#9596): one command, both passes, both files, each with its verdict.
10 const oid = "b".repeat(40);
11 const page = `<a href="/o/r/blob/${oid}/f.txt">f</a><a href="/o/r/tree/${oid}/d">d</a>`;
12
13 /** Runs scripts/release-load.ts against a fake forge answering after `delayMs`; returns both files and what each pass's request carried. */
14 const release = async (delayMs: number) => {
15 const dir = await mkdtemp(join(tmpdir(), "forge-release-load-"));
16 const cookies = { bypass: 0, none: 0 };
17 const server = Bun.serve({
18 port: 0,
19 fetch: async (req) => {
20 if (/\/(blob|tree|raw)\/[0-9a-f]{40}/.test(new URL(req.url).pathname)) {
21 req.headers.get("cookie") === "forge_load=bypass" ? cookies.bypass++ : cookies.none++;
22 if (delayMs > 0) await Bun.sleep(delayMs);
23 }
24 return new Response(page, { headers: { "content-type": "text/html" } });
25 },
26 });
27 try {
28 const p = Bun.spawn(
29 ["bun", "scripts/release-load.ts", "9.9.9", "--url", `http://127.0.0.1:${server.port}`, "--repo", "o/r", "--seconds", "1", "--warm", "1", "--results", dir],
30 { stdout: "pipe", stderr: "pipe" },
31 );
32 const [code, , err] = await Promise.all([p.exited, new Response(p.stdout).text(), new Response(p.stderr).text()]);
33 const read = async (f: string) => JSON.parse(await readFile(join(dir, f), "utf8"));
34 return { code, err, cookies, edge: await read("9.9.9.json"), bypass: await read("9.9.9.bypass.json") };
35 } finally {
36 server.stop(true);
37 await rm(dir, { recursive: true, force: true });
38 }
39 };
40
41 test("a forge that keeps up gets both files at 500/s, each with its warm-up and \"target met\"", async () => {
42 const r = await release(0);
43 expect(r.code).toBe(0);
44 for (const [file, cache] of [[r.edge, "edge"], [r.bypass, "bypass"]] as const) {
45 expect(file.version).toBe("9.9.9");
46 expect(file.cache).toBe(cache);
47 expect(file.asked).toBe(500);
48 expect(file.refused).toBe(0);
49 expect(file.servedPerSecond).toBeGreaterThanOrEqual(475);
50 expect(file.p95).toBeLessThan(100);
51 expect(typeof file.p50).toBe("number");
52 expect(file.warm.seconds).toBe(1);
53 expect(file.warm.served).toBeGreaterThan(0);
54 expect(file.target).toEqual({ rate: 500, refused: 0, p95UnderMs: 100 });
55 expect(file.verdict).toBe("target met");
56 }
57 // The edge pass carries no cookie; every request of the bypass pass carries the one the edge must pass on.
58 expect(r.cookies.none).toBeGreaterThan(0);
59 expect(r.cookies.bypass).toBeGreaterThan(0);
60 expect(r.err).toContain("9.9.9.json: target met");
61 expect(r.err).toContain("9.9.9.bypass.json: target met");
62 }, 30_000);
63
64 test("a forge answering in 150 ms gets both files saying how far p95 missed", async () => {
65 const r = await release(150);
66 // Missing the target is written down, not a failed run: only a fall from a kept result exits 1.
67 expect(r.code).toBe(0);
68 for (const file of [r.edge, r.bypass]) {
69 expect(file.p95).toBeGreaterThanOrEqual(150);
70 expect(file.verdict).toBe(`p95 ${file.p95} ms, ${Math.round((file.p95 - 100) * 10) / 10} ms over the 100 ms target`);
71 }
72 expect(r.err).toContain("9.9.9.bypass.json: p95 ");
73 }, 30_000);
74
75 test("the verdict says how far each part of the goal was missed", () => {
76 const r = (o: object) => ({ version: "1", asked: 500, seconds: 20, served: 10_000, refused: 0, servedPerSecond: 500, p50: 3, p95: 40, ...o });
77 expect(missed(r({}))).toBeUndefined();
78 expect(missed(r({ asked: 100 }))).toBe("asked 100/s, 400/s under the 500/s target");
79 expect(missed(r({ servedPerSecond: 400 }))).toBe("served 400/s, 100/s under the 500/s target");
80 expect(missed(r({ servedPerSecond: 476 }))).toBeUndefined();
81 expect(missed(r({ refused: 12, served: 9_988 }))).toBe("12 refused of 10000");
82 expect(missed(r({ p95: 131.4 }))).toBe("p95 131.4 ms, 31.4 ms over the 100 ms target");
83 });
84
85 test("production is refused by name, by any name under it and by its address; a copy is not", async () => {
86 let asked = 0;
87 const resolve = async () => (asked++, ["203.0.113.7", "2001:db8::7"]);
88 for (const url of [`https://${PRODUCTION_HOST}`, `https://FORGE.open-platform.sh./`, `http://${PRODUCTION_HOST}:8080/x`, `https://edge.${PRODUCTION_HOST}`, "https://203.0.113.7", "http://[2001:db8::7]:80"])
89 expect(await refuseProduction(url, resolve)).toContain("production");
90 expect(await refuseProduction("https://copy.example", resolve)).toBeUndefined();
91 expect(await refuseProduction("https://copy--forge.open-platform.sh", resolve)).toBeUndefined();
92 expect(await refuseProduction("http://127.0.0.1:9999", resolve)).toBeUndefined();
93 expect(await refuseProduction("https://198.51.100.1", resolve)).toBeUndefined();
94 // A name is decided without asking a resolver; only an address literal needs production's addresses.
95 expect(asked).toBe(3);
96 expect(await refuseProduction("https://198.51.100.1", async () => Promise.reject(new Error("no dns")))).toContain("could not be resolved");
97 expect(await refuseProduction("ftp://copy.example", resolve)).toContain("http or https");
98 expect(await refuseProduction("not a url", resolve)).toContain("not a URL");
99 });
100
101 test("release-load and bench/load.ts refuse a production --url before any request, writing nothing", async () => {
102 const dir = await mkdtemp(join(tmpdir(), "forge-release-load-"));
103 try {
104 for (const cmd of [
105 ["bun", "scripts/release-load.ts", "9.9.9", "--url", `https://${PRODUCTION_HOST}`, "--repo", "trevato/forge", "--results", dir],
106 ["bun", "bench/load.ts", "--url", `https://${PRODUCTION_HOST}/`, "--repo", "trevato/forge", "--out", join(dir, "x.json")],
107 ]) {
108 // A proxy that is not there: were a request sent, it would fail as a connection error, not this refusal.
109 const p = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe", env: { ...process.env, HTTPS_PROXY: "http://127.0.0.1:9", HTTP_PROXY: "http://127.0.0.1:9" } });
110 const [code, err] = await Promise.all([p.exited, new Response(p.stderr).text()]);
111 expect(code).toBe(2);
112 expect(err).toContain(`refused: --url https://${PRODUCTION_HOST} is production`);
113 }
114 expect(existsSync(join(dir, "9.9.9.json")) || existsSync(join(dir, "9.9.9.bypass.json")) || existsSync(join(dir, "x.json"))).toBe(false);
115 } finally {
116 await rm(dir, { recursive: true, force: true });
117 }
118 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs