重试上一个任务操作 TPL

Retry previous task action TPL

我想实现一个重试任务,它采用之前失败的任务操作并重复它。

这就是我目前所拥有的。然而,它只是重复任务出错的事实,而不是再次触发任务的操作。

public static async Task<T> Retry<T>(this Task<T> task, int retryCount, int delay, TaskCompletionSource<T> tcs = null)
{
    if (tcs == null)
    {
        tcs = new TaskCompletionSource<T>();
    }

    await task.ContinueWith(async _original =>
    {
        if (_original.IsFaulted)
        {
            if (retryCount == 0)
            {
                tcs.SetException(_original.Exception.InnerExceptions);
            }
            else
            {
                Console.WriteLine("Unhandled exception. Retrying...");

                await Task.Delay(delay).ContinueWith(async t =>
                {
                    await Retry(task, retryCount - 1, delay, tcs);
                });
            }
        }
        else
            tcs.SetResult(_original.Result);
    });
    return await tcs.Task;
}

我试图通过一点思考来获得动作。然而,一旦任务完成,操作似乎设置为空。

var action = task
    .GetType()
    .GetField("m_action", BindingFlags.NonPublic | BindingFlags.Instance)
    .GetValue(task) as Action;

理想情况下,我希望我的实现看起来像这样:

try
{
    await MakeFailure().Retry(5, 1000);
}
catch (Exception ex)
{
    Console.WriteLine("I had an exception");
}

这可能不可能,但我想在将代码重构为 Retry(Func<T> task)

之前确定

那里有一个非常棒的库,您可以在不编写自己的代码的情况下使用它。它被称为瞬态故障应用程序块。但我会首先评估块中名为 TransientFaultHandling.Core.

的单个库

它的使用方式与您上面的代码非常相似。这是一个简单的例子:

using System;
using Microsoft.Practices.TransientFaultHandling;

namespace Whosebug
{
    class Program
    {
        internal class MyTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy
        {
            public bool IsTransient(Exception ex)
            {
                return true;
            }
        }

        private static void Main(string[] args)
        {
            const int retryCount = 5;
            const int retryIntervalInSeconds = 1;

            // define the strategy for retrying
            var retryStrategy = new FixedInterval(
                retryCount,
                TimeSpan.FromSeconds(retryIntervalInSeconds));

            // define the policy 
            var retryPolicy =
                new RetryPolicy<MyTransientErrorDetectionStrategy>(retryStrategy);

            retryPolicy.Retrying += retryPolicy_Retrying;

            for (int i = 0; i < 50; i++)
            {
                // try this a few times just to illustrate

                try
                {
                    retryPolicy.ExecuteAction(SomeMethodThatCanSometimesFail);

                    // (the retry policy has async support as well)
                }
                catch (Exception)
                {
                    // if it got to this point, your retries were exhausted
                    // the original exception is rethrown
                    throw;
                }
            }

            Console.WriteLine("Press Enter to Exit");

            Console.ReadLine();
        }

        private static void SomeMethodThatCanSometimesFail()
        {
            var random = new Random().Next(1, 4);

            if (random == 2)
            {
                const string msg = "randomFailure";

                Console.WriteLine(msg);

                throw new Exception(msg);
            }
        }

        private static void retryPolicy_Retrying(object sender, RetryingEventArgs e)
        {
            Console.WriteLine("retrying");
        }
    }
}

您遇到的问题是,一旦您的 Task<T> 处于飞行状态,就无法撤消或重试。您必须以 Func<Task<T>> 开头才能重试。

现在您可以直接使用 TPL,但我建议您使用 Microsoft 的 Reactive Framework 来构建您需要的功能。比TPL强多了。

鉴于 Func<Task<T>> 这就是您需要的:

Func<Task<T>> taskFactory = ...
int retryCount = 5;
Task<T> retryingTask = Observable.FromAsync(taskFactory).Retry(retryCount).ToTask();

我用这段代码测试了这个:

var i = 0;

Func<Task<int>> taskFactory = () => Task.Run(() =>
{
    if (i++ == 0)
        throw new Exception("Foo");
    return i;
});

int retryCount = 5;
Task<int> retryingTask = Observable.FromAsync(taskFactory).Retry(retryCount).ToTask();

Console.WriteLine(retryingTask.Result);

Reactive Framework 可以让您做更多的事情——它是一个非常强大的库——但它确实使这项任务变得非常简单。您可以通过 NuGet "Rx-Main" 获取位。

Not completely against it. But it changes the flow of the code to a fault first layout which I don't like

考虑你的类型。 Task代表一个异步操作。在异步世界中,Task代表一个异步操作已经开始Task 不是你能做到的 "retry".

另一方面,Func<Task>表示可以启动的异步操作。或者重新启动。这就是您需要处理的问题。

一旦您使用了合适的类型,代码就很简单了:

public static async Task<T> Retry<T>(Func<Task<T>> action, int retryCount, int delay)
{
  while (retryCount > 0)
  {
    try
    {
      return await action().ConfigureAwait(false);
    }
    catch (Exception ex)
    {
      await Task.Delay(delay).ConfigureAwait(false);
      --retryCount;
    }
  }
  return await action().ConfigureAwait(false);
}

与其他回答者一样,我建议您使用专门为此设计的库。 Transient Fault Handling Application Block and Polly 是两个很好的例子。