109 lines
4.4 KiB
TypeScript
Executable File
109 lines
4.4 KiB
TypeScript
Executable File
// Process-topology decisions.
|
|
//
|
|
// The platform guard is the important one. `Deno.serve({reusePort: true})` is
|
|
// accepted on Windows and then FAILS AT BIND with os error 10048, because
|
|
// Windows has no SO_REUSEPORT. Without this planner a Windows operator setting
|
|
// FROSTY_WORKERS would get N processes racing for one port and N-1 opaque
|
|
// crashes; with it they get one process and a message saying why.
|
|
|
|
import { assert, assertEquals, assertStringIncludes } from "@std/assert";
|
|
import { denoRunFlags, planCluster, reusePortSupported } from "./cluster.ts";
|
|
|
|
Deno.test("reusePort is a POSIX capability", () => {
|
|
assert(reusePortSupported("linux"));
|
|
assert(reusePortSupported("darwin"));
|
|
// Measured, not assumed: two Deno processes binding :18099 with
|
|
// reusePort:true on Windows produce
|
|
// "Only one usage of each socket address ... (os error 10048)"
|
|
assert(!reusePortSupported("windows"));
|
|
});
|
|
|
|
Deno.test("unset or 1 serves in-process", () => {
|
|
assertEquals(planCluster(undefined, "linux").workers, 0);
|
|
assertEquals(planCluster("", "linux").workers, 0);
|
|
assertEquals(planCluster("1", "linux").workers, 0);
|
|
});
|
|
|
|
Deno.test("a valid count fans out on a platform that supports it", () => {
|
|
const plan = planCluster("4", "linux");
|
|
assertEquals(plan.workers, 4);
|
|
assert(plan.reason.includes("SO_REUSEPORT"));
|
|
});
|
|
|
|
Deno.test("Windows refuses to fan out and says why", () => {
|
|
const plan = planCluster("4", "windows");
|
|
// Degrade to single-process rather than crash at bind time.
|
|
assertEquals(plan.workers, 0);
|
|
assert(plan.reason.includes("SO_REUSEPORT"));
|
|
assert(plan.reason.includes("single-process"));
|
|
// The message has to name the way out, or the operator is just stuck.
|
|
assert(plan.reason.includes("Docker/Linux"));
|
|
});
|
|
|
|
Deno.test("malformed and out-of-range counts degrade to single-process", () => {
|
|
for (const raw of ["abc", "-2", "2.5", "0"]) {
|
|
const plan = planCluster(raw, "linux");
|
|
assertEquals(plan.workers, 0, `FROSTY_WORKERS=${raw} must not fan out`);
|
|
}
|
|
// Past the cap, add machines rather than processes.
|
|
const capped = planCluster("999", "linux");
|
|
assertEquals(capped.workers, 0);
|
|
assert(capped.reason.includes("cap"));
|
|
});
|
|
|
|
Deno.test("a worker child never re-forks", () => {
|
|
// Guards against a fork bomb: the child re-runs the same entry script, so it
|
|
// must read its role from the environment and serve rather than supervise.
|
|
const previous = Deno.env.get("FROSTY_WORKER_ROLE");
|
|
Deno.env.set("FROSTY_WORKER_ROLE", "child");
|
|
try {
|
|
const plan = planCluster("8", "linux");
|
|
assertEquals(plan.workers, 0);
|
|
assertEquals(plan.reason, "worker child");
|
|
} finally {
|
|
if (previous === undefined) {
|
|
Deno.env.delete("FROSTY_WORKER_ROLE");
|
|
} else {
|
|
Deno.env.set("FROSTY_WORKER_ROLE", previous);
|
|
}
|
|
}
|
|
});
|
|
|
|
Deno.test("planCluster: refuses to fan out without run permission", () => {
|
|
// Regression for the containerised failure: the runtime permission set
|
|
// deliberately omits --allow-run, so the supervisor used to die at the first
|
|
// Deno.Command with an uncaught NotCapable instead of serving.
|
|
const plan = planCluster("4", "linux", false);
|
|
assertEquals(plan.workers, 0);
|
|
assertStringIncludes(plan.reason, "run access");
|
|
assertStringIncludes(plan.reason, "single-process");
|
|
});
|
|
|
|
Deno.test("planCluster: fans out when the platform and permission allow", () => {
|
|
const plan = planCluster("4", "linux", true);
|
|
assertEquals(plan.workers, 4);
|
|
assertStringIncludes(plan.reason, "SO_REUSEPORT");
|
|
});
|
|
|
|
Deno.test("planCluster: permission is irrelevant where reusePort is absent", () => {
|
|
// Ordering matters: the platform message is the actionable one on Windows.
|
|
const plan = planCluster("4", "windows", true);
|
|
assertEquals(plan.workers, 0);
|
|
assertStringIncludes(plan.reason, "SO_REUSEPORT");
|
|
});
|
|
|
|
Deno.test("denoRunFlags: children mirror the supervisor's permission set", () => {
|
|
const flags = denoRunFlags(undefined);
|
|
// Least privilege: a child must NOT inherit the supervisor's --allow-run,
|
|
// or every worker could spawn processes.
|
|
assert(!flags.some((f) => f.startsWith("--allow-run")));
|
|
assertEquals(flags.includes("--unstable-net"), true, "reusePort needs it");
|
|
assertEquals(flags.includes("--allow-write=data"), true);
|
|
assert(!flags.some((f) => f.startsWith("--node-modules-dir")));
|
|
});
|
|
|
|
Deno.test("denoRunFlags: module resolution is inherited when set", () => {
|
|
const flags = denoRunFlags("none");
|
|
assert(flags.includes("--node-modules-dir=none"));
|
|
});
|