public async Task ActionListExecutor_AllActionsSucceed()
        {
            await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions =>
            {
                rootOptions.CreateCollectionRule(DefaultRuleName)
                .AddExecuteActionAppAction(new string[] { ActionTestsConstants.ZeroExitCode })
                .AddExecuteActionAppAction(new string[] { ActionTestsConstants.ZeroExitCode })
                .SetStartupTrigger();
            }, async host =>
            {
                ActionListExecutor executor = host.Services.GetService <ActionListExecutor>();

                using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(DefaultTimeout);

                CollectionRuleOptions ruleOptions      = host.Services.GetRequiredService <IOptionsMonitor <CollectionRuleOptions> >().Get(DefaultRuleName);
                ILogger <CollectionRuleService> logger = host.Services.GetRequiredService <ILogger <CollectionRuleService> >();
                ISystemClock clock = host.Services.GetRequiredService <ISystemClock>();

                CollectionRuleContext context = new(DefaultRuleName, ruleOptions, null, logger, clock);

                int callbackCount    = 0;
                Action startCallback = () => callbackCount++;

                await executor.ExecuteActions(context, startCallback, cancellationTokenSource.Token);

                VerifyStartCallbackCount(waitForCompletion: false, callbackCount);
            });
        }
        private async Task ActionListExecutor_SecondActionFail(bool waitForCompletion)
        {
            await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions =>
            {
                rootOptions.CreateCollectionRule(DefaultRuleName)
                .AddExecuteActionAppAction(waitForCompletion, new string[] { ActionTestsConstants.ZeroExitCode })
                .AddExecuteActionAppAction(waitForCompletion, new string[] { ActionTestsConstants.NonzeroExitCode })
                .SetStartupTrigger();
            }, async host =>
            {
                ActionListExecutor executor = host.Services.GetService <ActionListExecutor>();

                using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(DefaultTimeout);

                CollectionRuleOptions ruleOptions      = host.Services.GetRequiredService <IOptionsMonitor <CollectionRuleOptions> >().Get(DefaultRuleName);
                ILogger <CollectionRuleService> logger = host.Services.GetRequiredService <ILogger <CollectionRuleService> >();
                ISystemClock clock = host.Services.GetRequiredService <ISystemClock>();

                CollectionRuleContext context = new(DefaultRuleName, ruleOptions, null, logger, clock);

                int callbackCount    = 0;
                Action startCallback = () => callbackCount++;

                CollectionRuleActionExecutionException actionExecutionException = await Assert.ThrowsAsync <CollectionRuleActionExecutionException>(
                    () => executor.ExecuteActions(context, startCallback, cancellationTokenSource.Token));

                Assert.Equal(1, actionExecutionException.ActionIndex);

                Assert.Equal(string.Format(Strings.ErrorMessage_NonzeroExitCode, "1"), actionExecutionException.Message);

                VerifyStartCallbackCount(waitForCompletion, callbackCount);
            });
        }
        public async Task ActionListExecutor_Dependencies()
        {
            const string Output1 = nameof(Output1);
            const string Output2 = nameof(Output2);
            const string Output3 = nameof(Output3);

            string a2input1 = FormattableString.Invariant($"$(Actions.a1.{Output1}) with $(Actions.a1.{Output2})T");
            string a2input2 = FormattableString.Invariant($"$(Actions.a1.{Output2})");
            string a2input3 = FormattableString.Invariant($"Output $(Actions.a1.{Output3}) trail");

            PassThroughOptions a2Settings = null;

            await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions =>
            {
                CollectionRuleOptions options = rootOptions.CreateCollectionRule(DefaultRuleName)
                                                .AddPassThroughAction("a1", "a1input1", "a1input2", "a1input3")
                                                .AddPassThroughAction("a2", a2input1, a2input2, a2input3)
                                                .SetStartupTrigger();

                a2Settings = (PassThroughOptions)options.Actions.Last().Settings;
            }, async host =>
            {
                ActionListExecutor executor = host.Services.GetService <ActionListExecutor>();

                using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(DefaultTimeout);

                CollectionRuleOptions ruleOptions      = host.Services.GetRequiredService <IOptionsMonitor <CollectionRuleOptions> >().Get(DefaultRuleName);
                ILogger <CollectionRuleService> logger = host.Services.GetRequiredService <ILogger <CollectionRuleService> >();
                ISystemClock clock = host.Services.GetRequiredService <ISystemClock>();

                CollectionRuleContext context = new(DefaultRuleName, ruleOptions, null, logger, clock);

                int callbackCount    = 0;
                Action startCallback = () => callbackCount++;

                IDictionary <string, CollectionRuleActionResult> results = await executor.ExecuteActions(context, startCallback, cancellationTokenSource.Token);

                //Verify that the original settings were not altered during execution.
                Assert.Equal(a2input1, a2Settings.Input1);
                Assert.Equal(a2input2, a2Settings.Input2);
                Assert.Equal(a2input3, a2Settings.Input3);

                Assert.Equal(1, callbackCount);
                Assert.Equal(2, results.Count);
                Assert.True(results.TryGetValue("a2", out CollectionRuleActionResult a2result));
                Assert.Equal(3, a2result.OutputValues.Count);

                Assert.True(a2result.OutputValues.TryGetValue(Output1, out string a2output1));
                Assert.Equal("a1input1 with a1input2T", a2output1);
                Assert.True(a2result.OutputValues.TryGetValue(Output2, out string a2output2));
                Assert.Equal("a1input2", a2output2);
                Assert.True(a2result.OutputValues.TryGetValue(Output3, out string a2output3));
                Assert.Equal("Output a1input3 trail", a2output3);
            }, serviceCollection =>
            {
                serviceCollection.RegisterCollectionRuleAction <PassThroughActionFactory, PassThroughOptions>(nameof(PassThroughAction));
            });
        }
Exemplo n.º 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.
            }
        }