Beispiel #1
0
        internal async static Task <CollectionRuleActionResult> ExecuteAndDisposeAsync(ICollectionRuleAction action, TimeSpan timeout)
        {
            using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(timeout);

            CollectionRuleActionResult result;

            try
            {
                await action.StartAsync(cancellationTokenSource.Token);

                result = await action.WaitForCompletionAsync(cancellationTokenSource.Token);
            }
            finally
            {
                await DisposableHelper.DisposeAsync(action);
            }

            return(result);
        }
 private async Task WaitForCompletion(CollectionRuleContext context,
                                      Action startCallback,
                                      IDictionary <string, CollectionRuleActionResult> allResults,
                                      ActionCompletionEntry entry,
                                      CancellationToken cancellationToken)
 {
     try
     {
         await WaitForCompletion(context, startCallback, allResults, entry.Action, entry.Options, cancellationToken);
     }
     catch (Exception ex) when(ShouldHandleException(ex, context.Name, entry.Options.Type))
     {
         throw new CollectionRuleActionExecutionException(ex, entry.Options.Type, entry.Index);
     }
     finally
     {
         await DisposableHelper.DisposeAsync(entry.Action);
     }
 }
        private static async Task ValidateAction(Action <ExecuteOptions> optionsCallback, Func <ICollectionRuleAction, CancellationToken, Task> actionCallback)
        {
            ExecuteActionFactory factory = new();

            ExecuteOptions options = new();

            optionsCallback(options);

            using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(DefaultTimeout);

            ICollectionRuleAction action = factory.Create(null, options);

            try
            {
                await actionCallback(action, cancellationTokenSource.Token);
            }
            finally
            {
                await DisposableHelper.DisposeAsync(action);
            }
        }
Beispiel #4
0
        /// <summary>
        /// Runs the pipeline to completion.
        /// </summary>
        /// <remarks>
        /// The pipeline will only successfully complete in the following scenarios:
        /// (1) the trigger is a startup trigger and the action list successfully executes once.
        /// (2) without a specified action count window duration, the number of action list executions equals the action count limit.
        /// </remarks>
        protected override async Task OnRun(CancellationToken token)
        {
            if (!_triggerOperations.TryCreateFactory(_context.Options.Trigger.Type, out ICollectionRuleTriggerFactoryProxy factory))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Strings.ErrorMessage_CouldNotMapToTrigger, _context.Options.Trigger.Type));
            }

            using CancellationTokenSource durationCancellationSource = new();
            using CancellationTokenSource linkedCancellationSource   = CancellationTokenSource.CreateLinkedTokenSource(
                      durationCancellationSource.Token,
                      token);

            CancellationToken linkedToken = linkedCancellationSource.Token;

            TimeSpan?        actionCountWindowDuration = _context.Options.Limits?.ActionCountSlidingWindowDuration;
            int              actionCountLimit          = (_context.Options.Limits?.ActionCount).GetValueOrDefault(CollectionRuleLimitsOptionsDefaults.ActionCount);
            Queue <DateTime> executionTimestamps       = new(actionCountLimit);

            // Start cancellation timer for graceful stop of the collection rule
            // when the rule duration has been specified. Conditionally enable this
            // based on if the rule has a duration limit.
            TimeSpan?ruleDuration = _context.Options.Limits?.RuleDuration;

            if (ruleDuration.HasValue)
            {
                durationCancellationSource.CancelAfter(ruleDuration.Value);
            }

            try
            {
                bool completePipeline = false;
                while (!completePipeline)
                {
                    TaskCompletionSource <object> triggerSatisfiedSource =
                        new(TaskCreationOptions.RunContinuationsAsynchronously);

                    ICollectionRuleTrigger trigger = null;
                    try
                    {
                        KeyValueLogScope triggerScope = new();
                        triggerScope.AddCollectionRuleTrigger(_context.Options.Trigger.Type);
                        IDisposable triggerScopeRegistration = _context.Logger.BeginScope(triggerScope);

                        _context.Logger.CollectionRuleTriggerStarted(_context.Name, _context.Options.Trigger.Type);

                        trigger = factory.Create(
                            _context.EndpointInfo,
                            () => triggerSatisfiedSource.TrySetResult(null),
                            _context.Options.Trigger.Settings);

                        if (null == trigger)
                        {
                            throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Strings.ErrorMessage_TriggerFactoryFailed, _context.Options.Trigger.Type));
                        }

                        // Start the trigger.
                        await trigger.StartAsync(linkedToken).ConfigureAwait(false);

                        // The pipeline signals that it has started just after starting a non-startup trigger.
                        // Instances with startup triggers signal start after having finished executing the action list.
                        if (trigger is not ICollectionRuleStartupTrigger)
                        {
                            // Signal that the pipeline trigger is initialized.
                            InvokeStartCallback();
                        }

                        // Wait for the trigger to be satisfied.
                        await triggerSatisfiedSource.WithCancellation(linkedToken).ConfigureAwait(false);

                        _context.Logger.CollectionRuleTriggerCompleted(_context.Name, _context.Options.Trigger.Type);
                    }
                    finally
                    {
                        try
                        {
                            // Intentionally not using the linkedToken. If the linkedToken was signaled
                            // due to pipeline duration expiring, try to stop the trigger gracefully
                            // unless forced by a caller to the pipeline.
                            await trigger.StopAsync(token).ConfigureAwait(false);
                        }
                        finally
                        {
                            await DisposableHelper.DisposeAsync(trigger);
                        }
                    }

                    DateTime currentTimestamp = _context.Clock.UtcNow.UtcDateTime;

                    // If rule has an action count window, Remove all execution timestamps that fall outside the window.
                    if (actionCountWindowDuration.HasValue)
                    {
                        DateTime windowStartTimestamp = currentTimestamp - actionCountWindowDuration.Value;
                        while (executionTimestamps.Count > 0)
                        {
                            DateTime executionTimestamp = executionTimestamps.Peek();
                            if (executionTimestamp < windowStartTimestamp)
                            {
                                executionTimestamps.Dequeue();
                            }
                            else
                            {
                                // Stop clearing out previous executions
                                break;
                            }
                        }
                    }

                    // Check if executing actions has been throttled due to count limit
                    if (actionCountLimit > executionTimestamps.Count)
                    {
                        executionTimestamps.Enqueue(currentTimestamp);

                        bool actionsCompleted = false;
                        try
                        {
                            // Intentionally not using the linkedToken. Allow the action list to execute gracefully
                            // unless forced by a caller to cancel or stop the running of the pipeline.
                            await _actionListExecutor.ExecuteActions(_context, InvokeStartCallback, token);

                            actionsCompleted = true;
                        }
                        catch (Exception ex) when(ex is not OperationCanceledException)
                        {
                            // Bad action execution shouldn't fail the pipeline.
                            // Logging is already done by executor.
                        }
                        finally
                        {
                            // The collection rule has executed the action list the maximum
                            // number of times as specified by the limits and the action count
                            // window was not specified. Since the pipeline can no longer execute
                            // actions, the pipeline can complete.
                            completePipeline = actionCountLimit <= executionTimestamps.Count &&
                                               !actionCountWindowDuration.HasValue;
                        }

                        if (actionsCompleted)
                        {
                            _context.Logger.CollectionRuleActionsCompleted(_context.Name);
                        }
                    }
                    else
                    {
                        _context.ThrottledCallback?.Invoke();

                        _context.Logger.CollectionRuleThrottled(_context.Name);
                    }

                    linkedToken.ThrowIfCancellationRequested();

                    // If the trigger is a startup trigger, only execute the action list once
                    // and then complete the pipeline.
                    if (trigger is ICollectionRuleStartupTrigger)
                    {
                        // Signal that the pipeline trigger is initialized.
                        InvokeStartCallback();

                        // Complete the pipeline since the action list is only executed once
                        // for collection rules with startup triggers.
                        completePipeline = true;
                    }
                }
            }
            catch (OperationCanceledException) when(durationCancellationSource.IsCancellationRequested)
            {
                // This exception is caused by the pipeline duration expiring.
                // Handle it to allow pipeline to be in completed state.
            }
        }
        //CONSIDER Only named rules currently return results since only named results can be referenced
        public async Task <IDictionary <string, CollectionRuleActionResult> > ExecuteActions(
            CollectionRuleContext context,
            Action startCallback,
            CancellationToken cancellationToken)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            bool   started = false;
            Action wrappedStartCallback = () =>
            {
                if (!started)
                {
                    started = true;
                    startCallback?.Invoke();
                }
            };

            int actionIndex = 0;
            List <ActionCompletionEntry> deferredCompletions = new(context.Options.Actions.Count);

            var actionResults      = new Dictionary <string, CollectionRuleActionResult>(StringComparer.Ordinal);
            var dependencyAnalyzer = ActionOptionsDependencyAnalyzer.Create(context);

            try
            {
                // Start and optionally wait for each action to complete
                foreach (CollectionRuleActionOptions actionOption in context.Options.Actions)
                {
                    KeyValueLogScope actionScope = new();
                    actionScope.AddCollectionRuleAction(actionOption.Type, actionIndex);
                    using IDisposable actionScopeRegistration = _logger.BeginScope(actionScope);

                    _logger.CollectionRuleActionStarted(context.Name, actionOption.Type);

                    try
                    {
                        IList <CollectionRuleActionOptions> actionDependencies = dependencyAnalyzer.GetActionDependencies(actionIndex);
                        foreach (CollectionRuleActionOptions actionDependency in actionDependencies)
                        {
                            for (int i = 0; i < deferredCompletions.Count; i++)
                            {
                                ActionCompletionEntry deferredCompletion = deferredCompletions[i];
                                if (string.Equals(deferredCompletion.Options.Name, actionDependency.Name, StringComparison.Ordinal))
                                {
                                    deferredCompletions.RemoveAt(i);
                                    i--;
                                    await WaitForCompletion(context, wrappedStartCallback, actionResults, deferredCompletion, cancellationToken);

                                    break;
                                }
                            }
                        }

                        ICollectionRuleActionFactoryProxy factory;

                        if (!_actionOperations.TryCreateFactory(actionOption.Type, out factory))
                        {
                            throw new InvalidOperationException(Strings.ErrorMessage_CouldNotMapToAction);
                        }

                        object newSettings           = dependencyAnalyzer.SubstituteOptionValues(actionResults, actionIndex, actionOption.Settings);
                        ICollectionRuleAction action = factory.Create(context.EndpointInfo, newSettings);

                        try
                        {
                            await action.StartAsync(cancellationToken);

                            // Check if the action completion should be awaited synchronously (in respect to
                            // starting the next action). If not, add a deferred entry so that it can be completed
                            // after starting each action in the list.
                            if (actionOption.WaitForCompletion.GetValueOrDefault(CollectionRuleActionOptionsDefaults.WaitForCompletion))
                            {
                                await WaitForCompletion(context, wrappedStartCallback, actionResults, action, actionOption, cancellationToken);
                            }
                            else
                            {
                                deferredCompletions.Add(new(action, actionOption, actionIndex));

                                // Set to null to skip disposal
                                action = null;
                            }
                        }
                        finally
                        {
                            await DisposableHelper.DisposeAsync(action);
                        }
                    }
                    catch (Exception ex) when(ShouldHandleException(ex, context.Name, actionOption.Type))
                    {
                        throw new CollectionRuleActionExecutionException(ex, actionOption.Type, actionIndex);
                    }

                    ++actionIndex;
                }

                // Notify that all actions have started
                wrappedStartCallback?.Invoke();

                // Wait for any actions whose completion has been deferred.
                while (deferredCompletions.Count > 0)
                {
                    ActionCompletionEntry deferredCompletion = deferredCompletions[0];
                    deferredCompletions.RemoveAt(0);
                    await WaitForCompletion(context, wrappedStartCallback, actionResults, deferredCompletion, cancellationToken);
                }

                return(actionResults);
            }
            finally
            {
                // Always dispose any deferred action completions so that those actions
                // are stopped before leaving the action list executor.
                foreach (ActionCompletionEntry deferredCompletion in deferredCompletions)
                {
                    await DisposableHelper.DisposeAsync(deferredCompletion.Action);
                }
            }
        }