fix: do not count normal dispatcher exits as crashes

The close handler called recordCrash() unconditionally, even for exit
code 0 (normal MAX_REQUESTS restart). After 5 normal cycles within 60s
the dispatcher was permanently disabled. Now only non-zero exits count.
This commit is contained in:
SnapOtter
2026-04-30 18:45:55 +08:00
parent 67fa302376
commit 6d5d0a3673
2 changed files with 57 additions and 2 deletions
+3 -1
View File
@@ -216,12 +216,14 @@ function startDispatcher(): ChildProcess | null {
dispatcherReady = false;
});
child.on("close", () => {
child.on("close", (code) => {
for (const [id, req] of pendingRequests.entries()) {
req.reject(new Error("Python dispatcher exited unexpectedly"));
pendingRequests.delete(id);
}
if (code !== 0) {
recordCrash();
}
dispatcher = null;
dispatcherReady = false;
});
+53
View File
@@ -1035,6 +1035,59 @@ describe("bridge - dispatcher lifecycle via runPythonWithProgress", () => {
// The important thing is no crash -- the line is collected for pending requests
});
it("does not count exit code 0 as a crash (normal MAX_REQUESTS restart)", async () => {
const mockDispatcher = createMockProcess();
const mockPerReq = createMockProcess();
let callCount = 0;
vi.mocked(spawn).mockImplementation(() => {
callCount++;
if (callCount === 1) return mockDispatcher.process;
return mockPerReq.process;
});
const promise = runPythonWithProgress("test.py", []);
// Dispatcher exits with code 0 (normal MAX_REQUESTS shutdown)
mockDispatcher.emitEvent("close", 0, null);
await new Promise((r) => setTimeout(r, 10));
mockPerReq.stdout.emit("data", Buffer.from('{"ok": true}\n'));
mockPerReq.emitEvent("close", 0, null);
await promise;
const status = getDispatcherStatus();
expect(status.consecutiveCrashes).toBe(0);
expect(status.failed).toBe(false);
});
it("still counts non-zero exit codes as crashes", async () => {
const mockDispatcher = createMockProcess();
const mockPerReq = createMockProcess();
let callCount = 0;
vi.mocked(spawn).mockImplementation(() => {
callCount++;
if (callCount === 1) return mockDispatcher.process;
return mockPerReq.process;
});
const promise = runPythonWithProgress("test.py", []);
// Dispatcher exits with code 1 (real crash)
mockDispatcher.emitEvent("close", 1, null);
await new Promise((r) => setTimeout(r, 10));
mockPerReq.stdout.emit("data", Buffer.from('{"ok": true}\n'));
mockPerReq.emitEvent("close", 0, null);
await promise;
const status = getDispatcherStatus();
expect(status.consecutiveCrashes).toBeGreaterThanOrEqual(1);
});
it("per-request fallback retries with python3 when venv python fails with ENOENT", async () => {
const mockDispatcher = createMockProcess();
const mockVenvPython = createMockProcess();