internal async Task PerformActionTypeBehavior()

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;
            }
        }