为什么在使用TPL的异步重试包装器中没有处理异常?

时间:2014-08-09 17:17:57

标签: c# task-parallel-library

我在.Net 4上,并有以下Async“Retry”包装器:

public static Task<T> Retry<T, TException>(Func<T> work, Action<TException> onException, TimeSpan retryInterval, int maxExecutionCount = 3) where TException : Exception
{
    for (var i = 0; i < maxExecutionCount; i++)
    {
        try
        {
            return Task.Factory.StartNew(work);
        } 
        catch (AggregateException ae)
        {
            ae.Handle(e =>
            {
                if (e is TException)
                {
                    // allow program to continue in this case
                    // do necessary logging or whatever
                    if (onException != null) { onException((TException)e); }

                    Thread.Sleep(retryInterval);
                    return true;
                }
                throw new RetryWrapperException("Unexpected exception occurred", ae);
            });
        }
    }
    var msg = "Retry unsuccessful after: {0} attempt(s)".FormatWith(maxExecutionCount);
    throw new RetryWrapperException(msg);
}

我正在尝试使用:

[TestFixture]
public class RetryWrapperTest
{
    private static int _counter;

    private Func<string> _work;
    private Action<InvalidOperationException> _onException;
    private TimeSpan _retryInterval;
    private int _maxRetryCount;

    [TestFixtureSetUp]
    public void SetUp()
    {
        _counter = 0;

        _work =  GetSampleResult;
        _onException = e => Console.WriteLine("Caught the exception: {0}", e);
        _retryInterval = TimeSpan.FromSeconds(5);
        _maxRetryCount = 4;
    }

    [Test]
    public void Run()
    {
        var resultTask = RetryWrapper.Retry(_work, _onException, _retryInterval, _maxRetryCount);
        Console.WriteLine("This wrapper doesn't block!");

        // Why is this line throwing? I expect the exception to be handled
        // by the wrapper
        var result = resultTask.Result;
        result.Should().NotBeNullOrWhiteSpace();
        result.Should().Be("Sample result!");
    }

    private static string GetSampleResult()
    {
        if (_counter < 3)
        {
            _counter++;
            throw new InvalidOperationException("Baaah!");
        }
        return "Sample result!";
    }
}

然而,抛出 AggregateException 而不是被捕获。使用这个包装器的全部意义是不必在var result = resultTask.Result;周围放置try-catch我做错了什么?

请注意我不能使用async-await或Bcl.Async,因为我在.Net 4,VS 2010

3 个答案:

答案 0 :(得分:4)

你想做的事并不容易。实际上,为了解决这类问题(Rackspace Threading Library,Apache 2.0许可证),我写了一个特别的库来“恰到好处”是非常具有挑战性的。

期望的行为

尽管代码的最终版本不会使用此功能,但使用async / await最容易描述代码的所需行为。我对此Retry方法进行了一些更改。

  1. 为了提高方法支持真正异步操作的能力,我将work参数从Func<T>更改为Func<Task<T>>
  2. 为避免不必要地阻塞线程(即使它是后台线程),我使用了Task.Delay而不是Thread.Sleep
  3. 我假设onException永远不会null。虽然对于使用async / await的代码而言,这种假设并不容易省略,但如果必要,可以为下面的实现解决
  4. 我假设work返回的任务进入Faulted状态,它将具有InnerExceptions属性,只包含1个异常。如果它包含多个异常,则将忽略除第一个异常之外的所有异常。虽然对于使用async / await的代码而言,这种假设并不容易省略,但如果必要,可以为下面的实现解决
  5. 以下是最终的实现。请注意,我可以使用for循环而不是while循环,但正如您将看到的那样会使后续步骤复杂化。

    public async Task<T> Retry<T, TException>(Func<Task<T>> work, Action<TException> onException, TimeSpan retryInterval, int maxExecutionCount)
        where TException : Exception
    {
        int count = 0;
        while (count < maxExecutionCount)
        {
            if (count > 0)
                await Task.Delay(retryInterval);
    
            count++;
    
            try
            {
                return await work();
            }
            catch (TException ex)
            {
                onException(ex);
            }
            catch (Exception ex)
            {
                throw new RetryWrapperException("Unexpected exception occurred", ex);
            }
        }
    
        string message = string.Format("Retry unsuccessful after: {0} attempt(s)", maxExecutionCount);
        throw new RetryWrapperException(message);
    }
    

    移植到.NET 4.0

    将此代码转换为.NET 4.0(甚至是.NET 3.5)使用Rackspace线程库的以下功能:

    • TaskBlocks.While:转换while循环。
    • CoreTaskExtensions.Select:用于在先前任务成功完成后执行同步操作。
    • CoreTaskExtensions.Then:用于在先前任务成功完成后执行异步操作。
    • CoreTaskExtensions.Catch(V1.1的新内容):用于异常处理。
    • DelayedTask.Delay(V1.1的新内容):针对Task.Delay
    • 的行为

    此实现与上述实现之间存在一些行为差异。特别是:

    • 如果work返回null,此方法返回的Task将转换为已取消的状态(如this test所示),由于NullReferenceException,上述方法将转换为故障状态。
    • 此实现的行为就好像在前一个实现中的每个ConfigureAwait(false)之前调用await一样。至少在我看来,这实际上并不是一件坏事。
    • 如果onException方法抛出异常,则此实现会将该异常包装在RetryWrapperException中。换句话说,此实现实际上是对此代码进行建模,而不是在期望行为部分中编写的块:

      try
      {
          try
          {
              return await work();
          }
          catch (TException ex)
          {
              onException(ex);
          }
      }
      catch (Exception ex)
      {
          throw new RetryWrapperException("Unexpected exception occurred", ex);
      }
      

    以下是最终的实施:

    public static Task<T> Retry<T, TException>(Func<Task<T>> work, Action<TException> onException, TimeSpan retryInterval, int maxExecutionCount)
        where TException : Exception
    {
        int count = 0;
        bool haveResult = false;
        T result = default(T);
    
        Func<bool> condition = () => count < maxExecutionCount;
        Func<Task> body =
            () =>
            {
                Task t1 = count > 0 ? DelayedTask.Delay(retryInterval) : CompletedTask.Default;
    
                Task t2 =
                    t1.Then(
                        _ =>
                        {
                            count++;
                            return work();
                        })
                    .Select(
                        task =>
                        {
                            result = task.Result;
                            haveResult = true;
                        });
    
                Task t3 =
                    t2.Catch<TException>(
                        (_, ex) =>
                        {
                            onException(ex);
                        })
                    .Catch<Exception>((_, ex) =>
                        {
                            throw new RetryWrapperException("Unexpected exception occurred", ex);
                        });
    
                return t3;
            };
    
        Func<Task, T> selector =
            _ =>
            {
                if (haveResult)
                    return result;
    
                string message = string.Format("Retry unsuccessful after: {0} attempt(s)", maxExecutionCount);
                throw new RetryWrapperException(message);
            };
    
        return
            TaskBlocks.While(condition, body)
            .Select(selector);
    }
    

    样品测试

    以下测试方法演示了上述代码的上述代码。

    [TestMethod]
    public void Run()
    {
        Func<Task<string>> work = GetSampleResultAsync;
        Action<InvalidOperationException> onException = e => Console.WriteLine("Caught the exception: {0}", e);
        TimeSpan retryInterval = TimeSpan.FromSeconds(5);
        int maxRetryCount = 4;
    
        Task<string> resultTask = Retry(work, onException, retryInterval, maxRetryCount);
        Console.WriteLine("This wrapper doesn't block");
    
        var result = resultTask.Result;
        Assert.IsFalse(string.IsNullOrWhiteSpace(result));
        Assert.AreEqual("Sample result!", result);
    }
    
    private static int _counter;
    
    private static Task<string> GetSampleResultAsync()
    {
        if (_counter < 3)
        {
            _counter++;
            throw new InvalidOperationException("Baaah!");
        }
    
        return CompletedTask.FromResult("Sample result!");
    }
    

    未来考虑因素

    如果您真的想要坚如磐石的实现,我建议您通过以下方式进一步修改代码。

    1. 支持取消。

      1. 添加CancellationToken cancellationToken参数作为Retry方法的最后一个参数。
      2. work的类型更改为Func<CancellationToken, Task<T>>
      3. cancellationToken参数传递给work,然后传递给DelayedTask.Delay
    2. 支持退避政策。您可以删除retryIntervalmaxExecutionCount参数,然后使用IEnumerable<TimeSpan>,或者可以合并IBackoffPolicy之类的界面以及BackoffPolicy等默认实现(麻省理工学院获得许可)。

答案 1 :(得分:1)

如果希望此包装器工作,则需要使用async / await或使用continuation编写catch块。这是因为在您尝试获取结果之前不会抛出异常。

要在.NET 4.0上使用async / await执行此操作,请下载Microsoft.Bcl.Async,它为您提供async / await支持4.0

public static async Task<T> Retry<T, TException>(Func<T> work, Action<TException> onException, TimeSpan retryInterval, int maxExecutionCount = 3) where TException : Exception
{
    for (var i = 0; i < maxExecutionCount; i++)
    {
        try
        {
            return await Task.Factory.StartNew(work);
        } 
        catch (AggregateException ae)
        {
            ae.Handle(e =>
            {
                if (e is TException)
                {
                    // allow program to continue in this case
                    // do necessary logging or whatever
                    if (onException != null) { onException((TException)e); }

                    Thread.Sleep(retryInterval);
                    return true;
                }
                throw new RetryWrapperException("Unexpected exception occurred", ae);
            });
        }
    }
    var msg = "Retry unsuccessful after: {0} attempt(s)".FormatWith(maxExecutionCount);
    throw new RetryWrapperException(msg);
}

我不确定没有async / await的确切方法是什么。我知道.ContunueWith(TaskContinuationOptions.OnlyOnFaulted有关,但我不确定具体细节。

答案 2 :(得分:0)

您的代码存在一些问题:

  1. 如前所述,您没有await执行Task,没有机会 完成并捕获异常,因为返回的Task即时传播给调用者。如果await不可用,则必须使用延续样式ContinueWith方法而不是此实现。

  2. 您在代码中使用Thread.Sleep。如你所知,有人可能会在他们的主线程中使用它,而不是理解为什么线程会突然卡住,这非常不可靠。

  3. 如果onException委托是假的,则使用throw new重新抛出异常,这将失去你方法的StackTrace,这是你真正追求的吗?一个更好的建议可能是保存它们并在重试结束时使用AggregationException

  4. This post有你需要的东西:

    public static Task StartNewDelayed(int millisecondsDelay, Action action) 
    { 
        // Validate arguments 
        if (millisecondsDelay < 0) 
            throw new ArgumentOutOfRangeException("millisecondsDelay"); 
        if (action == null) throw new ArgumentNullException("action"); 
    
        // Create the task 
        var t = new Task(action); 
        // Start a timer that will trigger it 
        var timer = new Timer( 
            _ => t.Start(), null, millisecondsDelay, Timeout.Infinite); 
        t.ContinueWith(_ => timer.Dispose());
        return t; 
    }
    
    private static Task<T> Retry<T>(Func<T> func, int retryCount, int delay, TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    Task.Factory.StartNewDelayed(delay).ContinueWith(t =>
                    {
                        Retry(func, retryCount - 1, delay,tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    }