Repository: Azure/azure-functions-durable-js
Pull request #151 (2020-01-24 00:54:06)

Fix task.any() sorting and timer usage
A regression was introduced in 1.3.2 that causes Task.any() to never select Timers, as well as a bug in the new sorting logic introduced in 1.3.2. These two bugs in combination ensured that the test "Task.all() and Task.any(): Timer in combination with Task.any() executes deterministically" passed. A new test has been added to ensure this regression does not reappear. 


Patch:
diff --git a/src/orchestrator.ts b/src/orchestrator.ts
index 35c4196..14fb1a7 100644
--- a/src/orchestrator.ts
+++ b/src/orchestrator.ts
@@ -568,8 +568,8 @@ export class Orchestrator {
         const completedTasks = tasks
             .filter(TaskFilter.isSuccessfulSingleTask)
             .sort((a, b) => {
-                if (a.completedHistoryEventIndex > b.completedHistoryEventIndex) { return 1; }
-                if (a.completedHistoryEventIndex < b.completedHistoryEventIndex) { return -1; }
+                if (a.completionIndex > b.completionIndex) { return 1; }
+                if (a.completionIndex < b.completionIndex) { return -1; }
                 return 0;
             });
 
diff --git a/src/tasks/taskfilter.ts b/src/tasks/taskfilter.ts
index 306cec3..e2c00bb 100644
--- a/src/tasks/taskfilter.ts
+++ b/src/tasks/taskfilter.ts
@@ -33,7 +33,7 @@ export class TaskFilter {
     }
 
     public static isSuccessfulSingleTask(task: Task): task is SuccessfulSingleTask  {
-        return TaskFilter.isSingleTask(task) && task.isCompleted === true && task.isFaulted === false && task.result !== undefined;
+        return TaskFilter.isSingleTask(task) && task.isCompleted === true && task.isFaulted === false && task.completionIndex !== undefined;
     }
 
     public static isFailedTask(task: Task | TaskSet): task is FailedTask {
diff --git a/src/tasks/taskinterfaces.ts b/src/tasks/taskinterfaces.ts
index 2760013..41a7089 100644
--- a/src/tasks/taskinterfaces.ts
+++ b/src/tasks/taskinterfaces.ts
@@ -17,7 +17,7 @@ export interface UncompletedSingleTask extends TaskBase {
 }
 
 export interface CompletedSingleTask extends SingleTask {
-    completedHistoryEventIndex: number;
+    completionIndex: number;
     readonly timestamp: Date;
     readonly id: number;
     readonly isCompleted: true;


Test patch:
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index 297f38a..680a9e5 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -1940,6 +1940,59 @@ describe("Orchestrator", () => {
                 }),
             );
         });
+
+        it("Timer in combination with Task.any() timer wins", async () => {
+            const orchestrator = TestOrchestrations.TimerActivityRace;
+            const currentTime = moment.utc();
+
+            // first iteration
+            let mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetTimerActivityRaceTimerWinsHistory(
+                        currentTime.toDate(),
+                        1,
+                    ),
+                    null,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: false,
+                    actions:
+                    [
+                        [ new CreateTimerAction(currentTime.add(1, "s").toDate()), new CallActivityAction("TaskA") ],
+                    ],
+                    output: undefined,
+                }),
+            );
+
+            // second iteration
+            mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetTimerActivityRaceTimerWinsHistory(
+                        currentTime.toDate(),
+                        2,
+                    ),
+                    null,
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: true,
+                    actions:
+                    [
+                        [ new CreateTimerAction(currentTime.add(1, "s").toDate()), new CallActivityAction("TaskA") ],
+                    ],
+                    output: "Timer finished",
+                }),
+            );
+        });
     });
 
     // rewind
diff --git a/test/testobjects/testhistories.ts b/test/testobjects/testhistories.ts
index b78da39..90efefc 100644
--- a/test/testobjects/testhistories.ts
+++ b/test/testobjects/testhistories.ts
@@ -180,6 +180,61 @@ export class TestHistories {
         return history;
     }
 
+    public static GetTimerActivityRaceTimerWinsHistory(firstTimestamp: Date, iteration: number): HistoryEvent[] {
+        const firstIteration = moment(firstTimestamp);
+        const fireAt = firstIteration.add(1, "s").toDate();
+        const secondIteration = firstIteration.add(1100, "ms").toDate();
+
+        const history = [];
+        if (iteration >= 1) {
+            history.push(new OrchestratorStartedEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                isPlayed: false,
+            }));
+            history.push(new ExecutionStartedEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                isPlayed: true,
+                name: "TimerActivityRace",
+            }));
+            history.push(new TimerCreatedEvent({
+                eventId: 0,
+                timestamp: firstTimestamp,
+                isPlayed: false,
+                fireAt,
+            }));
+            history.push(new TaskScheduledEvent({
+                eventId: 1,
+                timestamp: firstTimestamp,
+                isPlayed: iteration > 1,
+                name: "TaskA",
+            }));
+            history.push(new OrchestratorCompletedEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                isPlayed: iteration > 1,
+            }));
+        }
+
+        if (iteration >= 2) {
+            history.push(new OrchestratorStartedEvent({
+                eventId: -1,
+                timestamp: secondIteration,
+                isPlayed: iteration > 2,
+            }));
+            history.push(new TimerFiredEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                fireAt,
+                isPlayed: iteration > 2,
+                timerId: 0,
+            }));
+        }
+
+        return history;
+    }
+
     public static GetCallEntitySet(firstTimestamp: Date, entityId: EntityId) {
         const firstMoment = moment(firstTimestamp);
         const orchestratorId = uuidv1();