写一个Rx“RetryAfter”扩展方法

时间:2013-09-24 10:03:19

标签: c# system.reactive

在书IntroToRx中,作者建议为I / O编写一个“智能”重试,在一段时间后重试I / O请求,如网络请求。

这是确切的段落:

  

添加到您自己的库的有用扩展方法可能是“返回”   关闭并重试“方法。我与之合作的团队已经找到了这样的方法   执行I / O时特别有用,特别是网络请求。该   概念是尝试,并在失败等待一段时间和   然后再试一次。您的此方法版本可能会考虑到   要重试的异常类型以及最大数量   重试的次数。你甚至可能希望延长到等待期   在每次后续重试中都不那么激进。

不幸的是,我无法弄清楚如何编写这种方法。 :(

6 个答案:

答案 0 :(得分:37)

执行退避重试的关键是deferred observables。延迟的observable将不会执行其工厂,直到有人订阅它。它将为每个订阅调用工厂,使其成为我们重试方案的理想选择。

假设我们有一个触发网络请求的方法。

public IObservable<WebResponse> SomeApiMethod() { ... }

出于这个小片段的目的,让我们将延迟定义为source

var source = Observable.Defer(() => SomeApiMethod());

每当有人订阅源代码时,它都会调用SomeApiMethod并启动新的Web请求。每当失败时重试它的天真方法是使用内置的重试操作符。

source.Retry(4)

虽然对API来说不是很好,但这并不是你所要求的。我们需要在每次尝试之间延迟发出请求。一种方法是使用delayed subscription

Observable.Defer(() => source.DelaySubscription(TimeSpan.FromSeconds(1))).Retry(4)

这不太理想,因为它会在第一次请求时添加延迟,让我们解决这个问题。

int attempt = 0;
Observable.Defer(() => { 
   return ((++attempt == 1)  ? source : source.DelaySubscription(TimeSpan.FromSeconds(1)))
})
.Retry(4)
.Select(response => ...)

暂停一秒钟并不是一个非常好的重试方法,所以让我们将该常量更改为一个接收重试计数并返回适当延迟的函数。指数退避很容易实现。

Func<int, TimeSpan> strategy = n => TimeSpan.FromSeconds(Math.Pow(n, 2));

((++attempt == 1)  ? source : source.DelaySubscription(strategy(attempt - 1)))

我们现在差不多完成了,我们只需要添加一种指定我们应该重试的异常的方法。让我们添加一个函数,给定异常返回是否有意义重试,我们称之为retryOnError。

现在我们需要编写一些可怕的代码,但请耐心等待。

Observable.Defer(() => {
    return ((++attempt == 1)  ? source : source.DelaySubscription(strategy(attempt - 1)))
        .Select(item => new Tuple<bool, WebResponse, Exception>(true, item, null))
        .Catch<Tuple<bool, WebResponse, Exception>, Exception>(e => retryOnError(e)
            ? Observable.Throw<Tuple<bool, WebResponse, Exception>>(e)
            : Observable.Return(new Tuple<bool, WebResponse, Exception>(false, null, e)));
})
.Retry(retryCount)
.SelectMany(t => t.Item1
    ? Observable.Return(t.Item2)
    : Observable.Throw<T>(t.Item3))

所有这些尖括号都可以编组一个我们不应该重试.Retry()的异常。我们已经将内部可观察量设为IObservable<Tuple<bool, WebResponse, Exception>>,其中第一个bool表示我们是否有响应或异常。如果retryOnError指示我们应该重试特定异常,则内部observable将抛出,并且将通过重试来获取。 SelectMany只是打开我们的元组,并使得到的可观察量再次为IObservable<WebRequest>

有关最终版本,请参阅我的gist with full source and tests。拥有这个运算符允许我们非常简洁地编写我们的重试代码

Observable.Defer(() => SomApiMethod())
  .RetryWithBackoffStrategy(
     retryCount: 4, 
     retryOnError: e => e is ApiRetryWebException
  )

答案 1 :(得分:11)

也许我过度简化了这种情况,但是如果我们看一下Retry的实现,它只是一个Observable.Catch,它包含了无数可观数的可观察数据集:

private static IEnumerable<T> RepeatInfinite<T>(T value)
{
    while (true)
        yield return value;
}

public virtual IObservable<TSource> Retry<TSource>(IObservable<TSource> source)
{
    return Observable.Catch<TSource>(QueryLanguage.RepeatInfinite<IObservable<TSource>(source));
}

因此,如果我们采用这种方法,我们可以在第一次收益后添加延迟。

private static IEnumerable<IObservable<TSource>> RepeateInfinite<TSource> (IObservable<TSource> source, TimeSpan dueTime)
{
    // Don't delay the first time        
    yield return source;

    while (true)
        yield return source.DelaySubscription(dueTime);
    }

public static IObservable<TSource> RetryAfterDelay<TSource>(this IObservable<TSource> source, TimeSpan dueTime)
{
    return RepeateInfinite(source, dueTime).Catch();
}

通过重试计数捕获特定异常的重载可以更简洁:

public static IObservable<TSource> RetryAfterDelay<TSource, TException>(this IObservable<TSource> source, TimeSpan dueTime, int count) where TException : Exception
{
    return source.Catch<TSource, TException>(exception =>
    {
        if (count <= 0)
        {
            return Observable.Throw<TSource>(exception);
        }

        return source.DelaySubscription(dueTime).RetryAfterDelay<TSource, TException>(dueTime, --count);
    });
}

请注意,此处的重载是使用递归。在第一次出现时,如果count类似于Int32.MaxValue,似乎可能出现StackOverflowException。但是,DelaySubscription使用调度程序来运行订阅操作,因此无法进行堆栈溢出(即使用“trampolining”)。我想通过查看代码并不是很明显。我们可以通过将DelaySubscription重载中的调度程序显式设置为Scheduler.Immediate,并传入TimeSpan.Zero和Int32.MaxValue来强制堆栈溢出。我们可以通过一个非立即调度程序来更明确地表达我们的意图,例如:

return source.DelaySubscription(dueTime, TaskPoolScheduler.Default).RetryAfterDelay<TSource, TException>(dueTime, --count);

更新:添加了重载以接收特定的调度程序。

public static IObservable<TSource> RetryAfterDelay<TSource, TException>(
    this IObservable<TSource> source,
    TimeSpan retryDelay,
    int retryCount,
    IScheduler scheduler) where TException : Exception
{
    return source.Catch<TSource, TException>(
        ex =>
        {
            if (retryCount <= 0)
            {
                return Observable.Throw<TSource>(ex);
            }

            return
                source.DelaySubscription(retryDelay, scheduler)
                    .RetryAfterDelay<TSource, TException>(retryDelay, --retryCount, scheduler);
        });
} 

答案 2 :(得分:2)

这是我正在使用的那个:

public static IObservable<T> DelayedRetry<T>(this IObservable<T> src, TimeSpan delay)
{
    Contract.Requires(src != null);
    Contract.Ensures(Contract.Result<IObservable<T>>() != null);

    if (delay == TimeSpan.Zero) return src.Retry();
    return src.Catch(Observable.Timer(delay).SelectMany(x => src).Retry());
}

答案 3 :(得分:2)

根据马库斯的回答,我写了以下内容:

public static class ObservableExtensions
{
    private static IObservable<T> BackOffAndRetry<T>(
        this IObservable<T> source,
        Func<int, TimeSpan> strategy,
        Func<int, Exception, bool> retryOnError,
        int attempt)
    {
        return Observable
            .Defer(() =>
            {
                var delay = attempt == 0 ? TimeSpan.Zero : strategy(attempt);
                var s = delay == TimeSpan.Zero ? source : source.DelaySubscription(delay);

                return s
                    .Catch<T, Exception>(e =>
                    {
                        if (retryOnError(attempt, e))
                        {
                            return source.BackOffAndRetry(strategy, retryOnError, attempt + 1);
                        }
                        return Observable.Throw<T>(e);
                    });
            });
    }

    public static IObservable<T> BackOffAndRetry<T>(
        this IObservable<T> source,
        Func<int, TimeSpan> strategy,
        Func<int, Exception, bool> retryOnError)
    {
        return source.BackOffAndRetry(strategy, retryOnError, 0);
    }
}

我更喜欢它,因为

  • 它不会修改attempts但会使用递归。
  • 它不使用retries,但将尝试次数传递给retryOnError

答案 4 :(得分:1)

这是我在研究Rxx如何做的时候提出的另一个稍微不同的实现。所以它主要是Rxx方法的缩减版本。

签名与Markus的版本略有不同。您指定要重试的Exception类型,延迟策略将获取异常和重试计数,因此每次连续重试可能会有更长的延迟等等。

我不能保证它是错误证明,或者是最好的方法,但似乎有效。

public static IObservable<TSource> RetryWithDelay<TSource, TException>(this IObservable<TSource> source, Func<TException, int, TimeSpan> delayFactory, IScheduler scheduler = null)
where TException : Exception
{
    return Observable.Create<TSource>(observer =>
    {
        scheduler = scheduler ?? Scheduler.CurrentThread;
        var disposable = new SerialDisposable();
        int retryCount = 0;

        var scheduleDisposable = scheduler.Schedule(TimeSpan.Zero,
        self =>
        {
            var subscription = source.Subscribe(
            observer.OnNext,
            ex =>
            {
                var typedException = ex as TException;
                if (typedException != null)
                {
                    var retryDelay = delayFactory(typedException, ++retryCount);
                    self(retryDelay);
                }
                else
                {
                    observer.OnError(ex);
                }
            },
            observer.OnCompleted);

            disposable.Disposable = subscription;
        });

        return new CompositeDisposable(scheduleDisposable, disposable);
    });
}

答案 5 :(得分:0)

这是我想出的那个。

没有想要将单个重试的项连接成一个序列,但是在每次重试时都将源序列作为一个整体发出 - 因此运算符返回IObservable<IObservable<TSource>>。如果不希望这样,可以简单地Switch()回到一个序列中。

(背景:在我的用例中,源是一个热门的热门序列,我GroupByUntil出现一个关闭组的项目。如果这个项目在两次重试之间丢失,则该组永远不会关闭,导致内存泄漏拥有一系列序列只允许对内部序列进行分组(或异常处理或......)。)

/// <summary>
/// Repeats <paramref name="source"/> in individual windows, with <paramref name="interval"/> time in between.
/// </summary>
public static IObservable<IObservable<TSource>> RetryAfter<TSource>(this IObservable<TSource> source, TimeSpan interval, IScheduler scheduler = null)
{
    if (scheduler == null) scheduler = Scheduler.Default;
    return Observable.Create<IObservable<TSource>>(observer =>
    {
        return scheduler.Schedule(self =>
        {
            observer.OnNext(Observable.Create<TSource>(innerObserver =>
            {
                return source.Subscribe(
                    innerObserver.OnNext,
                    ex => { innerObserver.OnError(ex); scheduler.Schedule(interval, self); },
                    () => { innerObserver.OnCompleted(); scheduler.Schedule(interval, self); });
            }));
        });
    });
}