Skip to content

Commit af38564

Browse files
authored
[copilot-finds] Bug: Fix whenAll([]) hanging orchestration forever (#118)
1 parent bdcf7e3 commit af38564

4 files changed

Lines changed: 66 additions & 0 deletions

File tree

packages/durabletask-js/src/task/when-all-task.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ export class WhenAllTask<T> extends CompositeTask<T[]> {
1313

1414
this._completedTasks = 0;
1515
this._failedTasks = 0;
16+
17+
// An empty task list should complete immediately with an empty result
18+
if (tasks.length === 0) {
19+
this._result = [] as T[];
20+
this._isComplete = true;
21+
}
1622
}
1723

1824
pendingTasks(): number {

packages/durabletask-js/src/task/when-any-task.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { Task } from "./task";
99
*/
1010
export class WhenAnyTask extends CompositeTask<Task<any>> {
1111
constructor(tasks: Task<any>[]) {
12+
if (tasks.length === 0) {
13+
throw new Error("whenAny requires at least one task");
14+
}
1215
super(tasks);
1316
}
1417

packages/durabletask-js/src/worker/runtime-orchestration-context.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ export class RuntimeOrchestrationContext extends OrchestrationContext {
121121

122122
// TODO: check if the task is null?
123123
this._previousTask = value;
124+
125+
// If the yielded task is already complete (e.g., whenAll with an empty array),
126+
// resume immediately so the generator can continue.
127+
if (this._previousTask instanceof Task && this._previousTask.isComplete) {
128+
await this.resume();
129+
}
124130
}
125131

126132
async resume() {

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,57 @@ describe("Orchestration Executor", () => {
11281128
expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED);
11291129
});
11301130
});
1131+
1132+
it("should complete immediately when whenAll is called with an empty task array", async () => {
1133+
const orchestrator: TOrchestrator = async function* (_ctx: OrchestrationContext): any {
1134+
const results = yield whenAll([]);
1135+
return results;
1136+
};
1137+
1138+
const registry = new Registry();
1139+
const orchestratorName = registry.addOrchestrator(orchestrator);
1140+
1141+
const oldEvents: any[] = [];
1142+
const newEvents = [newOrchestratorStartedEvent(), newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID)];
1143+
1144+
const executor = new OrchestrationExecutor(registry, testLogger);
1145+
const result = await executor.execute(TEST_INSTANCE_ID, oldEvents, newEvents);
1146+
1147+
// The orchestration should complete immediately with an empty array result
1148+
const completeAction = getAndValidateSingleCompleteOrchestrationAction(result);
1149+
expect(completeAction?.getOrchestrationstatus()).toEqual(pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED);
1150+
expect(completeAction?.getResult()?.getValue()).toEqual(JSON.stringify([]));
1151+
});
1152+
1153+
it("should complete when whenAll with empty array is followed by more work", async () => {
1154+
const hello = (_: any, name: string) => `Hello ${name}!`;
1155+
1156+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
1157+
const emptyResults = yield whenAll([]);
1158+
const activityResult = yield ctx.callActivity(hello, "World");
1159+
return { emptyResults, activityResult };
1160+
};
1161+
1162+
const registry = new Registry();
1163+
const orchestratorName = registry.addOrchestrator(orchestrator);
1164+
const activityName = registry.addActivity(hello);
1165+
1166+
// First execution: should schedule the activity after completing whenAll([])
1167+
const oldEvents: any[] = [];
1168+
const newEvents = [newOrchestratorStartedEvent(), newExecutionStartedEvent(orchestratorName, TEST_INSTANCE_ID)];
1169+
1170+
const executor = new OrchestrationExecutor(registry, testLogger);
1171+
const result = await executor.execute(TEST_INSTANCE_ID, oldEvents, newEvents);
1172+
1173+
// The whenAll([]) should complete, then an activity should be scheduled
1174+
expect(result.actions.length).toEqual(1);
1175+
expect(result.actions[0].hasScheduletask()).toBeTruthy();
1176+
expect(result.actions[0].getScheduletask()?.getName()).toEqual(activityName);
1177+
});
1178+
1179+
it("should throw when whenAny is called with an empty task array", () => {
1180+
expect(() => whenAny([])).toThrow("whenAny requires at least one task");
1181+
});
11311182
});
11321183

11331184
function getAndValidateSingleCompleteOrchestrationAction(

0 commit comments

Comments
 (0)