refactor(shutdown): coordinate worker drain handshake
This commit is contained in:
1 parent
9ee1425a71
commit
2ddc67cc3d
8 files changed
+286
-86
No files matched your search
@@ -285,4 +285,62 @@ describe("worker completion boundary", () => {
|
||||
expect.objectContaining({ type: "run-finished", runId: NEXT_RUN_ID }),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports shutdown-ready only after parent acknowledges run completion", async () => {
|
||||
const parentPort = new EventEmitter() as EventEmitter & {
|
||||
postMessage: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
parentPort.postMessage = vi.fn();
|
||||
Object.defineProperty(process, "parentPort", {
|
||||
configurable: true,
|
||||
value: parentPort,
|
||||
});
|
||||
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
|
||||
|
||||
vi.doMock("@main/worker/runtime", () => ({
|
||||
WorkerRunner: class TestWorkerRunner {
|
||||
constructor(
|
||||
private readonly deps: {
|
||||
post: (message: unknown) => void;
|
||||
},
|
||||
) {}
|
||||
|
||||
async run(job: RunJob): Promise<void> {
|
||||
this.deps.post(runFinishedMessage(job.runId));
|
||||
}
|
||||
|
||||
cancel(): void {}
|
||||
|
||||
steer(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async cancelAll(): Promise<void> {}
|
||||
},
|
||||
}));
|
||||
|
||||
await import("@main/worker/entry/index");
|
||||
|
||||
parentPort.emit("message", { data: { type: "enqueue-run", payload: runJob(RUN_ID) } });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
parentPort.emit("message", { data: { type: "shutdown" } });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(parentPort.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: "run-finished", runId: RUN_ID }),
|
||||
);
|
||||
expect(parentPort.postMessage).not.toHaveBeenCalledWith({ type: "shutdown-ready" });
|
||||
|
||||
parentPort.emit("message", { data: { type: "run-completion-ack", runId: RUN_ID } });
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(parentPort.postMessage).toHaveBeenCalledWith({ type: "shutdown-ready" });
|
||||
|
||||
parentPort.emit("message", { data: { type: "shutdown-exit" } });
|
||||
expect(exitSpy).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
forkMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
app: { isPackaged: false },
|
||||
utilityProcess: { fork: mocks.forkMock },
|
||||
}));
|
||||
|
||||
import { createWorkerService } from "../../src/main/worker-service";
|
||||
|
||||
function createWorker() {
|
||||
const worker = new EventEmitter() as EventEmitter & {
|
||||
stdout: EventEmitter;
|
||||
stderr: EventEmitter;
|
||||
postMessage: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
worker.stdout = new EventEmitter();
|
||||
worker.stderr = new EventEmitter();
|
||||
worker.postMessage = vi.fn();
|
||||
return worker;
|
||||
}
|
||||
|
||||
describe("worker service shutdown", () => {
|
||||
it("keeps completed run outcomes when cleaning shutdown runs", async () => {
|
||||
const worker = createWorker();
|
||||
mocks.forkMock.mockReturnValueOnce(worker);
|
||||
const finishRun = vi.fn();
|
||||
const db = {
|
||||
projection: {
|
||||
snapshot: {
|
||||
listSnapshot: vi.fn(() => ({
|
||||
runs: [
|
||||
{ id: "run-succeeded", status: "running", taskId: null, workspaceId: null },
|
||||
{ id: "run-queued", status: "queued", taskId: null, workspaceId: null },
|
||||
],
|
||||
})),
|
||||
},
|
||||
},
|
||||
core: {
|
||||
runs: {
|
||||
getRun: vi.fn((id: string) => ({
|
||||
id,
|
||||
status: id === "run-succeeded" ? "succeeded" : "queued",
|
||||
})),
|
||||
finishRun,
|
||||
},
|
||||
workspaces: { releaseWorkspaceLock: vi.fn() },
|
||||
tasks: { releaseTaskLock: vi.fn() },
|
||||
},
|
||||
};
|
||||
const service = createWorkerService({
|
||||
db,
|
||||
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
workerPath: "/tmp/worker.js",
|
||||
isQuitting: () => false,
|
||||
publishDomainChanged: vi.fn(),
|
||||
emitEvent: vi.fn(),
|
||||
wakeAgentIfPossible: vi.fn(() => null),
|
||||
dispatchRunToWorker: vi.fn(),
|
||||
processRunCompletion: vi.fn(),
|
||||
onRunStarted: vi.fn(),
|
||||
onRunCompleted: vi.fn(),
|
||||
notify: vi.fn(),
|
||||
budgetService: undefined,
|
||||
sessionService: undefined,
|
||||
getRuntimeBridge: vi.fn(() => null),
|
||||
getRuntimeMessageInjector: vi.fn(() => null),
|
||||
} as never);
|
||||
|
||||
service.createWorker();
|
||||
const shutdown = service.shutdown();
|
||||
worker.emit("exit", 0);
|
||||
await shutdown;
|
||||
|
||||
expect(worker.postMessage).toHaveBeenCalledWith({ type: "shutdown" });
|
||||
expect(finishRun).toHaveBeenCalledTimes(1);
|
||||
expect(finishRun).toHaveBeenCalledWith(expect.objectContaining({
|
||||
runId: "run-queued",
|
||||
status: "interrupted",
|
||||
}));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user