From 0c304304519310bc1ceaac9abb5887f19125433c Mon Sep 17 00:00:00 2001 From: wangbill Date: Tue, 7 Jul 2026 14:43:05 -0700 Subject: [PATCH] fix: clear stale in-memory purge queue entries InMemory purge removed instance state but left queued IDs in orchestrationQueue and orchestrationQueueSet. Clear both structures during purge so re-created instances with the same ID can enqueue and process. Fixes #249 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/testing/in-memory-backend.ts | 5 +++ .../test/in-memory-backend.spec.ts | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/durabletask-js/src/testing/in-memory-backend.ts b/packages/durabletask-js/src/testing/in-memory-backend.ts index 476f020..a343f6c 100644 --- a/packages/durabletask-js/src/testing/in-memory-backend.ts +++ b/packages/durabletask-js/src/testing/in-memory-backend.ts @@ -243,6 +243,11 @@ export class InMemoryOrchestrationBackend { } this.instances.delete(instanceId); + this.orchestrationQueueSet.delete(instanceId); + const queueIndex = this.orchestrationQueue.indexOf(instanceId); + if (queueIndex >= 0) { + this.orchestrationQueue.splice(queueIndex, 1); + } this.stateWaiters.delete(instanceId); this.cancelInstanceTimers(instanceId); return true; diff --git a/packages/durabletask-js/test/in-memory-backend.spec.ts b/packages/durabletask-js/test/in-memory-backend.spec.ts index 381fcde..faa0920 100644 --- a/packages/durabletask-js/test/in-memory-backend.spec.ts +++ b/packages/durabletask-js/test/in-memory-backend.spec.ts @@ -513,6 +513,37 @@ describe("In-Memory Backend", () => { expect(state).toBeUndefined(); }); + it("should clear stale queued work when purging completed orchestrations", async () => { + const orchestrator: TOrchestrator = async (_: OrchestrationContext, input: number) => { + return input * 2; + }; + const instanceId = "purge-queue-cleanup-test-id"; + + worker.addOrchestrator(orchestrator); + await worker.start(); + + await client.scheduleNewOrchestration(orchestrator, 1, instanceId); + const initialState = await client.waitForOrchestrationCompletion(instanceId, true, 10); + expect(initialState?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED); + + await worker.stop(); + await client.raiseOrchestrationEvent(instanceId, "late-event", "ignored"); + expect((backend as any).orchestrationQueue).toContain(instanceId); + expect((backend as any).orchestrationQueueSet.has(instanceId)).toBe(true); + + const result = await client.purgeOrchestration(instanceId); + expect(result.deletedInstanceCount).toEqual(1); + expect((backend as any).orchestrationQueue).not.toContain(instanceId); + expect((backend as any).orchestrationQueueSet.has(instanceId)).toBe(false); + + await client.scheduleNewOrchestration(orchestrator, 21, instanceId); + await worker.start(); + + const recreatedState = await client.waitForOrchestrationCompletion(instanceId, true, 10); + expect(recreatedState?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED); + expect(recreatedState?.serializedOutput).toEqual(JSON.stringify(42)); + }); + it("should cancel pending timers when purging a terminated orchestration", async () => { const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any { // Create a timer far in the future — it will still be pending when we terminate