Skip to content

Commit 29455dd

Browse files
authored
fix: Use strict undefined check for taskId in completion event handlers (#157)
1 parent 843c774 commit 29455dd

2 files changed

Lines changed: 100 additions & 2 deletions

File tree

packages/durabletask-js/src/worker/orchestration-executor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export class OrchestrationExecutor {
393393

394394
let subOrchTask;
395395

396-
if (taskId) {
396+
if (taskId !== undefined) {
397397
subOrchTask = ctx._pendingTasks[taskId];
398398
delete ctx._pendingTasks[taskId];
399399
}
@@ -694,7 +694,7 @@ export class OrchestrationExecutor {
694694
): Promise<void> {
695695
let task;
696696

697-
if (taskId) {
697+
if (taskId !== undefined) {
698698
task = ctx._pendingTasks[taskId];
699699
delete ctx._pendingTasks[taskId];
700700
}

packages/durabletask-js/test/orchestration_executor.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ActivityContext } from "../src/task/context/activity-context";
2828
import { CompletableTask } from "../src/task/completable-task";
2929
import { Task } from "../src/task/task";
3030
import { getName, whenAll, whenAny } from "../src/task";
31+
import { RuntimeOrchestrationContext } from "../src/worker/runtime-orchestration-context";
3132

3233
// Use NoOpLogger to suppress log output during tests
3334
const testLogger = new NoOpLogger();
@@ -246,6 +247,103 @@ describe("Orchestration Executor", () => {
246247
// const userCodeStatement = "ctx.callActivity(dummyActivity, orchestratorInput)";
247248
// expect(completeAction?.getFailuredetails()?.getStacktrace()?.getValue()).toContain(userCodeStatement);
248249
});
250+
it("should handle a task completion event with an unmatched taskScheduledId without error", async () => {
251+
const dummyActivity = async (_: ActivityContext) => {
252+
// do nothing
253+
};
254+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, _: any): any {
255+
const result = yield ctx.callActivity(dummyActivity);
256+
return result;
257+
};
258+
const registry = new Registry();
259+
const name = registry.addOrchestrator(orchestrator);
260+
const oldEvents = [
261+
newOrchestratorStartedEvent(),
262+
newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined),
263+
newTaskScheduledEvent(1, dummyActivity.name),
264+
];
265+
// Send a completion event with a non-matching taskScheduledId (999)
266+
const newEvents = [newTaskCompletedEvent(999, JSON.stringify("result"))];
267+
const executor = new OrchestrationExecutor(registry, testLogger);
268+
269+
// Should not throw — the unmatched event is logged and the orchestration continues waiting
270+
const result = await executor.execute(TEST_INSTANCE_ID, oldEvents, newEvents);
271+
// Orchestration should still be waiting (task at id 1 was not completed)
272+
expect(result.actions.length).toEqual(0);
273+
});
274+
275+
it("should handle a task completion event with taskScheduledId 0 by looking up the task (not skipping due to falsy 0)", async () => {
276+
// This test validates the fix for issue #148: the old code used `if (taskId)` which
277+
// treated taskId === 0 as falsy and skipped the lookup entirely. The fix uses
278+
// `if (taskId !== undefined)` so that 0 is properly looked up.
279+
const dummyActivity = async (_: ActivityContext) => {
280+
// do nothing
281+
};
282+
const injectedTask = new CompletableTask<string>();
283+
284+
// Use an orchestrator that injects a CompletableTask at _pendingTasks[0]
285+
// to simulate a task with taskScheduledId = 0
286+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, _: any): any {
287+
const runtimeCtx = ctx as unknown as RuntimeOrchestrationContext;
288+
runtimeCtx._pendingTasks[0] = injectedTask;
289+
const result = yield ctx.callActivity(dummyActivity);
290+
return result;
291+
};
292+
const registry = new Registry();
293+
const name = registry.addOrchestrator(orchestrator);
294+
registry.addActivity(dummyActivity);
295+
const oldEvents = [
296+
newOrchestratorStartedEvent(),
297+
newExecutionStartedEvent(name, TEST_INSTANCE_ID, undefined),
298+
newTaskScheduledEvent(1, dummyActivity.name),
299+
];
300+
// Send completion for taskId 0 — with the fix, this completes the injected task
301+
const newEvents = [newTaskCompletedEvent(0, JSON.stringify("result-for-zero"))];
302+
const executor = new OrchestrationExecutor(registry, testLogger);
303+
304+
await executor.execute(TEST_INSTANCE_ID, oldEvents, newEvents);
305+
306+
// With the fix (taskId !== undefined): the lookup at _pendingTasks[0] finds the injected
307+
// task and completes it. With the old code (if (taskId)): 0 is falsy, the lookup is
308+
// skipped, and the task is never completed.
309+
expect(injectedTask.isComplete).toBe(true);
310+
expect(injectedTask.getResult()).toEqual("result-for-zero");
311+
});
312+
313+
it("should handle a sub-orchestration completion event with taskScheduledId 0 by looking up the task", async () => {
314+
// Same regression test as above but for handleSubOrchestrationCompleted
315+
const subOrchestrator = async (_: OrchestrationContext) => {
316+
// do nothing
317+
};
318+
const injectedTask = new CompletableTask<string>();
319+
320+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, _: any): any {
321+
const runtimeCtx = ctx as unknown as RuntimeOrchestrationContext;
322+
// Inject a CompletableTask at ID 0
323+
runtimeCtx._pendingTasks[0] = injectedTask;
324+
const res = yield ctx.callSubOrchestrator(subOrchestrator);
325+
return res;
326+
};
327+
const registry = new Registry();
328+
const subOrchestratorName = registry.addOrchestrator(subOrchestrator);
329+
const orchestratorName = registry.addOrchestrator(orchestrator);
330+
const oldEvents = [
331+
newOrchestratorStartedEvent(),
332+
newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID, undefined),
333+
newSubOrchestrationCreatedEvent(1, subOrchestratorName, "sub-orch-123"),
334+
];
335+
// Send completion for taskId 0
336+
const newEvents = [newSubOrchestrationCompletedEvent(0, JSON.stringify("sub-result-zero"))];
337+
const executor = new OrchestrationExecutor(registry, testLogger);
338+
339+
await executor.execute(TEST_INSTANCE_ID, oldEvents, newEvents);
340+
341+
// With the fix: the lookup at _pendingTasks[0] finds the injected task and completes it.
342+
// With the old code: 0 is falsy, lookup is skipped, task never completed.
343+
expect(injectedTask.isComplete).toBe(true);
344+
expect(injectedTask.getResult()).toEqual("sub-result-zero");
345+
});
346+
249347
it("should test the non-determinism detection logic when callTimer is expected but some other method (callActivity) is called instead", async () => {
250348
const dummyActivity = async (_: ActivityContext) => {
251349
// do nothing

0 commit comments

Comments
 (0)