C# 在ITargetBlock中重试策略<;TInput>;

C# 在ITargetBlock中重试策略<;TInput>;,c#,task-parallel-library,tpl-dataflow,C#,Task Parallel Library,Tpl Dataflow,我需要在工作流中引入重试策略。假设有3个块以这种方式连接: var executionOptions = new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 3 }; var buffer = new BufferBlock<int>(); var processing = new TransformBlock<int, int>(..., executionOptions); var send = n

我需要在工作流中引入重试策略。假设有3个块以这种方式连接:

var executionOptions = new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 3 };
var buffer = new BufferBlock<int>();
var processing = new TransformBlock<int, int>(..., executionOptions);
var send = new ActionBlock<int>(...);

buffer.LinkTo(processing);
processing.LinkTo(send);
i、 g.延迟地将消息再次放入转换块中,但在这种情况下,重试上下文(剩余重试次数等)也应传递到此块中。听起来太复杂了


是否有人看到一种更简单的方法来实现工作流块的重试策略?

我认为您几乎必须这样做,您必须跟踪邮件的剩余重试次数,并且必须以某种方式安排重试尝试

但是,您可以通过将其封装在单独的方法中使其更好。比如:

// it's a private class, so public fields are okay
private class RetryingMessage<T>
{
    public T Data;
    public int RetriesRemaining;
    public readonly List<Exception> Exceptions = new List<Exception>();
}

public static IPropagatorBlock<TInput, TOutput>
    CreateRetryingBlock<TInput, TOutput>(
    Func<TInput, Task<TOutput>> transform, int numberOfRetries,
    TimeSpan retryDelay, Action<IEnumerable<Exception>> failureHandler)
{
    var source = new TransformBlock<TInput, RetryingMessage<TInput>>(
        input => new RetryingMessage<TInput>
        { Data = input, RetriesRemaining = numberOfRetries });

    // TransformManyBlock, so that we can propagate zero results on failure
    TransformManyBlock<RetryingMessage<TInput>, TOutput> target = null;
    target = new TransformManyBlock<RetryingMessage<TInput>, TOutput>(
        async message =>
        {
            try
            {
                return new[] { await transform(message.Data) };
            }
            catch (Exception ex)
            {
                message.Exceptions.Add(ex);
                if (message.RetriesRemaining == 0)
                {
                    failureHandler(message.Exceptions);
                }
                else
                {
                    message.RetriesRemaining--;
                    Task.Delay(retryDelay)
                        .ContinueWith(_ => target.Post(message));
                }
                return null;
            }
        });

    source.LinkTo(
        target, new DataflowLinkOptions { PropagateCompletion = true });

    return DataflowBlock.Encapsulate(source, target);
}
//它是一个私有类,所以公共字段是可以的
私有类重试消息
{
公共数据;
公共检索保留;
公共只读列表例外=新列表();
}
公共静态IPropagatorBlock
创建重试块(
Func变换,int numberOfRetries,
TimeSpan retryDelay,操作失败处理程序)
{
var源=新转换块(
输入=>新建重试消息
{Data=input,RetriesRemaining=numberOfRetries});
//TransformManyBlock,这样我们就可以在失败时传播零结果
TransformManyBlock目标=null;
目标=新TransformManyBlock(
异步消息=>
{
尝试
{
返回新的[]{await transform(message.Data)};
}
捕获(例外情况除外)
{
message.Exceptions.Add(ex);
if(message.RetriesRemaining==0)
{
failureHandler(message.Exceptions);
}
其他的
{
message.retriesMaining--;
任务延迟(retryDelay)
.ContinueWith(=>target.Post(message));
}
返回null;
}
});
source.LinkTo(
目标,新的DataflowLinkOptions{PropagateCompletion=true});
返回DataflowBlock.enclosure(源、目标);
}
我添加了跟踪异常的代码,因为我认为不应该忽略故障,至少应该记录它们


此外,此代码在完成时也不能很好地工作:如果有重试等待其延迟,并且您
Complete()
该块将立即完成,重试将丢失。如果这对您来说是一个问题,您将必须跟踪未完成的reties,并在
源代码
完成且没有重试等待时完成
目标。

除了svick的优秀答案外,还有几个其他选项:

  • 您可以使用
    TransientFaultHandling.Core
    ——只需将
    MaxDegreeOfParallelism
    设置为
    Unbounded
    ,其他消息就可以通过了
  • 您可以修改块输出类型以包括故障指示和重试计数,并创建数据流循环,将筛选器传递到
    LinkTo
    ,以检查是否需要再次重试。这种方法更为复杂;如果您的块正在进行重试,则必须向其添加延迟,并添加
    TransformBlock
    以删除网格其余部分的失败/重试信息

  • 以下是两种方法
    CreateRetryTransformBlock
    CreateRetryActionBlock
    ,它们在这些假设下运行:

  • 调用方希望处理所有项目,即使其中一些项目多次失败
  • 调用者希望了解所有发生的异常,即使是最终成功的项目(不适用于
    CreateRetryActionBlock
  • 调用者可能希望设置总重试次数的上限,之后块应转换为故障状态
  • 调用方希望能够在与重试功能相关的选项之上,设置普通块的所有可用选项,包括
    MaxDegreeOfParallelism
    BoundedCapacity
    CancellationToken
    EnsureOrdered
    下面的实现使用来控制第一次尝试的操作与在延迟持续时间过后重试的以前出现故障的操作之间的并发级别

    public class RetryExecutionDataflowBlockOptions : ExecutionDataflowBlockOptions
    {
        /// <summary>The limit after which an item is returned as failed.</summary>
        public int MaxAttemptsPerItem { get; set; } = 1;
        /// <summary>The delay duration before retrying an item.</summary>
        public TimeSpan RetryDelay { 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 readonly struct RetryResult<TInput, TOutput>
    {
        public readonly TInput Input { get; }
        public readonly TOutput Output { get; }
        public readonly bool Success { get; }
        public readonly Exception[] Exceptions { get; }
    
        public bool Failed => !Success;
        public Exception FirstException => Exceptions != null ? Exceptions[0] : null;
        public int Attempts =>
            Exceptions != null ? Exceptions.Length + (Success ? 1 : 0) : 1;
    
        public RetryResult(TInput input, TOutput output, bool success,
            Exception[] exceptions)
        {
            Input = input;
            Output = output;
            Success = success;
            Exceptions = exceptions;
        }
    }
    
    public class RetryLimitException : Exception
    {
        public RetryLimitException(string message, Exception innerException)
            : base(message, innerException) { }
    }
    
    public static IPropagatorBlock<TInput, RetryResult<TInput, 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.RetryDelay;
        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.RetryDelay));
        var cancellationToken = dataflowBlockOptions.CancellationToken;
    
        var exceptionsCount = 0;
        var semaphore = new SemaphoreSlim(
            dataflowBlockOptions.MaxDegreeOfParallelism);
    
        async Task<(TOutput, Exception)> ProcessOnceAsync(TInput item)
        {
            await semaphore.WaitAsync(); // Preserve the SynchronizationContext
            try
            {
                var result = await transform(item).ConfigureAwait(false);
                return (result, null);
            }
            catch (Exception ex)
            {
                if (maxRetriesTotal != -1)
                {
                    if (Interlocked.Increment(ref exceptionsCount) > maxRetriesTotal)
                    {
                        throw new RetryLimitException($"The max retry limit " +
                            $"({maxRetriesTotal}) has been reached.", ex);
                    }
                }
                return (default, ex);
            }
            finally
            {
                semaphore.Release();
            }
        }
    
        async Task<Task<RetryResult<TInput, TOutput>>> ProcessWithRetryAsync(
            TInput item)
        {
            // Creates a two-stages operation. Preserves the context on every await.
            var (result, firstException) = await ProcessOnceAsync(item);
            if (firstException == null) return Task.FromResult(
                new RetryResult<TInput, TOutput>(item, result, true, null));
            return RetryStageAsync();
    
            async Task<RetryResult<TInput, TOutput>> RetryStageAsync()
            {
                var exceptions = new List<Exception>();
                exceptions.Add(firstException);
                for (int i = 2; i <= maxAttemptsPerItem; i++)
                {
                    await Task.Delay(retryDelay, cancellationToken);
                    var (result, exception) = await ProcessOnceAsync(item);
                    if (exception != null)
                        exceptions.Add(exception);
                    else
                        return new RetryResult<TInput, TOutput>(item, result,
                            true, exceptions.ToArray());
                }
                return new RetryResult<TInput, TOutput>(item, default, false,
                    exceptions.ToArray());
            };
        }
    
        // The input block awaits the first stage of each operation
        var input = new TransformBlock<TInput, Task<RetryResult<TInput, TOutput>>>(
            item => ProcessWithRetryAsync(item), dataflowBlockOptions);
    
        // The output block awaits the second (and final) stage of each operation
        var output = new TransformBlock<Task<RetryResult<TInput, TOutput>>,
            RetryResult<TInput, TOutput>>(t => t, dataflowBlockOptions);
    
        input.LinkTo(output, new DataflowLinkOptions { PropagateCompletion = true });
    
        // In case of failure ensure that the input block is faulted too,
        // so that its input/output queues are emptied, and any pending
        // SendAsync operations are aborted
        PropagateFailure(output, input);
    
        return DataflowBlock.Encapsulate(input, output);
    
        async void PropagateFailure(IDataflowBlock block1, IDataflowBlock block2)
        {
            try { await block1.Completion.ConfigureAwait(false); }
            catch (Exception ex) { block2.Fault(ex); }
        }
    }
    
    public static ITargetBlock<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<RetryResult<TInput, object>>();
        block.LinkTo(nullTarget);
        return block;
    }
    
    公共类RetryExecutionDataflowBlockOptions:ExecutionDataflowBlockOptions
    {
    ///项目返回失败后的限制。
    public int MaxAttemptsPerItem{get;set;}=1;
    ///重试项目之前的延迟持续时间。
    公共TimeSpan RetryDelay{get;set;}=TimeSpan.Zero;
    ///块转换到故障状态的极限
    ///状态(默认为无限)。
    public int MaxRetriesTotal{get;set;}=-1;
    }
    公共只读结构RetryResult
    {
    公共只读TInput输入{get;}
    公共只读TOutput输出{get;}
    公共只读bool成功{get;}
    公共只读异常[]异常{get;}
    公共布尔失败=>!成功;
    公共异常FirstException=>异常!=null?异常[0]:null;
    公共整数尝试=>
    异常!=null?异常。长度+(成功?1:0):1;
    public RetryResult(TInput输入、TOutput输出、bool成功、,
    例外[]例外)
    {
    输入=输入;
    输出=输出;
    成功=成功;
    例外=例外;
    }
    }
    公共类RetryLimitException:异常
    {
    公共RetryLimitException(字符串消息,异常innerException)
    :base(消息,内部异常){}
    }
    公共静态IPropagatorBlock
    CreateRetryTransformBlock(
    Func变换,
    RetryExecutionDataflowBlockOptions(数据流块选项)
    {
    如果(transform==null)抛出新的ArgumentNullException(nameof(transform));
    如果(dataflowBlockOptions==null)
    抛出新的
    
    public class RetryExecutionDataflowBlockOptions : ExecutionDataflowBlockOptions
    {
        /// <summary>The limit after which an item is returned as failed.</summary>
        public int MaxAttemptsPerItem { get; set; } = 1;
        /// <summary>The delay duration before retrying an item.</summary>
        public TimeSpan RetryDelay { 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 readonly struct RetryResult<TInput, TOutput>
    {
        public readonly TInput Input { get; }
        public readonly TOutput Output { get; }
        public readonly bool Success { get; }
        public readonly Exception[] Exceptions { get; }
    
        public bool Failed => !Success;
        public Exception FirstException => Exceptions != null ? Exceptions[0] : null;
        public int Attempts =>
            Exceptions != null ? Exceptions.Length + (Success ? 1 : 0) : 1;
    
        public RetryResult(TInput input, TOutput output, bool success,
            Exception[] exceptions)
        {
            Input = input;
            Output = output;
            Success = success;
            Exceptions = exceptions;
        }
    }
    
    public class RetryLimitException : Exception
    {
        public RetryLimitException(string message, Exception innerException)
            : base(message, innerException) { }
    }
    
    public static IPropagatorBlock<TInput, RetryResult<TInput, 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.RetryDelay;
        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.RetryDelay));
        var cancellationToken = dataflowBlockOptions.CancellationToken;
    
        var exceptionsCount = 0;
        var semaphore = new SemaphoreSlim(
            dataflowBlockOptions.MaxDegreeOfParallelism);
    
        async Task<(TOutput, Exception)> ProcessOnceAsync(TInput item)
        {
            await semaphore.WaitAsync(); // Preserve the SynchronizationContext
            try
            {
                var result = await transform(item).ConfigureAwait(false);
                return (result, null);
            }
            catch (Exception ex)
            {
                if (maxRetriesTotal != -1)
                {
                    if (Interlocked.Increment(ref exceptionsCount) > maxRetriesTotal)
                    {
                        throw new RetryLimitException($"The max retry limit " +
                            $"({maxRetriesTotal}) has been reached.", ex);
                    }
                }
                return (default, ex);
            }
            finally
            {
                semaphore.Release();
            }
        }
    
        async Task<Task<RetryResult<TInput, TOutput>>> ProcessWithRetryAsync(
            TInput item)
        {
            // Creates a two-stages operation. Preserves the context on every await.
            var (result, firstException) = await ProcessOnceAsync(item);
            if (firstException == null) return Task.FromResult(
                new RetryResult<TInput, TOutput>(item, result, true, null));
            return RetryStageAsync();
    
            async Task<RetryResult<TInput, TOutput>> RetryStageAsync()
            {
                var exceptions = new List<Exception>();
                exceptions.Add(firstException);
                for (int i = 2; i <= maxAttemptsPerItem; i++)
                {
                    await Task.Delay(retryDelay, cancellationToken);
                    var (result, exception) = await ProcessOnceAsync(item);
                    if (exception != null)
                        exceptions.Add(exception);
                    else
                        return new RetryResult<TInput, TOutput>(item, result,
                            true, exceptions.ToArray());
                }
                return new RetryResult<TInput, TOutput>(item, default, false,
                    exceptions.ToArray());
            };
        }
    
        // The input block awaits the first stage of each operation
        var input = new TransformBlock<TInput, Task<RetryResult<TInput, TOutput>>>(
            item => ProcessWithRetryAsync(item), dataflowBlockOptions);
    
        // The output block awaits the second (and final) stage of each operation
        var output = new TransformBlock<Task<RetryResult<TInput, TOutput>>,
            RetryResult<TInput, TOutput>>(t => t, dataflowBlockOptions);
    
        input.LinkTo(output, new DataflowLinkOptions { PropagateCompletion = true });
    
        // In case of failure ensure that the input block is faulted too,
        // so that its input/output queues are emptied, and any pending
        // SendAsync operations are aborted
        PropagateFailure(output, input);
    
        return DataflowBlock.Encapsulate(input, output);
    
        async void PropagateFailure(IDataflowBlock block1, IDataflowBlock block2)
        {
            try { await block1.Completion.ConfigureAwait(false); }
            catch (Exception ex) { block2.Fault(ex); }
        }
    }
    
    public static ITargetBlock<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<RetryResult<TInput, object>>();
        block.LinkTo(nullTarget);
        return block;
    }