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