fix: reset stale execution state after SIGUSR1 in-process restart (#15195)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 676f9ec45135be0d3471bb0444bc2ac7ce7d5224
Co-authored-by: joeykrug <5925937+joeykrug@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Joseph Krug
2026-02-13 16:30:09 -04:00
committed by GitHub
parent 2086cdfb9b
commit 4e9f933e88
11 changed files with 572 additions and 20 deletions

View File

@@ -23,6 +23,7 @@ import {
enqueueCommandInLane,
getActiveTaskCount,
getQueueSize,
resetAllLanes,
setCommandLaneConcurrency,
waitForActiveTasks,
} from "./command-queue.js";
@@ -36,6 +37,12 @@ describe("command queue", () => {
diagnosticMocks.diag.error.mockClear();
});
it("resetAllLanes is safe when no lanes have been created", () => {
expect(getActiveTaskCount()).toBe(0);
expect(() => resetAllLanes()).not.toThrow();
expect(getActiveTaskCount()).toBe(0);
});
it("runs tasks one at a time in order", async () => {
let active = 0;
let maxActive = 0;
@@ -162,6 +169,49 @@ describe("command queue", () => {
await task;
});
it("resetAllLanes drains queued work immediately after reset", async () => {
const lane = `reset-test-${Date.now()}-${Math.random().toString(16).slice(2)}`;
setCommandLaneConcurrency(lane, 1);
let resolve1!: () => void;
const blocker = new Promise<void>((r) => {
resolve1 = r;
});
// Start a task that blocks the lane
const task1 = enqueueCommandInLane(lane, async () => {
await blocker;
});
await vi.waitFor(() => {
expect(getActiveTaskCount()).toBeGreaterThanOrEqual(1);
});
// Enqueue another task — it should be stuck behind the blocker
let task2Ran = false;
const task2 = enqueueCommandInLane(lane, async () => {
task2Ran = true;
});
await vi.waitFor(() => {
expect(getQueueSize(lane)).toBeGreaterThanOrEqual(2);
});
expect(task2Ran).toBe(false);
// Simulate SIGUSR1: reset all lanes. Queued work (task2) should be
// drained immediately — no fresh enqueue needed.
resetAllLanes();
// Complete the stale in-flight task; generation mismatch makes its
// completion path a no-op for queue bookkeeping.
resolve1();
await task1;
// task2 should have been pumped by resetAllLanes's drain pass.
await task2;
expect(task2Ran).toBe(true);
});
it("waitForActiveTasks ignores tasks that start after the call", async () => {
const lane = `drain-snapshot-${Date.now()}-${Math.random().toString(16).slice(2)}`;
setCommandLaneConcurrency(lane, 2);