Как вернуть AggregateException из асинхронного метода - PullRequest
0 голосов
/ 15 апреля 2019

Я получил асинхронный метод, работающий как расширенный Task.WhenAll. Он берет кучу задач и возвращается, когда все выполнено.

public async Task MyWhenAll(Task[] tasks) {
    ...
    await Something();
    ...

    // all tasks are completed
    if (someTasksFailed)
        throw ??
}

Мой вопрос заключается в том, как получить метод, возвращающий задачу, похожую на ту, которая была возвращена из Task.WhenAll, если одна или несколько задач не были выполнены?

Если я соберу исключения и сгенерирую AggregateException, они будут помещены в другое исключение AggregateException.

Редактировать: полный пример

async Task Main() {
    try {
        Task.WhenAll(Throw(1), Throw(2)).Wait();
    }
    catch (Exception ex) {
        ex.Dump();
    }

    try {
        MyWhenAll(Throw(1), Throw(2)).Wait();
    }
    catch (Exception ex) {
        ex.Dump();
    }
}

public async Task MyWhenAll(Task t1, Task t2) {
    await Task.Delay(TimeSpan.FromMilliseconds(100));
    try {
        await Task.WhenAll(t1, t2);
    }
    catch {
        throw new AggregateException(new[] { t1.Exception, t2.Exception });
    }
}
public async Task Throw(int id) {
    await Task.Delay(TimeSpan.FromMilliseconds(100));
    throw new InvalidOperationException("Inner" + id);
}

Для Task.WhenAll исключение составляет AggregateException с двумя внутренними исключениями.

Для MyWhenAll исключение составляет AggregateException с одним внутренним AggregateException с 2 внутренними исключениями.

Редактировать: Почему я это делаю

Мне часто нужно вызывать API пейджинга: я хочу ограничить количество одновременных подключений.

Фактические сигнатуры методов:

public static async Task<TResult[]> AsParallelAsync<TResult>(this IEnumerable<Task<TResult>> source, int maxParallel)
public static async Task<TResult[]> AsParallelUntilAsync<TResult>(this IEnumerable<Task<TResult>> source, int maxParallel, Func<Task<TResult>, bool> predicate)

Это означает, что я могу делать пейджинг вот так

var pagedRecords = await Enumerable.Range(1, int.MaxValue)
                                   .Select(x => GetRecordsAsync(pageSize: 1000, pageNumber: x)
                                   .AsParallelUntilAsync(maxParallel: 5, x => x.Result.Count < 1000);
var records = pagedRecords.SelectMany(x => x).ToList();

Все работает нормально, агрегат внутри агрегата - лишь незначительное неудобство.

Ответы [ 2 ]

1 голос
/ 15 апреля 2019

async методы предназначены только для каждого набора не более одного исключения в возвращаемой задаче, а не нескольких.

Это оставляет вам две опции, вы можете либо не использовать метод async дляначните с того, что вместо этого полагайтесь на другие способы выполнения вашего метода:

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

Если у вас есть более сложный метод, который будет сложнее написать без использования await, то вам нужно будет развернуть вложенныйсовокупные исключения, которые утомительны, хотя и не слишком сложны для выполнения:

    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 голосов
/ 15 апреля 2019

Используйте TaskCompletionSource.

Самое внешнее исключение создается .Wait() или .Result - это задокументировано как упаковка исключения, хранящегося внутри Задачи, внутри AggregateException (чтобы сохранить его трассировку стека - это было введено до создания ExceptionDispatchInfo) .

Однако Task может содержать много исключений. В этом случае .Wait() и .Result будут выбрасывать AggregateException, который содержит несколько InnerExceptions. Вы можете получить доступ к этой функции через TaskCompletionSource.SetException(IEnumerable<Exception> exceptions).

Итак, вы не хотите создать свой AggregateException. Установите несколько исключений для Задачи и позвольте .Wait() и .Result создать это AggregateException для вас.

Итак:

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

Конечно, если вы затем позвоните await MyWhenAll(..) или MyWhenAll(..).GetAwaiter().GetResult(), то он выдаст только первое исключение. Это соответствует поведению Task.WhenAll.

Это означает, что вам нужно передать tcs.Task в качестве возвращаемого значения вашего метода, что означает, что ваш метод не может быть async. В конечном итоге вы делаете такие ужасные вещи (корректируя пример кода из вашего вопроса):

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

Однако в этот момент я бы начал спрашивать, почему вы пытаетесь это сделать, и почему вы не можете использовать Task, возвращенный из Task.WhenAll напрямую.

...