How to return AggregateException from async method

后端 未结 3 1350
野的像风
野的像风 2021-01-24 17:06

I got an async method working like an enhanced Task.WhenAll. It takes a bunch of tasks and returns when all are completed.

public async Task MyWhenA         


        
相关标签:
3条回答
  • 2021-01-24 17:15

    async methods are designed to only every set at most a single exception on the returned task, not multiple.

    This leaves you with two options, you can either not use an async method to start with, instead relying on other means of performing your method:

    public Task MyWhenAll(Task t1, Task t2)
    {
        return Task.Delay(TimeSpan.FromMilliseconds(100))
            .ContinueWith(_ => Task.WhenAll(t1, t2))
            .Unwrap();
    }
    

    If you have a more complex method that would be harder to write without using await, then you'll need to unwrap the nested aggregate exceptions, which is tedious, although not overly complex, to do:

        public static Task UnwrapAggregateException(this Task taskToUnwrap)
        {
            var tcs = new TaskCompletionSource<bool>();
    
            taskToUnwrap.ContinueWith(task =>
            {
                if (task.IsCanceled)
                    tcs.SetCanceled();
                else if (task.IsFaulted)
                {
                    if (task.Exception is AggregateException aggregateException)
                        tcs.SetException(Flatten(aggregateException));
                    else
                        tcs.SetException(task.Exception);
                }
                else //successful
                    tcs.SetResult(true);
            });
    
            IEnumerable<Exception> Flatten(AggregateException exception)
            {
                var stack = new Stack<AggregateException>();
                stack.Push(exception);
                while (stack.Any())
                {
                    var next = stack.Pop();
                    foreach (Exception inner in next.InnerExceptions)
                    {
                        if (inner is AggregateException innerAggregate)
                            stack.Push(innerAggregate);
                        else
                            yield return inner;
                    }
                }
            }
    
            return tcs.Task;
        }
    
    0 讨论(0)
  • 2021-01-24 17:18

    Use a TaskCompletionSource.

    The outermost exception is created by .Wait() or .Result - this is documented as wrapping the exception stored inside the Task inside an AggregateException (to preserve its stack trace - this was introduced before ExceptionDispatchInfo was created).

    However, Task can actually contain many exceptions. When this is the case, .Wait() and .Result will throw an AggregateException which contains multiple InnerExceptions. You can access this functionality through TaskCompletionSource.SetException(IEnumerable<Exception> exceptions).

    So you do not want to create your own AggregateException. Set multiple exceptions on the Task, and let .Wait() and .Result create that AggregateException for you.

    So:

    var tcs = new TaskCompletionSource<object>();
    tcs.SetException(new[] { t1.Exception, t2.Exception });
    return tcs.Task;
    

    Of course, if you then call await MyWhenAll(..) or MyWhenAll(..).GetAwaiter().GetResult(), then it will only throw the first exception. This matches the behaviour of Task.WhenAll.

    This means you need to pass tcs.Task up as your method's return value, which means your method can't be async. You end up doing ugly things like this (adjusting the sample code from your question):

    public static Task MyWhenAll(Task t1, Task t2)
    {
        var tcs = new TaskCompletionSource<object>();
        var _ = Impl();
        return tcs.Task;
    
        async Task Impl()
        {
            await Task.Delay(10);
            try
            {
                await Task.WhenAll(t1, t2);
                tcs.SetResult(null);
            }
            catch
            {
                tcs.SetException(new[] { t1.Exception, t2.Exception });
            }
        }
    }
    

    At this point, though, I'd start to query why you're trying to do this, and why you can't use the Task returned from Task.WhenAll directly.

    0 讨论(0)
  • 2021-01-24 17:37

    This answer combines ideas from Servy's and canton7's solutions. As an example is used an actually useful method named Retry, that attempts to run a task multiple times before reporting failure. In case that the maximum number of attempts has been reached, the returned task transitions to a faulted state, containing all the exceptions bundled inside an AggregateException. So this method behaves very similar to the built-in Task.WhenAll method. Here is the Retry method:

    public static Task<TResult> Retry<TResult>(Func<Task<TResult>> taskFactory,
        int maxAttempts)
    {
        if (taskFactory == null) throw new ArgumentNullException(nameof(taskFactory));
        if (maxAttempts < 1) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
        return FlattenTopAggregateException(Implementation());
    
        async Task<TResult> Implementation()
        {
            var exceptions = new List<Exception>();
            while (true)
            {
                var task = taskFactory();
                try
                {
                    return await task.ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                    if (exceptions.Count >= maxAttempts)
                        throw new AggregateException(exceptions);
                }
            }
        }
    }
    

    The method is separated into two parts: 1) argument validation and 2) implementation. The implementation is an asynchronous local function, that throws an AggregateException. The resulting task of the local function is not well behaved because on failure it contains a nested AggregateException. To fix this problem the task is flattened before returned by the outer Retry method. It is flattened only at top level, meaning that a possibly deep AggregateException hierarchy will become shortened only by one level. The purpose of flattening is just to eliminate the top-level nesting that is caused by throwing an AggregateException from an async method. Here is the flattening method:

    private static Task<TResult> FlattenTopAggregateException<TResult>(Task<TResult> task)
    {
        var tcs = new TaskCompletionSource<TResult>();
        HandleTaskCompletion();
        return tcs.Task;
    
        async void HandleTaskCompletion()
        {
            try
            {
                var result = await task.ConfigureAwait(false);
                tcs.SetResult(result);
            }
            catch (OperationCanceledException ex) when (task.IsCanceled)
            {
                // Unfortunately the API SetCanceled(CancellationToken) is missing
                if (!tcs.TrySetCanceled(ex.CancellationToken)) tcs.SetCanceled();
            }
            catch (Exception ex)
            {
                var taskException = task.Exception;
                if (taskException == null || taskException.InnerExceptions.Count == 0)
                {
                    // Handle abnormal case
                    tcs.SetException(ex);
                }
                else if (taskException.InnerExceptions.Count == 1
                    && taskException.InnerException is AggregateException aex
                    && aex.InnerExceptions.Count > 0)
                {
                    // Fix nested AggregateException
                    tcs.SetException(aex.InnerExceptions);
                }
                else
                {
                    // Keep it as is
                    tcs.SetException(taskException.InnerExceptions);
                }
            }
        }
    }
    

    This method contains an async void local function. The reason for this is for ensuring that any bugs that may lurk in the implementation will be surfaced. In case async void methods are undesirable, it can be trivially converted to a fire-and-forget task.

    Here is a usage example of the Retry method. The returned task is awaited inside a try-catch block. The Exception caught by the block is ignored, and the Exception property of the task is observed instead:

    var task = Retry(async () =>
    {
        Console.WriteLine("Try to do something");
        await Task.Delay(100, new CancellationToken(true));
        return "OK";
    }, maxAttempts: 3);
    
    try
    {
        await task;
    }
    catch
    {
        if (task.IsFaulted)
        {
            Console.WriteLine($"Errors: {task.Exception.InnerExceptions.Count}");
            Console.WriteLine($"{task.Exception.Message}");
        }
        else
        {
            Console.WriteLine("Not faulted");
        }
    }
    

    Output:

    Try to do something
    Try to do something
    Try to do something
    Errors: 3
    One or more errors occurred. (A task was canceled.) (A task was canceled.) (A task was canceled.)

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