Repository: Azure/azure-functions-durable-js
Pull request #156 (2020-02-04 19:24:20)

Allow task sets in df.task.all and df.task.any
Unify the idea of `Task` and `TaskSet` so that `df.task.any()` and `df.task.all()` can work on the tasksets they generate themselves.

This cleaning of the two concepts also simplifies the code as well.

cgillum: Just an observation - no action necessary: I feel that the overall handling of tasks is unnecessarily complicated with our use of timestamps and completion indexes. At a high level, what we need to do in orchestrator.ts is similar to what [TaskOrchestrationExecutor](https://github.com/Azure/durabletask/blob/master/src/DurableTask.Core/TaskOrchestrationExecutor.cs) in the Durable Task Framework is doing, and that implementation is much simpler and more deterministic, IMO.
ConnorMcMahon: @cgillum 

I'm not opposed to refactoring an approach using a mapping of ids to tasks, and preprocessing the tasks. That being said, I don't think it gives us that much in this scenario. A lot of the complexity in `TaskOrchestrationExecutor` is hidden in `TaskOrchestrationContext`. It gets even more complex considering how closely `TaskOrchestrationContext` depends on the first-class support for `TaskCompletionSource`, an advantage we don't really have in JavaScript.

I also don't think we have any real way of implementing task.any() without keeping track of completion indices (or some other similar metadata), considering we don't have full Task support in JS.

Patch:
diff --git a/src/aggregatederror.ts b/src/aggregatederror.ts
new file mode 100644
index 0000000..ee41c78
--- /dev/null
+++ b/src/aggregatederror.ts
@@ -0,0 +1,27 @@
+const separator = "-----------------------------------";
+
+/**
+ * A specfic error thrown when context.df.Task.all() fails. Its message
+ * contains an aggregation of all the exceptions that failed. It should follow the
+ * below format:
+ *
+ * context.df.Task.all() encountered the below error messages:
+ *
+ * Name: DurableError
+ * Message: The activity function "ActivityA" failed.
+ * StackTrace: <stacktrace>
+ * -----------------------------------
+ * Name: DurableError
+ * Message: The activity function "ActivityB" failed.
+ * StackTrace: <stacktrace>
+ */
+export class AggregatedError extends Error {
+    public errors: Error[];
+
+    constructor(errors: Error[]) {
+        const errorStrings = errors.map((error, num) => `Name: ${error.name}\nMessage: ${error.message}\nStackTrace: ${error.stack}`);
+        const message = `context.df.Task.all() encountered the below error messages:\n\n${errorStrings.join(`\n${separator}\n`)}`;
+        super(message);
+        this.errors = errors;
+    }
+}
diff --git a/src/orchestrator.ts b/src/orchestrator.ts
index 6775625..ae3dd6f 100644
--- a/src/orchestrator.ts
+++ b/src/orchestrator.ts
@@ -1,4 +1,5 @@
 import * as debug from "debug";
+import { AggregatedError } from "./aggregatederror";
 import { CallActivityAction, CallActivityWithRetryAction, CallEntityAction, CallHttpAction,
     CallSubOrchestratorAction, CallSubOrchestratorWithRetryAction, ContinueAsNewAction,
     CreateTimerAction, DurableHttpRequest, DurableLock, DurableOrchestrationBindingInfo, EntityId,
@@ -11,7 +12,7 @@ import { CallActivityAction, CallActivityWithRetryAction, CallEntityAction, Call
 } from "./classes";
 import { DurableError } from "./durableerror";
 import { OrchestrationFailureError } from "./orchestrationfailureerror";
-import { FailedSingleTask } from "./tasks/taskinterfaces";
+import { CompletedTask, TaskBase } from "./tasks/taskinterfaces";
 import { TokenSource } from "./tokensource";
 
 /** @hidden */
@@ -82,7 +83,7 @@ export class Orchestrator {
         // Setup
         const gen = this.fn(context);
         const actions: IAction[][] = [];
-        let partialResult: Task | TaskSet;
+        let partialResult: TaskBase;
 
         try {
             // First execution, we have not yet "yielded" any of the tasks.
@@ -90,7 +91,7 @@ export class Orchestrator {
 
             while (true) {
 
-                if (!(TaskFilter.isTask(g.value))) {
+                if (!(TaskFilter.isYieldable(g.value))) {
                     if (!g.done) {
                         // The orchestrator must have yielded a non-Task related type,
                         // so just return execution flow with what they yielded back.
@@ -112,18 +113,10 @@ export class Orchestrator {
                     }
                 }
 
-                partialResult = g.value as Task | TaskSet;
-                if (TaskFilter.isSingleTask(partialResult)) {
-                    if (!partialResult.wasYielded) {
-                        actions.push([ partialResult.action ]);
-                        partialResult.wasYielded = true;
-                    }
-                } else if (TaskFilter.isTaskSet(partialResult)) {
-                    const unyieldedTasks = partialResult.tasks.filter((task) => !task.wasYielded);
-                    actions.push(unyieldedTasks.map((task) => task.action));
-                    unyieldedTasks.forEach((task) => {
-                        task.wasYielded = true;
-                    });
+                partialResult = g.value as TaskBase;
+                const newActions = partialResult.yieldNewActions();
+                if (newActions && newActions.length > 0) {
+                    actions.push(newActions);
                 }
 
                 // Return continue as new events as completed, as the execution itself is now completed.
@@ -550,37 +543,59 @@ export class Orchestrator {
         }
     }
 
-    private all(state: HistoryEvent[], tasks: Task[]): TaskSet {
-        const faulted = tasks.filter(TaskFilter.isFailedTask);
-        if (faulted.length > 0) {
-            return TaskFactory.FailedTaskSet(tasks, (faulted[0] as FailedSingleTask).exception);
+    private all(state: HistoryEvent[], tasks: TaskBase[]): TaskSet {
+        let maxCompletionIndex: number | undefined = undefined;
+        const errors: Error[] = [];
+        const results: unknown[] = [];
+        for (let index = 0; index < tasks.length; index++) {
+            const task = tasks[index];
+            if (!TaskFilter.isCompletedTask(task)) {
+                return TaskFactory.UncompletedTaskSet(tasks);
+            }
+
+            if (!maxCompletionIndex) {
+                maxCompletionIndex = task.completionIndex;
+            } else if (maxCompletionIndex < task.completionIndex) {
+                maxCompletionIndex = task.completionIndex;
+            }
+
+            if (TaskFilter.isFailedTask(task)) {
+                errors.push(task.exception);
+            } else {
+                results.push(task.result);
+            }
         }
 
-        const succesfulTasks = tasks.filter(TaskFilter.isSuccessfulSingleTask);
-        const isCompleted = succesfulTasks.length === tasks.length;
-        if (isCompleted) {
-            const results = succesfulTasks.reduce((acc, t) => {
-                return [...acc, t.result];
-            }, []);
+        // We are guaranteed that maxCompletionIndex is not undefined, or
+        // we would have alreayd returned an uncompleted task set.
+        const completionIndex = maxCompletionIndex as number;
 
-            return TaskFactory.SuccessfulTaskSet(tasks, results);
+        if (errors.length > 0) {
+            return TaskFactory.FailedTaskSet(tasks, completionIndex, new AggregatedError(errors));
         } else {
-            return TaskFactory.UncompletedTaskSet(tasks);
+            return TaskFactory.SuccessfulTaskSet(tasks, completionIndex, results);
         }
     }
 
-    private any(state: HistoryEvent[], tasks: Task[]): TaskSet {
-        const completedTasks = tasks
-            .filter(TaskFilter.isSuccessfulSingleTask)
-            .sort((a, b) => {
-                if (a.completionIndex > b.completionIndex) { return 1; }
-                if (a.completionIndex < b.completionIndex) { return -1; }
-                return 0;
-            });
+    private any(state: HistoryEvent[], tasks: TaskBase[]): TaskSet {
+        if (!tasks || tasks.length === 0) {
+            throw new Error("At least one yieldable task must be provided to wait for.");
+        }
+
+        let firstCompleted: CompletedTask | undefined = undefined;
+        for (let index = 0; index < tasks.length; index++) {
+            const task = tasks[index];
+            if (TaskFilter.isCompletedTask(task)) {
+                if (!firstCompleted) {
+                    firstCompleted = task;
+                } else if (task.completionIndex < firstCompleted.completionIndex) {
+                    firstCompleted = task;
+                }
+            }
+        }
 
-        const firstCompleted = completedTasks[0];
         if (firstCompleted) {
-            return TaskFactory.SuccessfulTaskSet(tasks, firstCompleted);
+            return TaskFactory.SuccessfulTaskSet(tasks, firstCompleted.completionIndex, firstCompleted);
         } else {
             return TaskFactory.UncompletedTaskSet(tasks);
         }
diff --git a/src/tasks/task.ts b/src/tasks/task.ts
index 6c7edfb..8299b9c 100644
--- a/src/tasks/task.ts
+++ b/src/tasks/task.ts
@@ -1,5 +1,5 @@
 import { IAction } from "../classes";
-import { SingleTask } from "./taskinterfaces";
+import { TaskBase } from "./taskinterfaces";
 
 /**
  * Represents some pending action. Similar to a native JavaScript promise in
@@ -33,12 +33,12 @@ import { SingleTask } from "./taskinterfaces";
  * return firstDone.result;
  * ```
  */
-export class Task implements SingleTask {
+export class Task implements TaskBase {
     /**
      * Used to keep track of how many times the task has been yielded to avoid
      * scheduling the internal action multiple times _Internal use only._
      */
-    public wasYielded = false;
+    private wasYielded = false;
 
     /** @hidden */
     constructor(
@@ -79,4 +79,16 @@ export class Task implements SingleTask {
          */
         public readonly completionIndex?: number,
     ) { }
+
+    /**
+     * _Internal use only._
+     */
+    public yieldNewActions(): IAction[] {
+        if (!this.wasYielded) {
+            this.wasYielded = true;
+            return [ this.action ];
+        }
+
+        return [];
+    }
 }
diff --git a/src/tasks/taskfactory.ts b/src/tasks/taskfactory.ts
index 4b35395..ae84a50 100644
--- a/src/tasks/taskfactory.ts
+++ b/src/tasks/taskfactory.ts
@@ -1,5 +1,6 @@
 import { CreateTimerAction, IAction } from "../classes";
 import { Task} from "./task";
+import { TaskBase} from "./taskinterfaces";
 import { TaskSet } from "./taskset";
 import { TimerTask } from "./timertask";
 
@@ -42,33 +43,36 @@ export class TaskFactory {
         return new TimerTask(false, action);
     }
 
-    public static SuccessfulTaskSet(tasks: Task[], result: unknown): TaskSet {
+    public static SuccessfulTaskSet(tasks: TaskBase[], completionIndex: number, result: unknown): TaskSet {
         return new TaskSet(
             true,
             false,
             tasks,
+            completionIndex,
             result,
             undefined,
         );
     }
 
-    public static FailedTaskSet(tasks: Task[], exception: Error): TaskSet {
+    public static FailedTaskSet(tasks: TaskBase[], completionIndex: number, exception: Error): TaskSet {
         return new TaskSet(
             true,
             true,
             tasks,
+            completionIndex,
             undefined,
             exception,
         );
     }
 
-    public static UncompletedTaskSet(tasks: Task[]): TaskSet {
+    public static UncompletedTaskSet(tasks: TaskBase[]): TaskSet {
         return new TaskSet(
             false,
             false,
             tasks,
             undefined,
             undefined,
+            undefined,
         );
     }
 }
diff --git a/src/tasks/taskfilter.ts b/src/tasks/taskfilter.ts
index e2c00bb..2aa2e80 100644
--- a/src/tasks/taskfilter.ts
+++ b/src/tasks/taskfilter.ts
@@ -1,42 +1,43 @@
 import { Task } from "./task";
-import { CompletedTask, FailedTask, SuccessfulSingleTask, SuccessfulTask, UncompletedTask } from "./taskinterfaces";
+import { CompletedTask, FailedTask, SuccessfulTask, TaskBase, UncompletedTask } from "./taskinterfaces";
 import { TaskSet } from "./taskset";
 
 export class TaskFilter {
-    public static isTask(value: unknown): value is (Task | TaskSet) {
-        if (!value) {
-            return false;
-        }
-        const task = value as (Task | TaskSet);
-        return task.isCompleted !== undefined && task.isFaulted !== undefined;
+    public static CompareFinishedTime(taskA: CompletedTask, taskB: CompletedTask) {
+        if (taskA.completionIndex > taskB.completionIndex) { return 1; }
+        if (taskA.completionIndex < taskB.completionIndex) { return -1; }
+        return 0;
     }
 
-    public static isSingleTask(task: Task | TaskSet): task is Task {
+    public static isYieldable(task: any): task is TaskBase {
+        const taskBase = task as TaskBase;
+        return taskBase
+            && taskBase.isCompleted !== undefined
+            && taskBase.isFaulted !== undefined
+            && taskBase.yieldNewActions !== undefined;
+    }
+
+    public static isSingleTask(task: TaskBase): task is Task {
         return (task instanceof Task);
     }
 
-    public static isTaskSet(task: Task | TaskSet): task is TaskSet {
+    public static isTaskSet(task: TaskBase): task is TaskSet {
         return (task instanceof TaskSet);
     }
 
-    public static isCompletedTask(task: Task | TaskSet): task is CompletedTask {
+    public static isCompletedTask(task: TaskBase): task is CompletedTask {
         return task.isCompleted;
     }
 
-    public static isUncompletedTask(task: Task | TaskSet): task is UncompletedTask  {
+    public static isUncompletedTask(task: TaskBase): task is UncompletedTask  {
         return task.isCompleted === false;
     }
 
-    public static isSuccessfulTask(task: Task | TaskSet): task is SuccessfulTask  {
-        const successfulTask = task as SuccessfulTask;
-        return successfulTask.isCompleted === true && successfulTask.isFaulted === false && successfulTask.result !== undefined;
-    }
-
-    public static isSuccessfulSingleTask(task: Task): task is SuccessfulSingleTask  {
-        return TaskFilter.isSingleTask(task) && task.isCompleted === true && task.isFaulted === false && task.completionIndex !== undefined;
+    public static isSuccessfulTask(task: TaskBase): task is SuccessfulTask {
+        return task.isCompleted === true && task.isFaulted === false;
     }
 
-    public static isFailedTask(task: Task | TaskSet): task is FailedTask {
+    public static isFailedTask(task: TaskBase): task is FailedTask {
         return task.isCompleted === true && task.isFaulted === true;
     }
 }
diff --git a/src/tasks/taskinterfaces.ts b/src/tasks/taskinterfaces.ts
index d24c565..25bbcf7 100644
--- a/src/tasks/taskinterfaces.ts
+++ b/src/tasks/taskinterfaces.ts
@@ -1,69 +1,31 @@
 import { IAction } from "../classes";
 
  // Base interfaces
-interface TaskBase {
+export interface TaskBase {
     readonly isCompleted: boolean;
     readonly isFaulted: boolean;
+    yieldNewActions(): IAction[];
  }
 
-export interface SingleTask extends TaskBase {
-    readonly action: IAction;
-    wasYielded: boolean;
-}
-
-export interface UncompletedSingleTask extends TaskBase {
+export interface UncompletedTask extends TaskBase {
     readonly isCompleted: false;
     readonly isFaulted: false;
 }
 
-export interface CompletedSingleTask extends SingleTask {
-    completionIndex: number;
-    readonly timestamp: Date;
-    readonly id: number;
+export interface CompletedTask extends TaskBase {
+    readonly completionIndex: number;
     readonly isCompleted: true;
-    readonly result: unknown;
+    readonly result: unknown | undefined;
 }
 
-export interface SuccessfulSingleTask extends CompletedSingleTask {
+export interface SuccessfulTask extends CompletedTask {
     readonly isFaulted: false;
-    readonly exception: undefined;
-}
-
-export interface FailedSingleTask extends CompletedSingleTask {
-    readonly isFaulted: true;
-    readonly exception: Error;
-    readonly result: undefined;
-}
-
-interface TaskCollection extends TaskBase {
-    readonly tasks: SingleTask[];
-}
-
-export interface CompletedTaskSet extends TaskCollection {
-    readonly isCompleted: true;
     readonly result: unknown;
+    readonly exception: undefined;
 }
 
-export interface UncompletedTaskSet extends TaskCollection {
-    readonly isCompleted: false;
-    readonly isFaulted: false;
-}
-
-export interface FailedTaskSet extends CompletedTaskSet {
+export interface FailedTask extends CompletedTask {
     readonly isFaulted: true;
     readonly exception: Error;
     readonly result: undefined;
 }
-
-export interface SuccessfulTaskSet extends CompletedTaskSet {
-    readonly isFaulted: false;
-    readonly exception: undefined;
-}
-
-export type CompletedTask = CompletedSingleTask | CompletedTaskSet;
-
-export type UncompletedTask = UncompletedTaskSet & UncompletedSingleTask;
-
-export type SuccessfulTask =  SuccessfulTaskSet & SuccessfulSingleTask;
-
-export type FailedTask = FailedTaskSet & FailedSingleTask;
diff --git a/src/tasks/taskset.ts b/src/tasks/taskset.ts
index c240b8a..0acb98d 100644
--- a/src/tasks/taskset.ts
+++ b/src/tasks/taskset.ts
@@ -1,12 +1,21 @@
-import { SingleTask } from "./taskinterfaces";
+import { IAction } from "../classes";
+import { TaskBase } from "./taskinterfaces";
 
 /** @hidden */
-export class TaskSet {
+export class TaskSet implements TaskBase {
+
     constructor(
         public readonly isCompleted: boolean,
         public readonly isFaulted: boolean,
-        public readonly tasks: SingleTask[],
+        private readonly tasks: TaskBase[],
+        private readonly completionIndex?: number,
         public result?: unknown,
         public exception?: Error,
     ) { }
+
+    public yieldNewActions(): IAction[] {
+        // Get all of the actions in subtasks and flatten into one array.
+        return this.tasks.map((task) => task.yieldNewActions())
+            .reduce((actions, subTaskActions) => actions.concat(subTaskActions));
+    }
 }


Test patch:
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index 12765a7..11a2b2f 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -1730,7 +1730,8 @@ describe("Orchestrator", () => {
                 ),
             });
 
-            const expectedErr = "Activity function 'GetFileSize' failed: Could not find file.";
+            const expectedErr1 = "Activity function 'GetFileSize' failed: Could not find file file2.png";
+            const expectedErr2 = "Activity function 'GetFileSize' failed: Could not find file file3.csx";
 
             orchestrator(mockContext);
 
@@ -1749,7 +1750,9 @@ describe("Orchestrator", () => {
                     ],
                 },
             );
-            expect(orchestrationState.error).to.include(expectedErr);
+
+            expect(orchestrationState.error).to.include(expectedErr1);
+            expect(orchestrationState.error).to.include(expectedErr2);
         });
 
         it("Task.any proceeds if a scheduled parallel task completes in order", async () => {
@@ -1833,6 +1836,125 @@ describe("Orchestrator", () => {
             );
         });
 
+        it("Task.any called with one TaskSet and one timer where TaskSet wins", async () => {
+            const orchestrator = TestOrchestrations.AnyWithTaskSet;
+            const eventsWin = true;
+            const initialTime = moment.utc();
+            let mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetAnyWithTaskSet(
+                        initialTime.toDate(),
+                        1,
+                        eventsWin,
+                    ),
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: false,
+                    actions:
+                    [
+                        [
+                            new WaitForExternalEventAction("firstRequiredEvent"),
+                            new WaitForExternalEventAction("secondRequiredEvent"),
+                            new CreateTimerAction(initialTime.add(300, "s").toDate()),
+                        ],
+                    ],
+                    output: undefined,
+                }),
+            );
+
+            mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetAnyWithTaskSet(
+                        initialTime.toDate(),
+                        2,
+                        eventsWin,
+                    ),
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: false,
+                    actions:
+                    [
+                        [
+                            new WaitForExternalEventAction("firstRequiredEvent"),
+                            new WaitForExternalEventAction("secondRequiredEvent"),
+                            new CreateTimerAction(initialTime.add(300, "s").toDate()),
+                        ],
+                        [ new CallActivityAction("Hello", "Tokyo") ],
+                    ],
+                    output: undefined,
+                }),
+            );
+        });
+
+        it("Task.any called with one TaskSet and one timer where timer wins", async () => {
+            const orchestrator = TestOrchestrations.AnyWithTaskSet;
+            const eventsWin = false;
+            const initialTime = moment.utc();
+            let mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetAnyWithTaskSet(
+                        initialTime.toDate(),
+                        1,
+                        eventsWin,
+                    ),
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: false,
+                    actions:
+                    [
+                        [
+                            new WaitForExternalEventAction("firstRequiredEvent"),
+                            new WaitForExternalEventAction("secondRequiredEvent"),
+                            new CreateTimerAction(initialTime.add(300, "s").toDate()),
+                        ],
+                    ],
+                    output: undefined,
+                }),
+            );
+
+            mockContext = new MockContext({
+                context: new DurableOrchestrationBindingInfo(
+                    TestHistories.GetAnyWithTaskSet(
+                        initialTime.toDate(),
+                        2,
+                        eventsWin,
+                    ),
+                ),
+            });
+
+            orchestrator(mockContext);
+
+            expect(mockContext.doneValue).to.be.deep.equal(
+                new OrchestratorState({
+                    isDone: true,
+                    actions:
+                    [
+                        [
+                            new WaitForExternalEventAction("firstRequiredEvent"),
+                            new WaitForExternalEventAction("secondRequiredEvent"),
+                            new CreateTimerAction(initialTime.add(300, "s").toDate()),
+                        ],
+                    ],
+                    output: ["timeout"],
+                }),
+            );
+        });
+
         it("Task yielded in both Task.any() and afterwards scheduled only once", async () => {
             const orchestrator = TestOrchestrations.AnyAOrBYieldATwice;
             const completeInOrder = true;
diff --git a/test/testobjects/TestOrchestrations.ts b/test/testobjects/TestOrchestrations.ts
index d6a255f..4c2c072 100644
--- a/test/testobjects/TestOrchestrations.ts
+++ b/test/testobjects/TestOrchestrations.ts
@@ -45,6 +45,31 @@ export class TestOrchestrations {
         return output.result;
     });
 
+    public static AnyWithTaskSet: any = df.orchestrator(function*(context: any) {
+        const now = new Date(context.df.currentUtcDateTime).getTime();
+        const deadline = new Date(now + 300000);
+        const timeoutTask = context.df.createTimer(deadline);
+
+        const firstRequiredTask = context.df.waitForExternalEvent("firstRequiredEvent");
+        const secondRequiredTask = context.df.waitForExternalEvent("secondRequiredEvent");
+
+        const allRequiredEvents = context.df.Task.all([firstRequiredTask, secondRequiredTask]);
+
+        const winner = yield context.df.Task.any([allRequiredEvents, timeoutTask]);
+        const outputs = [];
+
+        if (winner === timeoutTask) {
+            // timeout case
+            outputs.push("timeout");
+        } else {
+            // success case
+            const okaction = yield context.df.callActivity("Hello", "Tokyo");
+            timeoutTask.cancel();
+            outputs.push(okaction);
+        }
+        return outputs;
+    });
+
     public static CallActivityNoInput: any = df.orchestrator(function*(context: any) {
        const returnValue = yield context.df.callActivity("ReturnsFour");
        return returnValue;
diff --git a/test/testobjects/testhistories.ts b/test/testobjects/testhistories.ts
index 90efefc..ce9f33b 100644
--- a/test/testobjects/testhistories.ts
+++ b/test/testobjects/testhistories.ts
@@ -235,6 +235,87 @@ export class TestHistories {
         return history;
     }
 
+    public static GetAnyWithTaskSet(firstTimestamp: Date, iteration: number, eventsBeatTimer: boolean): HistoryEvent[] {
+        const firstIteration = moment(firstTimestamp);
+        const fireAt = firstIteration.add(300, "s").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: "AnyWithTaskSet",
+            }));
+            history.push(new TimerCreatedEvent({
+                eventId: 0,
+                timestamp: firstTimestamp,
+                isPlayed: false,
+                fireAt,
+            }));
+            history.push(new OrchestratorCompletedEvent({
+                eventId: -1,
+                timestamp: firstTimestamp,
+                isPlayed: iteration > 1,
+            }));
+        }
+
+        if (iteration >= 2) {
+            const secondIteration: Date = eventsBeatTimer
+                ? firstIteration.add(2500, "ms").toDate()
+                : firstIteration.add(31500, "ms").toDate();
+
+            history.push(new OrchestratorStartedEvent({
+                eventId: -1,
+                timestamp: secondIteration,
+                isPlayed: iteration > 2,
+            }));
+            history.push(new EventRaisedEvent({
+                eventId: -1,
+                timestamp: firstIteration.add(2, "s").toDate(),
+                isPlayed: false,
+                name: "firstRequiredEvent",
+            }));
+            if (eventsBeatTimer) {
+                history.push(new EventRaisedEvent({
+                    eventId: -1,
+                    timestamp: firstIteration.add(2, "s").toDate(),
+                    isPlayed: false,
+                    name: "secondRequiredEvent",
+                }));
+                history.push(new TimerFiredEvent({
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    fireAt,
+                    isPlayed: false,
+                    timerId: 0,
+                }));
+            } else {
+                history.push(new TimerFiredEvent({
+                    eventId: -1,
+                    timestamp: firstTimestamp,
+                    fireAt,
+                    isPlayed: false,
+                    timerId: 0,
+                }));
+                history.push(new EventRaisedEvent({
+                    eventId: -1,
+                    timestamp: firstIteration.add(3, "s").toDate(),
+                    isPlayed: false,
+                    name: "secondRequiredEvent",
+                }));
+            }
+        }
+
+        return history;
+    }
+
     public static GetCallEntitySet(firstTimestamp: Date, entityId: EntityId) {
         const firstMoment = moment(firstTimestamp);
         const orchestratorId = uuidv1();
@@ -520,7 +601,7 @@ export class TestHistories {
             new OrchestratorStartedEvent(
                 {
                     eventId: -1,
-                    timestamp: firstMoment.add(3, "s").toDate(),
+                    timestamp: firstMoment.add(4, "s").toDate(),
                     isPlayed: false,
                 },
             ),
@@ -533,27 +614,15 @@ export class TestHistories {
                     taskScheduledId: 1,
                 },
             ),
-            new TaskCompletedEvent(
+            new TaskFailedEvent(
                 {
                     eventId: -1,
                     timestamp: firstMoment.add(3, "s").toDate(),
                     isPlayed: false,
                     result: JSON.stringify(2),
                     taskScheduledId: 2,
-                },
-            ),
-            new OrchestratorCompletedEvent(
-                {
-                    eventId: -1,
-                    timestamp: firstTimestamp,
-                    isPlayed: false,
-                },
-            ),
-            new OrchestratorStartedEvent(
-                {
-                    eventId: -1,
-                    timestamp: firstMoment.add(4, "s").toDate(),
-                    isPlayed: false,
+                    reason: `Activity function 'GetFileSize' failed: Could not find file ${files[1]}`,
+                    details: "Serialized System.Exception here",
                 },
             ),
             new TaskFailedEvent(
@@ -562,7 +631,7 @@ export class TestHistories {
                     timestamp: firstMoment.add(4, "s").toDate(),
                     isPlayed: false,
                     taskScheduledId: 3,
-                    reason: "Activity function 'GetFileSize' failed: Could not find file.",
+                    reason: `Activity function 'GetFileSize' failed: Could not find file ${files[2]}`,
                     details: "Serialized System.Exception here",
                 },
             ),