// Run with a dedicated test MongoDB. Starts and stops only its own Relay process.
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, writeFile, open } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import assert from "node:assert/strict";
const seconds = Number(process.argv[2] || process.env.RELAY_SOAK_SECONDS || 3600);
assert(Number.isFinite(seconds) && seconds > 0, "RELAY_SOAK_SECONDS must be positive");
const binary = resolve(process.env.RELAY_BINARY || "target/release/relay");
const binarySha256 = createHash("sha256").update(await readFile(binary)).digest("hex");
const directory = await mkdtemp(join(tmpdir(), "relay-soak-"));
const log = await open(join(directory, "server.jsonl"), "w");
const port = Number(process.env.RELAY_SOAK_PORT || 28187);
const server = spawn(binary, ["run", "examples/production_probe.ry"], {
env: { ...process.env, RELAY_ENV: "development", RELAY_BIND: `127.0.0.1:${port}`,
RELAY_MANAGEMENT_BIND: `127.0.0.1:${port + 1}`, RELAY_MONGO_DATABASE: `relay_soak_${Date.now()}` },
stdio: ["ignore", "ignore", log.fd],
});
let exited = false;
const stopped = new Promise(resolve => server.on("exit", (code, signal) => { exited = true; resolve({ code, signal }); }));
const base = `http://127.0.0.1:${port}`;
const stats = {};
const memory = [];
let unexpected = 0;
let outcome;
try {
let ready = false;
for (let attempt = 0; attempt < 150 && !exited; attempt++) {
try { ready = (await fetch(`${base}/plain`, { signal: AbortSignal.timeout(1000) })).ok; } catch {}
if (ready) break;
await delay(200);
}
assert(ready, `Server did not start; see ${directory}/server.jsonl`);
console.log(`Soak started: ${seconds}s, binary ${binarySha256}, logs ${directory}`);
const started = performance.now();
let lastSample = -15000;
let batch = 0;
while (performance.now() - started < seconds * 1000) {
assert(!exited, "Server exited during soak");
const cycle = performance.now();
await Promise.all(Array.from({ length: 8 }, async (_, worker) => {
const kind = ["plain", "json", "mongo", "isolation"][(batch + worker) % 4];
const name = `${batch}_${worker}`;
const url = `${base}/${kind}${kind === "isolation" ? `/${name}` : ""}`;
const before = performance.now();
try {
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
assert.equal(response.status, 200);
if (kind === "plain") assert.equal(await response.text(), "ok");
else {
const value = await response.json();
if (kind === "json") assert.deepEqual(value, { ok: true, count: 42, items: [1, null] });
if (kind === "mongo") assert.equal(value.count, 0);
if (kind === "isolation") {
assert.equal(value.name, name);
assert.equal(value.request_id, response.headers.get("x-request-id"));
}
}
const elapsed = performance.now() - before;
const entry = stats[kind] ||= { requests: 0, totalMs: 0, maxMs: 0, histogram: Array(24).fill(0) };
entry.requests++; entry.totalMs += elapsed; entry.maxMs = Math.max(entry.maxMs, elapsed);
entry.histogram[Math.min(23, Math.max(0, Math.ceil(Math.log2(Math.max(1, elapsed)))))]++;
} catch (error) { unexpected++; throw error; }
}));
batch++;
const elapsed = performance.now() - started;
if (elapsed - lastSample >= 15000) {
const status = await readFile(`/proc/${server.pid}/status`, "utf8");
memory.push({ seconds: elapsed / 1000, rssKiB: Number(status.match(/^VmRSS:\s+(\d+)/m)?.[1]) });
lastSample = elapsed;
if (memory.length % 4 === 0) console.log(`Soak ${Math.round(elapsed / 1000)}s: ${batch * 8} requests; RSS ${memory.at(-1).rssKiB} KiB`);
}
await delay(Math.max(0, 125 - (performance.now() - cycle)));
}
const metrics = await (await fetch(`http://127.0.0.1:${port + 1}/metrics`)).text();
assert.match(metrics, /relay_active_requests 0\n/);
assert.match(metrics, /relay_background_tasks 0\n/);
let steady = memory.filter(sample => sample.seconds >= Math.min(60, seconds / 4));
if (steady.length === 0) steady = memory;
// A reproducible bound, plus samples for reviewing any continuing growth.
const first = steady.slice(0, Math.max(1, Math.floor(steady.length / 3)));
const last = steady.slice(-Math.max(1, Math.floor(steady.length / 3)));
const average = samples => samples.reduce((sum, sample) => sum + sample.rssKiB, 0) / samples.length;
const growthKiB = average(last) - average(first);
assert(growthKiB <= Math.max(20 * 1024, average(first) * 0.2), `Excessive steady-state RSS growth: ${growthKiB} KiB`);
outcome = { passed: true, completeReleaseSoak: seconds >= 3600, seconds, binarySha256, unexpected, stats, memory, growthKiB, metrics, logDirectory: directory };
} catch (error) {
outcome = { passed: false, seconds, binarySha256, unexpected, stats, memory, error: error.stack, logDirectory: directory };
process.exitCode = 1;
} finally {
if (!exited) server.kill("SIGTERM");
const result = await Promise.race([stopped, delay(35000, null, { ref: false })]);
if (!result) { server.kill("SIGKILL"); outcome.passed = false; outcome.shutdown = "forced"; process.exitCode = 1; }
else { outcome.shutdown = result; if (result.code !== 0) { outcome.passed = false; process.exitCode = 1; } }
await log.close();
const report = resolve(process.env.RELAY_SOAK_REPORT || join(directory, "report.json"));
await writeFile(report, JSON.stringify(outcome, null, 2) + "\n");
console.log(`Soak ${outcome.passed ? "passed" : "failed"}: ${report}`);
}