执行退避重试的关键是deferred observables。延迟observable将不会执行它的工厂直到有人订阅它,并且它会为每个订阅调用工厂,使它成为我们重试的理想选择场景。
假设我们有触发的网络请求的方法。
public IObservable<WebResponse> SomeApiMethod() { ... }
对于这个小片段的目的,让我们定义为递延source
var source = Observable.Defer(() => SomeApiMethod());
每当有人订阅源时,它将调用SomeApiMethod并启动一个新的Web请求。无论何时失败都会重试它的天真方式是使用内置的Retry运算符。
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指示我们应该重试某个特定的异常,那么内部可观察值将会抛出,并且会被重试拾取。 SelectMany只是展开我们的元组,并使得可观测值再次变为IObservable<WebRequest>
。
查看我的gist with full source and tests的最终版本。有了这个操作符可以让我们写我们重试代码相当简洁
Observable.Defer(() => SomApiMethod())
.RetryWithBackoffStrategy(
retryCount: 4,
retryOnError: e => e is ApiRetryWebException
)
伟大的东西马库斯数量。 –
它看起来像我这个实现,源observable从不退订。这是一个有点难以粘贴在这里,但试试这个,你会看到的间隔保持滴答声:。 Observable.Interval(TimeSpan.FromSeconds(1))不要(Console.WriteLine).RetryWithBackoffStrategy()采取(1)。订阅(); –
@NiallConnaughton漂亮!之所以没有取消订阅源代码,是因为我最初是在我们拥有另一个内部运营商的基础上建立了这个方法的模型,这个运营商生产的是热门的可观测数据。这个操作员不应该那样做。我已经更改了代码来生成冷观察值,并添加了一个测试来验证它是否取消订阅。谢谢! –