How to keep track of faulted items in TPL pipeline in (thread)safe way

前端 未结 1 1135
天命终不由人
天命终不由人 2021-01-14 13:27

I am using TPL pipeline design together with Stephen Cleary\'s Try library In short it wraps value/exception and floats it down the pipeline. So even items that have thrown

相关标签:
1条回答
  • 2021-01-14 14:02

    I converted a retry-block implementation from an answer to a similar question, to work with Stephen Cleary's Try types as input and output. The method CreateRetryTransformBlock returns a TransformBlock<Try<TInput>, Try<TOutput>>, and the method CreateRetryActionBlock returns something that is practically an ActionBlock<Try<TInput>>.

    Three more options are available, the MaxAttemptsPerItem, MinimumRetryDelay and MaxRetriesTotal, on top of the standard execution options.

    public class RetryExecutionDataflowBlockOptions : ExecutionDataflowBlockOptions
    {
        /// <summary>The limit after which an item is returned as failed.</summary>
        public int MaxAttemptsPerItem { get; set; } = 1;
        /// <summary>The minimum delay duration before retrying an item.</summary>
        public TimeSpan MinimumRetryDelay { get; set; } = TimeSpan.Zero;
        /// <summary>The limit after which the block transitions to a faulted
        /// state (unlimited is the default).</summary>
        public int MaxRetriesTotal { get; set; } = -1;
    }
    
    public class RetryLimitException : Exception
    {
        public RetryLimitException(string message, Exception innerException)
            : base(message, innerException) { }
    }
    
    public static TransformBlock<Try<TInput>, Try<TOutput>>
        CreateRetryTransformBlock<TInput, TOutput>(
        Func<TInput, Task<TOutput>> transform,
        RetryExecutionDataflowBlockOptions dataflowBlockOptions)
    {
        if (transform == null) throw new ArgumentNullException(nameof(transform));
        if (dataflowBlockOptions == null)
            throw new ArgumentNullException(nameof(dataflowBlockOptions));
        int maxAttemptsPerItem = dataflowBlockOptions.MaxAttemptsPerItem;
        int maxRetriesTotal = dataflowBlockOptions.MaxRetriesTotal;
        TimeSpan retryDelay = dataflowBlockOptions.MinimumRetryDelay;
        if (maxAttemptsPerItem < 1) throw new ArgumentOutOfRangeException(
            nameof(dataflowBlockOptions.MaxAttemptsPerItem));
        if (maxRetriesTotal < -1) throw new ArgumentOutOfRangeException(
            nameof(dataflowBlockOptions.MaxRetriesTotal));
        if (retryDelay < TimeSpan.Zero) throw new ArgumentOutOfRangeException(
            nameof(dataflowBlockOptions.MinimumRetryDelay));
    
        var internalCTS = CancellationTokenSource
            .CreateLinkedTokenSource(dataflowBlockOptions.CancellationToken);
    
        var maxDOP = dataflowBlockOptions.MaxDegreeOfParallelism;
        var taskScheduler = dataflowBlockOptions.TaskScheduler;
    
        var exceptionsCount = 0;
        SemaphoreSlim semaphore;
        if (maxDOP == DataflowBlockOptions.Unbounded)
        {
            semaphore = new SemaphoreSlim(Int32.MaxValue);
        }
        else
        {
            semaphore = new SemaphoreSlim(maxDOP, maxDOP);
    
            // The degree of parallelism is controlled by the semaphore
            dataflowBlockOptions.MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded;
    
            // Use a limited-concurrency scheduler for preserving the processing order
            dataflowBlockOptions.TaskScheduler = new ConcurrentExclusiveSchedulerPair(
                taskScheduler, maxDOP).ConcurrentScheduler;
        }
    
        var block = new TransformBlock<Try<TInput>, Try<TOutput>>(async item =>
        {
            // Continue on captured context after every await
            if (item.IsException) return Try<TOutput>.FromException(item.Exception);
            var result1 = await ProcessOnceAsync(item);
            if (item.IsException || result1.IsValue) return result1;
            for (int i = 2; i <= maxAttemptsPerItem; i++)
            {
                await Task.Delay(retryDelay, internalCTS.Token);
                var result = await ProcessOnceAsync(item);
                if (result.IsValue) return result;
            }
            return result1; // Return the first-attempt exception
        }, dataflowBlockOptions);
    
        dataflowBlockOptions.MaxDegreeOfParallelism = maxDOP; // Restore initial value
        dataflowBlockOptions.TaskScheduler = taskScheduler; // Restore initial value
    
        _ = block.Completion.ContinueWith(_ => internalCTS.Dispose(),
            TaskScheduler.Default);
    
        return block;
    
        async Task<Try<TOutput>> ProcessOnceAsync(Try<TInput> item)
        {
            await semaphore.WaitAsync(internalCTS.Token);
            try
            {
                var result = await item.Map(transform);
                if (item.IsValue && result.IsException)
                {
                    ObserveNewException(result.Exception);
                }
                return result;
            }
            finally
            {
                semaphore.Release();
            }
        }
    
        void ObserveNewException(Exception ex)
        {
            if (maxRetriesTotal == -1) return;
            uint newCount = (uint)Interlocked.Increment(ref exceptionsCount);
            if (newCount <= (uint)maxRetriesTotal) return;
            if (newCount == (uint)maxRetriesTotal + 1)
            {
                internalCTS.Cancel(); // The block has failed
                throw new RetryLimitException($"The max retry limit " +
                    $"({maxRetriesTotal}) has been reached.", ex);
            }
            throw new OperationCanceledException();
        }
    }
    
    public static ITargetBlock<Try<TInput>> CreateRetryActionBlock<TInput>(
        Func<TInput, Task> action,
        RetryExecutionDataflowBlockOptions dataflowBlockOptions)
    {
        if (action == null) throw new ArgumentNullException(nameof(action));
        var block = CreateRetryTransformBlock<TInput, object>(async input =>
        {
            await action(input).ConfigureAwait(false); return null;
        }, dataflowBlockOptions);
        var nullTarget = DataflowBlock.NullTarget<Try<object>>();
        block.LinkTo(nullTarget);
        return block;
    }
    

    Usage example:

    var downloadBlock = CreateRetryTransformBlock(async (int construct) =>
    {
        int result = await DownloadAsync(construct);
        return result;
    }, new RetryExecutionDataflowBlockOptions()
    {
        MaxDegreeOfParallelism = 10,
        MaxAttemptsPerItem = 3,
        MaxRetriesTotal = 100,
        MinimumRetryDelay = TimeSpan.FromSeconds(10)
    });
    
    var processBlock = new TransformBlock<Try<int>, Try<int>>(
        construct => construct.Map(async value =>
    {
        return await ProcessAsync(value);
    }));
    
    downloadBlock.LinkTo(processBlock,
        new DataflowLinkOptions() { PropagateCompletion = true });
    

    To keep things simple, in case that an item has been retried the maximum number of times, the exception preserved is the first one that occurred. The subsequent exceptions are lost. In most cases the lost exceptions are going to be of the same type as the first one anyway.

    0 讨论(0)
提交回复
热议问题