【问题标题】:Ensure Polly Policy Runs at Least Once确保 Polly 策略至少运行一次
【发布时间】:2020-06-19 15:36:36
【问题描述】:

所以,我正在编写一些使用 Polly 获取锁的重试逻辑。整体超时值将由 API 调用者提供。我知道我可以在整体超时中包装策略。但是,如果提供的超时值太低,是否有办法确保策略至少执行一次?

显然我可以在执行策略之前单独调用委托,但我只是想知道是否有办法在 Polly 中表达此要求。

var result = Policy.Timeout(timeoutFromApiCaller)
                    .Wrap(Policy.HandleResult(false)
                                .WaitAndRetryForever(_ => TimeSpan.FromMilliseconds(500))
                    .Execute(() => this.TryEnterLock());

如果 timeoutFromApiCaller1 tick 并且很有可能需要更长的时间才能达到超时策略,则不会调用委托(该策略将超时并抛出 TimeoutRejectedException)。

我希望发生的事情可以表达为:

var result = this.TryEnterLock();

if (!result)
{
    result = Policy.Timeout(timeoutFromApiCaller)
                   .Wrap(Policy.HandleResult(false)
                               .WaitAndRetryForever(_ => TimeSpan.FromMilliseconds(500))
                   .Execute(() => this.TryEnterLock());
}

不过要是能用纯波利来表达就好了……

【问题讨论】:

  • 请分享你的代码
  • 当前您的实现没有全局(整体)超时。它作为“本地”运行。假设超时为 2 秒,重试次数为 3,重试延迟为 4 秒。它等待 2 秒,然后在下一次重试之前暂停 4 秒……重要的是您包装策略的顺序:如果内部失败,则升级到外部。本地超时:重试 > 超时,全局超时:超时 > 重试,本地+整体:超时 > 重试 > 超时。
  • 谢谢@PeterCsala,你知道我想要实现的目标是否可行吗?即总是尝试一次然后遵守任何 global 超时?
  • @Nick 事实上,开箱即用,您至少获得了一次通话保证。因为重试计数 3 意味着 4 次尝试。初始调用是第一次尝试。如果失败,则将触发重试。因此,您的第一次重试将是您的第二次尝试。
  • @PeterCsala,我稍微修改了代码,因此它实际上代表了我想要实现的目标。 IE。我在外部范围内超时并在内部范围内重试。如果在内部有机会操作之前外部超时,这将抛出TimeoutRejectedException。我还指出了我希望它如何运作。

标签: c# .net polly


【解决方案1】:

说实话,我不明白 1 勾 是什么意思,在你的情况下?它是一纳秒还是更大?您的全局超时应大于本地超时。

但正如我所见,您没有指定本地人TryEnterLock 应该收到一个 TimeSpan 以便不会无限期地阻塞调用者。如果您查看内置的同步原语,其中大多数都提供了这样的功能:Monitor.TryEnterSpinLock.TryEnterWaitHandle.WaitOne 等。

所以,总结一下:

var timeoutPolicy = Policy.Timeout(TimeSpan.FromMilliseconds(1000));
var retryPolicy = Policy.HandleResult(false)
       .WaitAndRetryForever(_ => TimeSpan.FromMilliseconds(500));
var resilientStrategy = Policy.Wrap(timeoutPolicy, retryPolicy);    

var result = resilientStrategy.Execute(() => this.TryEnterLock(TimeSpan.FromMilliseconds(100))); 

超时和延迟值应根据您的业务需求进行调整。我强烈建议您在全局超时 (onTimeout / onTimeoutAsync) 触发和重试时 (onRetry / onRetryAsync) 进行记录,以便能够微调/校准这些值。


编辑:基于这篇文章的 cmets

事实证明,timeoutFromApiCaller 无法控制,因此它可以任意小。 (在给定的示例中,它只有几纳秒,目的是强调问题。)因此,为了获得至少一个呼叫保证,我们必须使用Fallback policy

我们应该将其称为最后一个操作来满足要求,而不是在策略之外预先手动调用TryEnterLock。因为策略使用升级,所以每当 inner 失败时,它会将问题委托给下一个 outer 策略。

因此,如果提供的超时时间太短以至于操作在该时间段之前无法完成,那么它将抛出TimeoutRejectedException。使用 Fallback,我们可以处理这个问题,并且可以再次执行该操作,但现在没有任何超时限制。这将为我们提供所需的至少一项保证。

var atLeastOnce = Policy.Handle<TimeoutRejectedException>
    .Fallback((ct) => this.TryEnterLock());
var globalTimeout = Policy.Timeout(TimeSpan.FromMilliseconds(1000));
var foreverRetry = Policy.HandleResult(false)
       .WaitAndRetryForever(_ => TimeSpan.FromMilliseconds(500));

var resilientStrategy = Policy.Wrap(atLeastOnce, globalTimeout, foreverRetry);    
var result = resilientStrategy.Execute(() => this.TryEnterLock()); 

【讨论】:

  • 谢谢彼得。 TryEnterLock 是一种现有方法,它调用 SPROC 将值写入 db 表 - 基本上是在不使用行级锁定的情况下进行行级锁定! (不要问我!)。我正在尝试像当前一样实现一个简单的等待/重试/超时,如果我们在第一次尝试失败时没有获得锁定。
  • @Nick 1 在 100 纳秒左右打勾。您确定在您想要进行数据库调用的用例中这是一个有效的超时吗?我会考虑对timeoutFromApiCaller 执行一些验证逻辑。如果您使用截止日期/分布式超时模式,那么如果剩余超时低于可靠阈值,您可能会快速失败。
  • @Nick 您可以使用后备策略来做到这一点。与其一开始就直接调用它(在策略之外),不如把它作为最后的机会。如果封装在策略中的调用抛出了TimeoutRejectedException,那么您可以调用该方法本身。因此,您的弹性策略将如下所示:后备 >> 重试 >> 超时 >> 操作。你觉得这有意义吗?
  • 啊,是的,听起来像。如果你用这个更新你的答案,我会接受它。
  • 谢谢你给了彼得很大的帮助。
猜你喜欢
  • 2021-09-11
  • 1970-01-01
  • 2020-10-29
  • 1970-01-01
  • 1970-01-01
  • 2017-10-27
  • 2022-10-02
  • 2022-10-02
  • 1970-01-01
相关资源
最近更新 更多