Пример #1
0
        /// <summary>
        /// Executes the given actionTask and commits the ActionResponse to forgeState on success.
        /// </summary>
        /// <param name="treeNodeKey">The TreeNode's key where the actions are taking place.</param>
        /// <param name="treeActionKey">The TreeAction's key of the action taking place.</param>
        /// <param name="treeAction">The TreeAction object that holds properties of the action.</param>
        /// <param name="actionDefinition">The object holding definitions for the action to execute.</param>
        /// <param name="actionTimeoutTask">The delay task tied to the action timeout.</param>
        /// <param name="token">The cancellation token.</param>
        /// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
        /// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
        /// <returns>
        ///     RanToCompletion if the action was completed successfully.
        ///     Exceptions are thrown on timeout, cancellation, or retriable failures.
        /// </returns>
        internal async Task ExecuteAction(
            string treeNodeKey,
            string treeActionKey,
            TreeAction treeAction,
            ActionDefinition actionDefinition,
            Task actionTimeoutTask,
            CancellationToken token)
        {
            // Set up a linked cancellation token to trigger on timeout if ContinuationOnTimeout is set.
            // This ensures the runActionTask gets canceled when Forge timeout is hit.
            CancellationTokenSource actionCts = CancellationTokenSource.CreateLinkedTokenSource(token);

            token = treeAction.ContinuationOnTimeout ? actionCts.Token : token;

            // Evaluate the dynamic properties that are used by the actionTask.
            ActionContext actionContext = new ActionContext(
                this.Parameters.SessionId,
                treeNodeKey,
                treeActionKey,
                treeAction.Action,
                await this.EvaluateDynamicProperty(treeAction.Input, actionDefinition.InputType).ConfigureAwait(false),
                await this.EvaluateDynamicProperty(treeAction.Properties, null).ConfigureAwait(false),
                this.Parameters.UserContext,
                token,
                this.Parameters.ForgeState
                );

            // Instantiate the BaseAction-derived ActionType class and invoke the RunAction method on it.
            var                   actionObject  = Activator.CreateInstance(actionDefinition.ActionType);
            MethodInfo            method        = typeof(BaseAction).GetMethod("RunAction");
            Task <ActionResponse> runActionTask = (Task <ActionResponse>)method.Invoke(actionObject, new object[] { actionContext });

            // Await for the first completed task between our runActionTask and the timeout task.
            // This allows us to continue without awaiting the runActionTask upon timeout.
            var completedTask = await Task.WhenAny(runActionTask, actionTimeoutTask).ConfigureAwait(false);

            if (completedTask == actionTimeoutTask)
            {
                // Throw on cancellation requested if that's the reason the timeout task completed.
                token.ThrowIfCancellationRequested();

                // If the timeout is hit and the ContinuationOnTimeout flag is set, commit a new ActionResponse
                // with the status set to TimeoutOnAction and return.
                if (treeAction.ContinuationOnTimeout)
                {
                    // Trigger linked cancellation token before continuing to ensure the runActionTask gets cancelled.
                    actionCts.Cancel();

                    ActionResponse timeoutResponse = new ActionResponse
                    {
                        Status = "TimeoutOnAction"
                    };

                    await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);

                    return;
                }

                // ActionTimeout has been hit. Throw special exception to indicate this.
                throw new ActionTimeoutException(string.Format(
                                                     "ActionTimeoutTask timed out before Action could complete. TreeNodeKey: {0}, TreeActionKey: {1}, ActionName: {2}.",
                                                     treeNodeKey,
                                                     treeActionKey,
                                                     treeAction.Action));
            }
            else
            {
                // Handle the completed runActionTask.
                if (runActionTask.Status == TaskStatus.RanToCompletion)
                {
                    await this.CommitActionResponse(treeActionKey, await runActionTask).ConfigureAwait(false);
                }

                // Await the completed task to propagate any exceptions.
                // Exceptions are thrown here if the action hit a timeout, was cancelled, or failed.
                await runActionTask;
            }
        }
Пример #2
0
        /// <summary>
        /// Executes the given action. Attempts retries according to the retry policy and timeout.
        /// Returns without throwing exception if the action was completed successfully.
        /// </summary>
        /// <param name="treeNodeKey">The TreeNode's key where the actions are taking place.</param>
        /// <param name="treeActionKey">The TreeAction's key of the action taking place.</param>
        /// <param name="treeAction">The TreeAction object that holds properties of the action.</param>
        /// <param name="actionDefinition">The object holding definitions for the action to execute.</param>
        /// <param name="token">The cancellation token.</param>
        /// <exception cref="ActionTimeoutException">If the action-level timeout was hit.</exception>
        /// <exception cref="OperationCanceledException">If the cancellation token was triggered.</exception>
        internal async Task ExecuteActionWithRetry(
            string treeNodeKey,
            string treeActionKey,
            TreeAction treeAction,
            ActionDefinition actionDefinition,
            CancellationToken token)
        {
            // Initialize values. Default infinite timeout. Default RetryPolicyType.None.
            int       retryCount     = 0;
            Exception innerException = null;
            Stopwatch stopwatch      = new Stopwatch();

            int actionTimeout = (int)await this.EvaluateDynamicProperty(treeAction.Timeout ?? -1, typeof(int)).ConfigureAwait(false);

            RetryPolicyType retryPolicyType = treeAction.RetryPolicy != null ? treeAction.RetryPolicy.Type : RetryPolicyType.None;
            TimeSpan        waitTime        = treeAction.RetryPolicy != null?TimeSpan.FromMilliseconds(treeAction.RetryPolicy.MinBackoffMs) : new TimeSpan();

            // Kick off timers.
            Task actionTimeoutTask = Task.Delay(actionTimeout, token);

            stopwatch.Start();

            // Attmpt to ExecuteAction based on RetryPolicy and Timeout.
            // Throw on non-retriable exceptions.
            while (actionTimeout == -1 || stopwatch.ElapsedMilliseconds < actionTimeout)
            {
                token.ThrowIfCancellationRequested();

                try
                {
                    await this.ExecuteAction(treeNodeKey, treeActionKey, treeAction, actionDefinition, actionTimeoutTask, token).ConfigureAwait(false);

                    return; // success!
                }
                catch (OperationCanceledException)
                {
                    throw; // non-retriable exception
                }
                catch (ActionTimeoutException)
                {
                    throw; // non-retriable exception
                }
                catch (EvaluateDynamicPropertyException)
                {
                    throw; // non-retriable exception
                }
                catch (Exception e)
                {
                    // Cache exception as innerException in case we need to throw ActionTimeoutException.
                    innerException = e;

                    // Hit retriable exception. Retry according to RetryPolicy.
                    // When retries are exhausted, throw ActionTimeoutException with Exception e as the innerException.
                    switch (retryPolicyType)
                    {
                    case RetryPolicyType.FixedInterval:
                    {
                        // FixedInterval retries every MinBackoffMs until the timeout.
                        // Ex) 200ms, 200ms, 200ms...
                        waitTime = TimeSpan.FromMilliseconds(treeAction.RetryPolicy.MinBackoffMs);
                        break;
                    }

                    case RetryPolicyType.ExponentialBackoff:
                    {
                        // ExponentialBackoff retries every Math.Min(MinBackoffMs * 2^(retryCount), MaxBackoffMs) until the timeout.
                        // Ex) 100ms, 200ms, 400ms...
                        waitTime = TimeSpan.FromMilliseconds(Math.Min(treeAction.RetryPolicy.MaxBackoffMs, waitTime.TotalMilliseconds * 2));
                        break;
                    }

                    case RetryPolicyType.None:
                    default:
                    {
                        // No retries. Break out below to throw non-retriable exception.
                        break;
                    }
                    }
                }

                // Break out if no retry policy set.
                if (retryPolicyType == RetryPolicyType.None)
                {
                    // If the retries have exhausted and the ContinuationOnRetryExhaustion flag is set, commit a new ActionResponse
                    // with the status set to RetryExhaustedOnAction and return.
                    if (treeAction.ContinuationOnRetryExhaustion)
                    {
                        ActionResponse timeoutResponse = new ActionResponse
                        {
                            Status = "RetryExhaustedOnAction"
                        };

                        await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);

                        return;
                    }

                    break;
                }

                // Break out early if we would hit timeout before next retry.
                if (actionTimeout != -1 && stopwatch.ElapsedMilliseconds + waitTime.TotalMilliseconds >= actionTimeout)
                {
                    // If the timeout is hit and the ContinuationOnTimeout flag is set, commit a new ActionResponse
                    // with the status set to TimeoutOnAction and return.
                    if (treeAction.ContinuationOnTimeout)
                    {
                        ActionResponse timeoutResponse = new ActionResponse
                        {
                            Status = "TimeoutOnAction"
                        };

                        await this.CommitActionResponse(treeActionKey, timeoutResponse).ConfigureAwait(false);

                        return;
                    }

                    break;
                }

                token.ThrowIfCancellationRequested();
                await Task.Delay(waitTime, token).ConfigureAwait(false);

                retryCount++;
            }

            // Retries are exhausted. Throw ActionTimeoutException with executeAction exception as innerException.
            throw new ActionTimeoutException(
                      string.Format(
                          "Action did not complete successfully. TreeNodeKey: {0}, TreeActionKey: {1}, ActionName: {2}, RetryCount: {3}, RetryPolicy: {4}",
                          treeNodeKey,
                          treeActionKey,
                          treeAction.Action,
                          retryCount,
                          retryPolicyType),
                      innerException);
        }