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)); }); }
/// <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. } }