summaryrefslogtreecommitdiffhomepage
path: root/packages/host-bin/src/collector-supervisor.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/host-bin/src/collector-supervisor.test.ts')
-rw-r--r--packages/host-bin/src/collector-supervisor.test.ts574
1 files changed, 287 insertions, 287 deletions
diff --git a/packages/host-bin/src/collector-supervisor.test.ts b/packages/host-bin/src/collector-supervisor.test.ts
index 8c1f104..f590806 100644
--- a/packages/host-bin/src/collector-supervisor.test.ts
+++ b/packages/host-bin/src/collector-supervisor.test.ts
@@ -2,300 +2,300 @@ import { describe, expect, it } from "vitest";
import { type ChildHandle, createCollectorSupervisor } from "./collector-supervisor.js";
interface FakeChild {
- readonly handle: ChildHandle;
- resolveExit: (code: number) => void;
- readonly signals: string[];
+ readonly handle: ChildHandle;
+ resolveExit: (code: number) => void;
+ readonly signals: string[];
}
function createFakeChild(code = 0): FakeChild {
- let resolveExit!: (code: number) => void;
- const exited = new Promise<number>((r) => {
- resolveExit = r;
- });
- const signals: string[] = [];
- const handle: ChildHandle = {
- kill: (signal?: string) => {
- signals.push(signal ?? "SIGTERM");
- if (signal === "SIGKILL") resolveExit(code);
- },
- exited,
- };
- return { handle, resolveExit, signals };
+ let resolveExit!: (code: number) => void;
+ const exited = new Promise<number>((r) => {
+ resolveExit = r;
+ });
+ const signals: string[] = [];
+ const handle: ChildHandle = {
+ kill: (signal?: string) => {
+ signals.push(signal ?? "SIGTERM");
+ if (signal === "SIGKILL") resolveExit(code);
+ },
+ exited,
+ };
+ return { handle, resolveExit, signals };
}
function createFakeLogger() {
- const msgs: Array<{ level: string; msg: string }> = [];
- return {
- msgs,
- debug: () => {},
- info: (msg: string) => msgs.push({ level: "info", msg }),
- warn: (msg: string) => msgs.push({ level: "warn", msg }),
- error: () => {},
- child: () => createFakeLogger(),
- span: () => ({
- id: "s",
- log: createFakeLogger(),
- setAttributes: () => {},
- addLink: () => {},
- child: () => ({}) as never,
- end: () => {},
- }),
- };
+ const msgs: Array<{ level: string; msg: string }> = [];
+ return {
+ msgs,
+ debug: () => {},
+ info: (msg: string) => msgs.push({ level: "info", msg }),
+ warn: (msg: string) => msgs.push({ level: "warn", msg }),
+ error: () => {},
+ child: () => createFakeLogger(),
+ span: () => ({
+ id: "s",
+ log: createFakeLogger(),
+ setAttributes: () => {},
+ addLink: () => {},
+ child: () => ({}) as never,
+ end: () => {},
+ }),
+ };
}
describe("createCollectorSupervisor", () => {
- const DEFAULTS = {
- journalPath: "/tmp/journal.ndjson",
- dbPath: "/tmp/traces.db",
- };
-
- it("start() spawns with the correct command and args", () => {
- let capturedCmd: string[] = [];
- const children: FakeChild[] = [];
- const spawn = (cmd: string[]) => {
- capturedCmd = cmd;
- const child = createFakeChild();
- children.push(child);
- return child.handle;
- };
-
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- spawn,
- logger: createFakeLogger() as never,
- });
- supervisor.start();
-
- expect(capturedCmd).toEqual([
- "bun",
- "packages/observability-collector/src/main.ts",
- "--journal",
- "/tmp/journal.ndjson",
- "--db",
- "/tmp/traces.db",
- ]);
- });
-
- it("start() passes --interval when provided", () => {
- let capturedCmd: string[] = [];
- const spawn = (cmd: string[]) => {
- capturedCmd = cmd;
- return createFakeChild().handle;
- };
-
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- interval: 500,
- spawn,
- logger: createFakeLogger() as never,
- });
- supervisor.start();
-
- expect(capturedCmd).toContain("--interval");
- expect(capturedCmd).toContain("500");
- });
-
- it("unexpected child exit respawns the collector", async () => {
- const children: FakeChild[] = [];
- let spawnCount = 0;
- const spawn = () => {
- spawnCount++;
- const child = createFakeChild();
- children.push(child);
- return child.handle;
- };
-
- const time = 0;
- const now = () => time;
- const delayResolvers: Array<() => void> = [];
- const delay = (_ms: number) =>
- new Promise<void>((r) => {
- delayResolvers.push(r);
- });
-
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- spawn,
- logger: createFakeLogger() as never,
- now,
- delay,
- });
- supervisor.start();
-
- expect(spawnCount).toBe(1);
-
- // Simulate unexpected exit
- children[0]?.resolveExit(1);
- await Promise.resolve();
- await Promise.resolve();
-
- // Trigger the backoff delay resolver
- expect(delayResolvers.length).toBe(1);
- delayResolvers[0]?.();
- await Promise.resolve();
- await Promise.resolve();
-
- expect(spawnCount).toBe(2);
- });
-
- it("restart guard caps respawns in a tight loop", async () => {
- const children: FakeChild[] = [];
- let spawnCount = 0;
- const spawn = () => {
- spawnCount++;
- const child = createFakeChild();
- children.push(child);
- return child.handle;
- };
-
- const time = 0;
- const now = () => time;
- const delayResolvers: Array<() => void> = [];
- const delay = (_ms: number) =>
- new Promise<void>((r) => {
- delayResolvers.push(r);
- });
-
- const logger = createFakeLogger();
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- spawn,
- logger: logger as never,
- now,
- delay,
- });
- supervisor.start();
-
- // Simulate rapid crashes (within the restart window)
- for (let i = 0; i < 5; i++) {
- children[i]?.resolveExit(1);
- await Promise.resolve();
- await Promise.resolve();
- if (delayResolvers.length > i) {
- delayResolvers[i]?.();
- await Promise.resolve();
- await Promise.resolve();
- }
- }
-
- // Should have spawned 6 times (1 initial + 5 restarts)
- expect(spawnCount).toBe(6);
-
- // 6th child also dies — should NOT respawn (cap reached)
- children[5]?.resolveExit(1);
- await Promise.resolve();
- await Promise.resolve();
-
- // spawnCount should still be 6
- expect(spawnCount).toBe(6);
- expect(logger.msgs.some((m) => m.msg === "Collector restart cap reached; giving up")).toBe(
- true,
- );
- });
-
- it("stop() sends SIGTERM and does not respawn", async () => {
- const child = createFakeChild();
- const spawn = () => child.handle;
-
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- spawn,
- logger: createFakeLogger() as never,
- });
- supervisor.start();
-
- // Resolve exit after SIGTERM (simulating graceful shutdown)
- const stopPromise = supervisor.stop();
- child.resolveExit(0);
- await stopPromise;
-
- expect(child.signals).toContain("SIGTERM");
- });
-
- it("stop() sends SIGKILL when child does not exit in time", async () => {
- const child = createFakeChild();
- const spawn = () => child.handle;
-
- const time = 0;
- const now = () => time;
- const delayResolvers: Array<() => void> = [];
- const delay = (_ms: number) =>
- new Promise<void>((r) => {
- delayResolvers.push(r);
- });
-
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- spawn,
- logger: createFakeLogger() as never,
- now,
- delay,
- });
- supervisor.start();
-
- const stopPromise = supervisor.stop();
-
- // Don't resolve exit — simulate hung child
- // Resolve the timeout delay instead
- expect(delayResolvers.length).toBe(1);
- delayResolvers[0]?.();
- await stopPromise;
-
- expect(child.signals).toContain("SIGTERM");
- expect(child.signals).toContain("SIGKILL");
- });
-
- it("stop() does not respawn after unexpected exit during stop", async () => {
- const children: FakeChild[] = [];
- let spawnCount = 0;
- const spawn = () => {
- spawnCount++;
- const child = createFakeChild();
- children.push(child);
- return child.handle;
- };
-
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- spawn,
- logger: createFakeLogger() as never,
- });
- supervisor.start();
- expect(spawnCount).toBe(1);
-
- // Child exits during stop — supervisor is already stopping
- const stopPromise = supervisor.stop();
- children[0]?.resolveExit(1);
- await stopPromise;
-
- // Should NOT have respawned
- expect(spawnCount).toBe(1);
- });
-
- it("spawn throwing does not throw to caller", () => {
- const spawn = () => {
- throw new Error("spawn failed");
- };
-
- const logger = createFakeLogger();
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- spawn,
- logger: logger as never,
- });
-
- expect(() => supervisor.start()).not.toThrow();
- expect(logger.msgs.some((m) => m.msg === "Failed to spawn collector")).toBe(true);
- });
-
- it("stop() is safe to call when no child was started", async () => {
- const spawn = () => createFakeChild().handle;
- const supervisor = createCollectorSupervisor({
- ...DEFAULTS,
- spawn,
- logger: createFakeLogger() as never,
- });
-
- await expect(supervisor.stop()).resolves.toBeUndefined();
- });
+ const DEFAULTS = {
+ journalPath: "/tmp/journal.ndjson",
+ dbPath: "/tmp/traces.db",
+ };
+
+ it("start() spawns with the correct command and args", () => {
+ let capturedCmd: string[] = [];
+ const children: FakeChild[] = [];
+ const spawn = (cmd: string[]) => {
+ capturedCmd = cmd;
+ const child = createFakeChild();
+ children.push(child);
+ return child.handle;
+ };
+
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ spawn,
+ logger: createFakeLogger() as never,
+ });
+ supervisor.start();
+
+ expect(capturedCmd).toEqual([
+ "bun",
+ "packages/observability-collector/src/main.ts",
+ "--journal",
+ "/tmp/journal.ndjson",
+ "--db",
+ "/tmp/traces.db",
+ ]);
+ });
+
+ it("start() passes --interval when provided", () => {
+ let capturedCmd: string[] = [];
+ const spawn = (cmd: string[]) => {
+ capturedCmd = cmd;
+ return createFakeChild().handle;
+ };
+
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ interval: 500,
+ spawn,
+ logger: createFakeLogger() as never,
+ });
+ supervisor.start();
+
+ expect(capturedCmd).toContain("--interval");
+ expect(capturedCmd).toContain("500");
+ });
+
+ it("unexpected child exit respawns the collector", async () => {
+ const children: FakeChild[] = [];
+ let spawnCount = 0;
+ const spawn = () => {
+ spawnCount++;
+ const child = createFakeChild();
+ children.push(child);
+ return child.handle;
+ };
+
+ const time = 0;
+ const now = () => time;
+ const delayResolvers: Array<() => void> = [];
+ const delay = (_ms: number) =>
+ new Promise<void>((r) => {
+ delayResolvers.push(r);
+ });
+
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ spawn,
+ logger: createFakeLogger() as never,
+ now,
+ delay,
+ });
+ supervisor.start();
+
+ expect(spawnCount).toBe(1);
+
+ // Simulate unexpected exit
+ children[0]?.resolveExit(1);
+ await Promise.resolve();
+ await Promise.resolve();
+
+ // Trigger the backoff delay resolver
+ expect(delayResolvers.length).toBe(1);
+ delayResolvers[0]?.();
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(spawnCount).toBe(2);
+ });
+
+ it("restart guard caps respawns in a tight loop", async () => {
+ const children: FakeChild[] = [];
+ let spawnCount = 0;
+ const spawn = () => {
+ spawnCount++;
+ const child = createFakeChild();
+ children.push(child);
+ return child.handle;
+ };
+
+ const time = 0;
+ const now = () => time;
+ const delayResolvers: Array<() => void> = [];
+ const delay = (_ms: number) =>
+ new Promise<void>((r) => {
+ delayResolvers.push(r);
+ });
+
+ const logger = createFakeLogger();
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ spawn,
+ logger: logger as never,
+ now,
+ delay,
+ });
+ supervisor.start();
+
+ // Simulate rapid crashes (within the restart window)
+ for (let i = 0; i < 5; i++) {
+ children[i]?.resolveExit(1);
+ await Promise.resolve();
+ await Promise.resolve();
+ if (delayResolvers.length > i) {
+ delayResolvers[i]?.();
+ await Promise.resolve();
+ await Promise.resolve();
+ }
+ }
+
+ // Should have spawned 6 times (1 initial + 5 restarts)
+ expect(spawnCount).toBe(6);
+
+ // 6th child also dies — should NOT respawn (cap reached)
+ children[5]?.resolveExit(1);
+ await Promise.resolve();
+ await Promise.resolve();
+
+ // spawnCount should still be 6
+ expect(spawnCount).toBe(6);
+ expect(logger.msgs.some((m) => m.msg === "Collector restart cap reached; giving up")).toBe(
+ true,
+ );
+ });
+
+ it("stop() sends SIGTERM and does not respawn", async () => {
+ const child = createFakeChild();
+ const spawn = () => child.handle;
+
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ spawn,
+ logger: createFakeLogger() as never,
+ });
+ supervisor.start();
+
+ // Resolve exit after SIGTERM (simulating graceful shutdown)
+ const stopPromise = supervisor.stop();
+ child.resolveExit(0);
+ await stopPromise;
+
+ expect(child.signals).toContain("SIGTERM");
+ });
+
+ it("stop() sends SIGKILL when child does not exit in time", async () => {
+ const child = createFakeChild();
+ const spawn = () => child.handle;
+
+ const time = 0;
+ const now = () => time;
+ const delayResolvers: Array<() => void> = [];
+ const delay = (_ms: number) =>
+ new Promise<void>((r) => {
+ delayResolvers.push(r);
+ });
+
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ spawn,
+ logger: createFakeLogger() as never,
+ now,
+ delay,
+ });
+ supervisor.start();
+
+ const stopPromise = supervisor.stop();
+
+ // Don't resolve exit — simulate hung child
+ // Resolve the timeout delay instead
+ expect(delayResolvers.length).toBe(1);
+ delayResolvers[0]?.();
+ await stopPromise;
+
+ expect(child.signals).toContain("SIGTERM");
+ expect(child.signals).toContain("SIGKILL");
+ });
+
+ it("stop() does not respawn after unexpected exit during stop", async () => {
+ const children: FakeChild[] = [];
+ let spawnCount = 0;
+ const spawn = () => {
+ spawnCount++;
+ const child = createFakeChild();
+ children.push(child);
+ return child.handle;
+ };
+
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ spawn,
+ logger: createFakeLogger() as never,
+ });
+ supervisor.start();
+ expect(spawnCount).toBe(1);
+
+ // Child exits during stop — supervisor is already stopping
+ const stopPromise = supervisor.stop();
+ children[0]?.resolveExit(1);
+ await stopPromise;
+
+ // Should NOT have respawned
+ expect(spawnCount).toBe(1);
+ });
+
+ it("spawn throwing does not throw to caller", () => {
+ const spawn = () => {
+ throw new Error("spawn failed");
+ };
+
+ const logger = createFakeLogger();
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ spawn,
+ logger: logger as never,
+ });
+
+ expect(() => supervisor.start()).not.toThrow();
+ expect(logger.msgs.some((m) => m.msg === "Failed to spawn collector")).toBe(true);
+ });
+
+ it("stop() is safe to call when no child was started", async () => {
+ const spawn = () => createFakeChild().handle;
+ const supervisor = createCollectorSupervisor({
+ ...DEFAULTS,
+ spawn,
+ logger: createFakeLogger() as never,
+ });
+
+ await expect(supervisor.stop()).resolves.toBeUndefined();
+ });
});