Files
regex-tools/src/regex/execution/EngineSupervisor.test.ts

275 lines
8.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type {
RegexExecutionRequest,
RegexExecutionResult,
} from "../model/match";
import type { RegexFlavourId } from "../model/flavour";
import { EngineSupervisor } from "./EngineSupervisor";
import {
ENGINE_WORKER_REGISTRY,
EngineWorkerRegistry,
} from "./engine-registry";
import type { WorkerLike } from "./WorkerSupervisor";
import {
WORKER_PROTOCOL_VERSION,
type EngineWorkerOperation,
type EngineWorkerResult,
type WorkerRequest,
type WorkerResponse,
} from "./worker-protocol";
function request(flavour: RegexFlavourId): RegexExecutionRequest {
return {
flavour,
flavourVersion: "fixture",
pattern: "a",
flags: [],
options: {},
subject: "a",
captureMetadata: [],
scanAll: false,
maximumMatches: 10,
maximumCaptureRows: 10,
};
}
function result(flavour: RegexFlavourId): RegexExecutionResult {
return {
accepted: true,
engine: {
flavour,
adapterVersion: "fixture",
engineName: `${flavour} fixture`,
engineVersion: "fixture",
offsetUnit: "utf16",
capabilities: {
compilation: true,
matching: true,
replacement: false,
namedCaptures: false,
captureHistory: false,
actualTrace: false,
benchmark: false,
},
},
flags: {
userFlags: "",
effectiveFlags: "",
internallyAddedIndicesFlag: false,
internallyAddedGlobalFlag: false,
},
matches: [],
diagnostics: [],
elapsedMs: 0,
truncated: false,
};
}
class ResponsiveWorker implements WorkerLike {
onmessage: ((event: MessageEvent<unknown>) => void) | null = null;
onerror: ((event: ErrorEvent) => void) | null = null;
onmessageerror: ((event: MessageEvent<unknown>) => void) | null = null;
readonly operations: EngineWorkerOperation[] = [];
terminateCalls = 0;
private readonly flavour: RegexFlavourId;
constructor(flavour: RegexFlavourId) {
this.flavour = flavour;
}
postMessage(message: unknown): void {
const request = message as WorkerRequest<EngineWorkerOperation>;
this.operations.push(request.payload);
const payload: EngineWorkerResult =
request.payload.kind === "load"
? {
kind: "load",
info: result(this.flavour).engine,
}
: request.payload.kind === "execute"
? {
kind: "execute",
result: result(request.payload.request.flavour),
}
: {
kind: "replace",
result: {
execution: result(request.payload.request.flavour),
output: "",
outputBytes: 0,
outputTruncated: false,
truncated: false,
},
};
const response: WorkerResponse<EngineWorkerResult> = {
protocolVersion: WORKER_PROTOCOL_VERSION,
requestId: request.requestId,
generation: request.generation,
ok: true,
payload,
};
this.onmessage?.({ data: response } as MessageEvent<unknown>);
}
terminate(): void {
this.terminateCalls += 1;
}
}
class StallingWorker implements WorkerLike {
onmessage: ((event: MessageEvent<unknown>) => void) | null = null;
onerror: ((event: ErrorEvent) => void) | null = null;
onmessageerror: ((event: MessageEvent<unknown>) => void) | null = null;
readonly operations: EngineWorkerOperation[] = [];
terminateCalls = 0;
postMessage(message: unknown): void {
const request = message as WorkerRequest<EngineWorkerOperation>;
this.operations.push(request.payload);
}
terminate(): void {
this.terminateCalls += 1;
}
}
describe("engine worker selection", () => {
it("registers every shipped execution worker with a startup budget", () => {
expect(
ENGINE_WORKER_REGISTRY.registrations.map(
(registration) => registration.flavour,
),
).toEqual(["ecmascript", "pcre2", "python", "java"]);
expect(ENGINE_WORKER_REGISTRY.require("pcre2").label).toBe("PCRE2 engine");
expect(ENGINE_WORKER_REGISTRY.require("python").startupTimeoutMs).toBe(
30_000,
);
});
it("selects and reuses a dedicated supervisor per registered flavour", async () => {
const workers = new Map<RegexFlavourId, ResponsiveWorker[]>();
const registration = (flavour: RegexFlavourId) => ({
flavour,
label: `${flavour} fixture`,
startupTimeoutMs: 100,
createWorker: () => {
const worker = new ResponsiveWorker(flavour);
workers.set(flavour, [...(workers.get(flavour) ?? []), worker]);
return worker;
},
});
const supervisor = new EngineSupervisor(
new EngineWorkerRegistry([
registration("ecmascript"),
registration("pcre2"),
]),
);
await expect(
supervisor.execute(request("ecmascript"), 100),
).resolves.toEqual(
expect.objectContaining({
engine: expect.objectContaining({ flavour: "ecmascript" }),
}),
);
await expect(supervisor.execute(request("pcre2"), 100)).resolves.toEqual(
expect.objectContaining({
engine: expect.objectContaining({ flavour: "pcre2" }),
}),
);
await supervisor.execute(request("ecmascript"), 100);
expect(workers.get("ecmascript")).toHaveLength(1);
expect(workers.get("pcre2")).toHaveLength(1);
expect(
workers
.get("ecmascript")?.[0]
?.operations.map((operation) => operation.kind),
).toEqual(["load", "execute", "execute"]);
expect(
workers.get("pcre2")?.[0]?.operations.map((operation) => operation.kind),
).toEqual(["load", "execute"]);
supervisor.dispose();
expect(workers.get("ecmascript")?.[0]?.terminateCalls).toBe(1);
expect(workers.get("pcre2")?.[0]?.terminateCalls).toBe(1);
});
it("fails closed before constructing a worker for an unavailable flavour", async () => {
const supervisor = new EngineSupervisor(new EngineWorkerRegistry([]));
await expect(supervisor.execute(request("pcre2"), 100)).rejects.toThrow(
/No execution worker is registered/u,
);
supervisor.dispose();
});
it("lets the newest request supersede a shared ready-engine continuation", async () => {
const worker = new ResponsiveWorker("python");
const supervisor = new EngineSupervisor(
new EngineWorkerRegistry([
{
flavour: "python",
label: "Python fixture",
startupTimeoutMs: 100,
createWorker: () => worker,
},
]),
);
await supervisor.load("python");
const older = supervisor.execute(request("python"), 100);
const newer = supervisor.execute(request("python"), 100);
await expect(older).rejects.toMatchObject({ kind: "cancelled" });
await expect(newer).resolves.toEqual(
expect.objectContaining({
engine: expect.objectContaining({ flavour: "python" }),
}),
);
expect(worker.operations.map((operation) => operation.kind)).toEqual([
"load",
"execute",
]);
supervisor.dispose();
});
it("restarts a superseded cold runtime under the startup budget before execution", async () => {
const coldWorker = new StallingWorker();
const readyWorker = new ResponsiveWorker("python");
let factoryCalls = 0;
const supervisor = new EngineSupervisor(
new EngineWorkerRegistry([
{
flavour: "python",
label: "Python fixture",
startupTimeoutMs: 100,
createWorker: () => {
factoryCalls += 1;
return factoryCalls === 1 ? coldWorker : readyWorker;
},
},
]),
);
const older = supervisor.execute(request("python"), 1);
const newer = supervisor.execute(request("python"), 1);
await expect(older).rejects.toMatchObject({ kind: "cancelled" });
await expect(newer).resolves.toEqual(
expect.objectContaining({
engine: expect.objectContaining({ flavour: "python" }),
}),
);
expect(coldWorker.operations.map((operation) => operation.kind)).toEqual([
"load",
]);
expect(coldWorker.terminateCalls).toBe(1);
expect(readyWorker.operations.map((operation) => operation.kind)).toEqual([
"load",
"execute",
]);
supervisor.dispose();
});
});