【问题标题】:Retry previous task action TPL重试上一个任务操作 TPL
【发布时间】:2015-08-26 07:41:19
【问题描述】:

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

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

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

我试图通过一点反思来获得行动。但是,似乎一旦任务完成,操作就会设置为 null。

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&lt;T&gt; task)之前确定一下

【问题讨论】:

  • 您是否反对不将此作为扩展方法并将方法签名更改为static async Task&lt;T&gt; Retry&lt;T&gt;(Func&lt;Task&lt;T&gt;&gt; taskFactory, int retryCount, ...
  • 不完全反对。但它将代码流更改为故障优先布局,我不喜欢 Retry(async () =&gt; await MakeFailure(),5,1000) 不如 await MakeFailure().Retry(5,1000) 干净
  • 不要试图破解 TPL 来做一些不该做的事情。在它上面创建一个抽象。

标签: c# task-parallel-library


【解决方案1】:

并不完全反对。但是它将代码流更改为我不喜欢的故障优先布局

考虑你的类型。 Task 表示异步操作。在异步世界中,Task 代表一个异步操作已经开始Task 不是你可以“重试”的东西。

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

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

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 BlockPolly 就是两个很好的例子。

【讨论】:

  • 在使用 TPL 时,我同意这是最好的答案。我希望一个任务保留一些关于它正在/正在处理的操作的细节,但它似乎没有。
  • @JonathanSheely:在现代 (async/await) 代码中,大多数任务甚至没有动作。
【解决方案2】:

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

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

using System;
using Microsoft.Practices.TransientFaultHandling;

namespace Stackoverflow
{
    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");
        }
    }
}

【讨论】:

  • 虽然这个概念是合理的。对于这个框架,我发现 API 的冗长性质非常冗长。一个很好的答案还是不错的
【解决方案3】:

您遇到的问题是,一旦您有一个 Task&lt;T&gt; 在运行中,它就无法撤消或重试。您必须以 Func&lt;Task&lt;T&gt;&gt; 开头才能重试。

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

给定Func&lt;Task&lt;T&gt;&gt;,这就是您所需要的:

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

反应式框架可以让你做更多事情——它是一个非常强大的库——但它确实让这项任务变得非常简单。您可以 NuGet "Rx-Main" 来获取这些位。

【讨论】:

  • 我已经在我的许多其他项目中转而使用 Rx。这正是为什么我想知道是否有一种方法可以在 TPL 中仅针对重试例程进行速记实现。
  • @JonathanSheely - 很公平。比起 TPL,我更喜欢 Rx。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-15
  • 1970-01-01
  • 1970-01-01
  • 2021-12-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多