简体   繁体   English

如何将 ActionBlock 的所有异常包装在单个 AggregateException 中

[英]How to wrap all exceptions of an ActionBlock in a single AggregateException

I came across TPL ActionBlock which seems to be pretty handy for (throttled) parallel async operations.我遇到了 TPL ActionBlock ,它对于(受限制的)并行异步操作似乎非常方便。 So far I am using Task.WhenAll() (+ Semaphore for throttling).到目前为止,我正在使用Task.WhenAll() (+ 用于节流的Semaphore )。 When it comes to exceptions there seem to be big differences though:但是,当涉及到异常时,似乎存在很大差异:

var successList = new List<int>();
var failedList = new List<int>();
try
{
    var actionBlock = new ActionBlock<int>(
        async x => await Task.Run(() =>
        {
            if (x < 5)
            {
                failedList.Add(x);
                throw new Exception(x.ToString());
            }

            successList.Add(x);
        }),
        new ExecutionDataflowBlockOptions());

    Enumerable.Range(1, 10).Each(x => actionBlock.Post(x));
    actionBlock.Complete();

    await actionBlock.Completion.ConfigureAwait(false);
}
catch (Exception ex)
{
    // works for approach using task.whenall
    Console.WriteLine(ex);
    Assert.True(failedList.Count == 4);
    Assert.True(successList.Count == 6);
    return;
}

Assert.Fail();

This test fails because ActionBlock stops immediately when exceptions occur.此测试失败,因为ActionBlock在发生异常时立即停止。 I found this being an issue on github: Dataflow: Add options for Task Faulting .我发现这是 github 上的一个问题: Dataflow: Add options for Task Faulting Apparently this behavior is not configurable.显然,这种行为是不可配置的。

Task.WhenAll() in combination with an extension method like this: Task.WhenAll()结合这样的扩展方法:

public static async Task PreserveAllExceptions(this Task task)
{
    try
    {
        await task.ConfigureAwait(false);
    }
    catch
    {
        throw task.Exception;
    }
}

Wraps all(!) exceptions in an AggregateException but continues processing:将所有(!)异常包装在AggregateException但继续处理:

  await Task.WhenAll(task1,task2).PreserveAllExceptions().ConfigureAwait(false);

Is there a handy way to achieve this using ActionBlock ?有没有一种方便的方法可以使用ActionBlock来实现这一点?

UPDATE : To clarify:更新:澄清:

  1. I am not planning to use sempahore for throtteling (Why would I?) since there is already such an option in ExecutionDataflowBlockOptions我不打算使用信号量进行节流(我为什么要这样做?),因为ExecutionDataflowBlockOptions已经有这样的选项
  2. Code snippet is just a dummy to demonstrate the "problem";代码片段只是一个演示“问题”的假人; Task.Run() is only used as placeholder for an actual async functon. Task.Run()仅用作实际异步函数的占位符。
  3. What I really want to do is the following: Process all messages in a parallel fashion.我真正想做的是:以并行方式处理所有消息。 Do not cancel further message processing on error.不要在出错时取消进一步的消息处理。 After processing all messages, return and indicate that at least on error occured and return all errors -> Exactly how my Task.WhenAll() with AggregateException is working.处理Task.WhenAll()所有消息后,返回并指出至少发生了错误并返回所有错误 -> 带有 AggregateException 的Task.WhenAll()究竟是如何工作的。 I know I simply could try{}catch{} within my ActionBlock and somehow store the exceptions but I was wondering if there would be any configuration possiblity to make this easier.我知道我可以在ActionBlock try{}catch{}并以某种方式存储异常,但我想知道是否有任何配置可能使这更容易。 Anyhow, it's not that big of a deal to just use a try catch and collect exceptions everywhere where I make use of ActionBlock .无论如何,在我使用ActionBlock任何地方使用 try catch 和收集异常并不是什么大不了的事。 I just find that the Task.WhenAll() + PreserveException Extension is cleaner for my purpose.我只是发现Task.WhenAll() + PreserveException Extension 对我来说更干净。

It's unclear what the question asks.目前还不清楚问题问的是什么。 What's clear though is that the ActionBlock is misused.但很明显的是,ActionBlock 被滥用了。 There's no need for Task.Run since the ActionBlock already uses one or more worker tasks.不需要Task.Run因为 ActionBlock 已经使用了一个或多个工作任务。 There's no need for semaphores, since ActionBlock (and the other blocks) already supports throttling by limiting the number of worker tasks and the input queues.不需要信号量,因为 ActionBlock(和其他块)已经通过限制工作任务和输入队列的数量来支持节流。

The code seems to be trying to use exceptions as a control flow mechanism too, which would be wrong in procedural code too.代码似乎也试图使用异常作为控制流机制,这在过程代码中也是错误的。

Exceptions aren't meant to escape the block.异常并不是为了逃避这个块。 Dataflow is a completely different computing paradigm from the common procedural paradigm - there are no functions calling each other, so there's no caller to receive and handle the exception. Dataflow 是一种与通用过程范式完全不同的计算范式——没有函数相互调用,因此没有调用者来接收和处理异常。

In a dataflow, blocks pass messages to each other, in a single direction.在数据流中,块以单一方向相互传递消息。 Blocks are combined in pipelines or networks, receiving messages, processing them and passing them to any connected blocks.块在管道或网络中组合,接收消息,处理它们并将它们传递给任何连接的块。 If an exception occurs there's no "caller" that could receive that exception.如果发生异常,则没有可以接收该异常的“调用者”。 Unhandled exceptions are catastrophic and bring down the entire pipeline - not just a single block, but any downstream block it's linked to with PropagateCompletion set to true.未处理的异常是灾难性的,会导致整个管道中断——不仅仅是单个块,而是它链接到的任何下游块,并将PropagateCompletion设置为 true。 Upstream block will never know about this though, leading to unexpected situations.但是,上游块永远不会知道这一点,从而导致意外情况。

Throttling节流

Throttling with an ActionBlock is easy - for starters, all blocks use just a single worker task.使用 ActionBlock 进行节流很容易——对于初学者来说,所有块只使用一个工作任务。 One can throttle upstream callers by limiting their input buffer and using await block.SendAsync() instead of block.Post .可以通过限制上游调用者的输入缓冲区并使用await block.SendAsync()而不是block.Post来限制上游调用者。 There's no need for Task.Run as the block already uses worker tasks :不需要Task.Run因为块已经使用了工作任务:

var options=new ExecutionDataflowBlockOptions 
{ 
    MaxDegreeOfParallelism=2, 
    BoundedCapacity=2
};
var block =new ActionBlock<Message>(processMessage,options);

...
async Task processMessage(Message msg) { ...}

That's enough to allow only two concurrent operations, and stop posters if there are two messages waiting already.这足以只允许两个并发操作,如果已经有两条消息在等待,则停止发布。 If the buffer is at capacity, SendAsync in the following code will wait until a slot becomes available:如果缓冲区SendAsync ,以下代码中的SendAsync将等待插槽可用:

foreach(var msg in someInputCollection)
{
    await block.SendAsync(msg);
}

That's it.就是这样。 The block will process 2 messages concurrently (the default is just 1) and accept only 2 messages at a time in its input buffer.该块将同时处理 2 条消息(默认值为 1 条),并且在其输入缓冲区中一次仅接受 2 条消息。 If the buffer is at capacity, the posting loop will wait.如果缓冲区已满,则发布循环将等待。

Quick & dirty rate limiting can be achieved by adding a delay in the processing method :可以通过在处理方法中添加延迟来实现快速和脏速率限制:

var block =new ActionBlock<Message>(msg=>{
    await Task.Delay(200);
    await processMessage(msg);
},options);

Conditional routing条件路由

The question's code seems to be using exceptions to implement control flow.问题的代码似乎使用异常来实现控制流。 That would be wrong in any library or paradigm.这在任何库或范式中都是错误的。 Since dataflows work in networks the equivalent of control flow is conditional routing.由于数据流在网络中工作,因此控制流的等价物是条件路由。

This is also available, through the LinkTo overloads that accept a predicate parameter that decides whether a message should be passed down a specific link.这也是可用的,通过LinkTo重载接受一个predicate参数,该参数决定消息是否应该向下传递到特定链接。

In the question's case, assuming there's an upstream TransformBlock that produces integers, LinkTo can be used to route messages to different BufferBlocks:在问题的情况下,假设有一个产生整数的上游TransformBlockLinkTo可用于将消息路由到不同的 BufferBlocks:

var success=new BufferBlock<int>();
var failure=new BufferBlock<int>();

var block=new TransformBlock<Message,int>(...);
//Success if x>=5
block.LinkTo(success,x=>x>=5);
//By default, everything else goes to Failure
block.LinkTo(failure);

That's it.就是这样。 The only "trick" is that the predicates should cover all options, otherwise messages will remain stuck in block 's output buffer.唯一的“技巧”是谓词应该涵盖所有选项,否则消息将停留在block的输出缓冲区中。 It helps to use a default link after all others to ensure no messages are left unhandled.在所有其他链接之后使用default链接有助于确保没有未处理的消息。

Error Handling错误处理

Blocks shouldn't allow exceptions to escape.块不应该允许异常逃逸。 There are several error handling strategies that depend on what the application wants to do.有几种错误处理策略取决于应用程序想要做什么。

Handle and log处理和记录

One option is to handle them and log them in place, the same way one would handle errors in a web application:一种选择是处理它们并将它们记录到位,就像处理 Web 应用程序中的错误一样:

var block =new ActionBlock(msg=>{ try { await processMessage(msg); } catch(Exception exc) { _logger.LogError(exc,....); } },options); var block =new ActionBlock(msg=>{ try { await processMessage(msg); } catch(Exception exc) { _logger.LogError(exc,....); } },options);

Post to another block发布到另一个区块

Another possibility is to post the exception, and possibly information about the incoming message, to another block directly.另一种可能性是将异常以及可能有关传入消息的信息直接发布到另一个块。 That block could log the error and messag or retry it after a delay.该块可以记录错误和消息,或在延迟后重试。 There may be a different pipeline behind that block to retry messages with increasing delays before sending them to a dead-letter buffer, similar to what one would do with message queues:在将消息发送到死信缓冲区之前,该块后面可能有一个不同的管道来重试具有增加延迟的消息,类似于对消息队列所做的事情:

var block =new ActionBlock<Message>(msg=>{
    try
    {
        await processMessage(msg);
    }
    catch(SomeRetriableException exc)
    {
        _retryBlock.Post(new RetryMsg(msg,exc));
    }
    catch(Exception exc)
    {
       
       _logger.LogError(exc,....);
    }
},options);

The strategy to use depends on what the application does.要使用的策略取决于应用程序的作用。 If the ActionBlock is used as a simple background worker, it may be enough to just log.如果 ActionBlock 被用作一个简单的后台工作者,它可能就足够了。

Wrap and route包裹和路由

In a more advanced scenario messages can be wrapped in an Envelope<> that carries the message and possibly any exceptions.在更高级的场景中,消息可以包装在Envelope<> ,它携带消息和可能的任何异常。 Routing can be used to separate success from failure messages :路由可用于将成功消息与失败消息分开:

class Envelope<T>
{
    public T Message{get;}
    public Exception Error {get;}
    
    public Envelope (T msg)
    {
        Message=msg;
    }
    
    public Envelope(T msg,Exception err)
    {
        Message=msg;
        Error=err;
    }
}

The block now returns an Envelope :该块现在返回一个 Envelope :

var block=new TransformBlock<Envelope<Message>,Envelope<int>>(env=>{
    try
    {
        var msg=env.Message;
        ....
        return new Envelope(6);
    }
    catch(Exception exc)
    {
        return new Envelope(msg,exc);
    }
});

This allows routing errors to an errorBlock using conditional routing :这允许使用条件路由将错误路由到errorBlock

var errorBlock = ActionBlock<Envelope<Message>>(...);

var success=new BufferBlock<int>();
var failure=new BufferBlock<int>();

//Send errors to `errorBlock`
block.LinkTo(errorBlock,env=>env.Error!=null);

//Success if x>=5
block.LinkTo(success,x=>x.Message>=5);
//By default, everything else goes to Failure
block.LinkTo(failure);

There is no easy way to aggregate all exceptions and propagate them through the Completion property of the ActionBlock .没有简单的方法可以聚合所有异常并通过ActionBlockCompletion属性传播它们。 Unfortunately the TPL Dataflow components are not easily extensible.不幸的是,TPL 数据流组件不容易扩展。 You could do it if you really wanted to, by encapsulating an ActionBlock inside a custom block and customizing the Completion of this block.如果您真的想这样做,您可以通过将ActionBlock封装在自定义块中并自定义该块的Completion来实现。 For example:例如:

public class MyActionBlock<TInput> : ITargetBlock<TInput>
{
    private readonly ActionBlock<TInput> _actionBlock;
    private readonly ConcurrentQueue<Exception> _exceptions;

    //...

    public Task Completion
    {
        get
        {
            return _actionBlock.Completion.ContinueWith(t =>
            {
                if (_exceptions.Count > 0)
                    throw new AggregateException(_exceptions);
            });
        }
    }
}

...but this simple code will not propagate cancellation, neither will propagate any exceptions passed through the Fault method of the IDataflowBlock interface. ...但是这个简单的代码不会传播取消,也不会传播通过IDataflowBlock接口的Fault方法传递的任何异常。 So you'll have to spend considerable amount of effort in order to make it work correctly in all cases, and it's doubtful that the investment will pay of.因此,您必须花费大量精力才能使其在所有情况下都能正常工作,而且投资是否会得到回报值得怀疑。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM