in Forge.TreeWalker/src/TreeWalkerSession.cs [618:689]
internal async Task PerformActionTypeBehavior(TreeNode treeNode, string treeNodeKey)
{
List<Task> actionTasks = new List<Task>();
// Perform pre-checks.
bool preCheck_ContainsNoSubroutineActions = true;
if (treeNode.Actions == null)
{
throw new ArgumentException("Action TreeNodeTypes must contain at least one Action. TreeNodeKey: " + treeNodeKey);
}
foreach (TreeAction treeAction in treeNode.Actions.Values)
{
if (treeAction.Action == nameof(SubroutineAction))
{
preCheck_ContainsNoSubroutineActions = false;
break;
}
}
if (treeNode.Type != TreeNodeType.Subroutine && !preCheck_ContainsNoSubroutineActions)
{
throw new ArgumentException("Action TreeNodeTypes must contain zero SubroutineActions. TreeNodeKey: " + treeNodeKey);
}
// Start new parallel tasks for each action on this node.
foreach (KeyValuePair<string, TreeAction> kvp in treeNode.Actions)
{
string treeActionKey = kvp.Key;
TreeAction treeAction = kvp.Value;
if (await this.GetOutputAsync(treeActionKey).ConfigureAwait(false) != null)
{
// Handle rehydration case. Do not execute actions for which we have a persisted response.
continue;
}
if (this.actionsMap.TryGetValue(treeAction.Action, out ActionDefinition actionDefinition))
{
actionTasks.Add(this.ExecuteActionWithRetry(treeNodeKey, treeActionKey, treeAction, actionDefinition, this.walkTreeCts.Token));
}
}
// Wait for all parallel tasks to complete until the given timout.
// If any task hits a timeout, gets cancelled, or fails, an exception will be thrown.
// Note: CancelWalkTree is called at the end of every session to ensure all Actions/Tasks see the triggered cancellation token.
Task nodeTimeoutTask = Task.Delay((int)await this.EvaluateDynamicProperty(treeNode.Timeout ?? -1, typeof(int)).ConfigureAwait(false), this.walkTreeCts.Token);
actionTasks.Add(nodeTimeoutTask);
while (actionTasks.Count > 1)
{
// Throw if cancellation was requested between actions completing.
this.walkTreeCts.Token.ThrowIfCancellationRequested();
Task completedTask = await Task.WhenAny(actionTasks).ConfigureAwait(false);
actionTasks.Remove(completedTask);
if (completedTask == nodeTimeoutTask)
{
// Throw on cancellation requested if that's the reason the timeout task completed.
this.walkTreeCts.Token.ThrowIfCancellationRequested();
// NodeTimeout was hit, throw a special exception to differentiate between timeout and cancellation.
throw new TimeoutException("Hit node-level timeout in TreeNodeKey: " + treeNodeKey);
}
// Await the completed task to propagate any exceptions.
// Exceptions are thrown here if the action hit a timeout, was cancelled, or failed.
await completedTask;
}
}