当前位置: 首页 > 工具软件 > Polly > 使用案例 >

Polly-超时

夏侯腾
2023-12-01

超时策略 (v5.0 起可用)

翻译自:Timeout · App-vNext/Polly Wiki · GitHub

目的

确保调用方不会一直等待直到超过配置的超时时间。

对没有内置超时的操作强制超时。

前提:“不要永远等待”

永远等待(不设超时时间)是一种糟糕的设计策略:特别是在出现故障的场景中,它会导致线程或连接阻塞(这通常还会导致进一步的系统错误)。

等待了一定时间过后,迎来的往往会是失败,这时需要设置超时时间。

语法

TimeoutPolicy timeoutPolicy = Policy
  .Timeout([int|TimeSpan|Func<TimeSpan> timeout]
           [, TimeoutStrategy.Optimistic|Pessimistic]
           [, Action<Context, TimeSpan, Task> onTimeout])

TimeoutPolicy timeoutPolicy = Policy
  .TimeoutAsync([int|TimeSpan|Func<TimeSpan> timeout]
                [, TimeoutStrategy.Optimistic|Pessimistic]
                [, Func<Context, TimeSpan, Task, Task> onTimeoutAsync])

参数:

  • timeout: 委托执行timeout时长后就会被放弃. 可以是 int(单位秒)、TimeSpan、或者返回TimeSpan的委托
  • timeoutStrategy (可选参数): 悲观或乐观策略,详情见下
  • onTimeout/Async (可选参数): 触发超时后要执行的委托. 该委托在异常TimeoutRejectedException 抛出之前执行.

抛出异常:

  • TimeoutRejectedException,执行时间达到timeout后抛出.

使用

TimeoutPolicy 支持 乐观 和 悲观 超时.

乐观超时

TimeoutStrategy.Optimistic 假设你所执行的委托支持 co-operative cancellation (ie honor CancellationTokens),并且委托通过抛出OperationCanceledException来表示超时(这是大多数.net库代码的标准)。

如果通过策略执行的代码是您自己的代码, 推荐写法 是在可取消的任务中的合适的位置调用cancellationToken.ThrowIfCancellationRequested() .

该策略将超时的“CancellationToken”组合到任何传入的“CancellationToken”中,通过取消已执行的的委托来实现超时。你必须使用Execute/Async(...)或类似的重载来传递 CancellationToken,并且执行的的委托必须要有CancellationToken参数传入lambda 表达式:

Policy timeoutPolicy = Policy.TimeoutAsync(30, TimeoutStrategy.Optimistic);
HttpResponseMessage httpResponse = await timeoutPolicy
    .ExecuteAsync(
        async ct => await httpClient.GetAsync(requestEndpoint, ct), //执行的委托要对 CancellationToken 有响应
        CancellationToken.None // CancellationToken.None 表示你没有独立的独立的线程取消,并且愿意使用超时策略提供的线程取消
    );

您还可以结合自己的' CancellationToken '(可能携带由用户发出的独立取消信号)。例如:

CancellationTokenSource userCancellationSource = new CancellationTokenSource();
// userCancellationSource perhaps hooked up to the user clicking a 'cancel' button, or other independent cancellation

Policy timeoutPolicy = Policy.TimeoutAsync(30, TimeoutStrategy.Optimistic);

HttpResponseMessage httpResponse = await timeoutPolicy
    .ExecuteAsync(
        async ct => await httpClient.GetAsync(requestEndpoint, ct), 
        userCancellationSource.Token
        );
    // GetAsync(...) 会在用户发出取消指令或超时时取消.

我们建议尽可能使用乐观超时,因为它消耗的资源更少。默认是乐观超时。

悲观超时

TimeoutStrategy.Pessimistic 表示到,在某些情况下,您可能需要执行没有内置超时且不允许取消的委托。

TimeoutStrategy.Pessimistic 目的是在这些情况下仍然允许您执行超时,保证在超时的时候仍能返回到调用者线程上。

在这种情况下,超时的意思是调用者“走开”:停止等待底层委托完成。不允许取消的底层委托自然不会被神奇的取消-参见下面内容超时的委托会发生什么?

附加说明:异步执行的悲观超时

对于异步执行,额外的资源成本是很小的:不涉及额外的线程或执行中的Task

请注意,异步执行的悲观超时策略并不完全是在超时的时候将异步委托标记为同步。(PS:没看懂,请看原文:Note that pessimistic timeout for async executions will not timeout purely synchronous delegates which happen to be labelled async. )它期望执行的async 代码符合标准的async 模式,返回一个Task表示该async工作的继续执行(例如,当被执行的委托遇到第一个内部await语句时)。

在单元测试中使用Thread.Sleep(...)无法达到触发超时策略的效果,这是因为设计如此,即异步超时策略是针对大多数行为良好的异步情况优化的,而不是针对实际的同步边缘情况。要编写针对异步超时策略的单元测试,使用_ await Task.Delay(..., cancellationToken), 不要用Thread.Sleep(…)详细讨论和示例: #318;(# 340) (https://github.com/App-vNext/Polly/issues/340);(# 623) (https://github.com/App-vNext/Polly/issues/623)

附加说明:同步执行的悲观超时

对于同步执行,调用线程“离开”不可超时操作是有代价的:为了允许当前线程离开,策略会将用户的委托作为一个线程池的一个 Task线程

因此我们不建议在并发请求处理数量可能很高或无限大的情况下使用悲观同步TimeoutPolicy。在这种高/无限制并发的场景中,代价可能非常昂贵(实际使用的线程数翻倍)。

我们建议将悲观同步超时策略与显式限制代码路径上调用的并行性结合起来。控制并行性的选项包括:

  • 在超时策略上游使用Polly的BulkheadPolicy(这是一个平行节流)
  • 在超时策略上游使用限制并发量的TaskScheduler
  • 在超时策略上游使用断路器策略,因为断路器可以在太多下游调用超时的时候断路,它可以防止在超时时将过多的调用传递到下游系统(以及阻塞线程)
  • 调用环境中其他的的内置并行性控制策略。

超时委托会发生什么?

任何超时策略的一个关键问题是如何处理被放弃的(超时)任务。

悲观超时

Polly不会因为单方面终止线程而危及应用程序的状态。相反,对于悲观执行,TimeoutPolicy _捕获并将被放弃的执行作为onTimeout/onTimeoutAsync委托的' Task '参数传递给你。

这可以防止这些任务凭空消失(对于_悲观_执行,根据定义,我们是在谈论我们认为无法通过取消令牌控制的委托:它们将继续执行,直到延迟完成或故障)。

onTimeout/Asynctask属性允许你在这些无法控制的调用之后优雅地进行清理。当它们最终终止时,你可以释放资源,执行其他清理,并捕获超时任务最终可能引发的任何异常(重要的是,要防止这些异常显示为UnobservedTaskExceptions):

Policy.Timeout(30, TimeoutStrategy.Pessimistic, (context, timespan, task) => 
    {
        task.ContinueWith(t => { // ContinueWith important!: the abandoned task may very well still be executing, when the caller times out on waiting for it! 

            if (t.IsFaulted) 
            {
                logger.Error($"{context.PolicyKey} at {context.OperationKey}: execution timed out after {timespan.TotalSeconds} seconds, eventually terminated with: {t.Exception}.");
            }
            else if (t.IsCanceled)
            {
               // (If the executed delegates do not honour cancellation, this IsCanceled branch may never be hit.  It can be good practice however to include, in case a Policy configured with TimeoutStrategy.Pessimistic is used to execute a delegate honouring cancellation.)  
               logger.Error($"{context.PolicyKey} at {context.OperationKey}: execution timed out after {timespan.TotalSeconds} seconds, task cancelled.");
            }
            else
            {
               // extra logic (if desired) for tasks which complete, despite the caller having 'walked away' earlier due to timeout.
            }

            // Additionally, clean up any resources ...

        });
    });

Note: 在上述代码的async情况下,不要使用 await task.ContinueWith(...),因为这会使onTimeoutAsync:委托等待那个被策略抛弃的任务执行完成。导致策略一直在 task执行完成,并超过超时。TPL-style:异步情况必须附加continuation;而不是await“等待”。

乐观超时策略

对于乐观执行,假设CancellationToken将导致超时执行清理(如果需要)工作,然后通过抛出任务取消异常来终止任务,标准示例

这将终止超时的任务,并将该终止信号返回给调用者。这里不会像悲观案例中那样留下独立的、持续的、一走了之的任务。因此,传递给onTimeout/onTimeoutAsyncTask参数在乐观超时时故意总是null

也可以这么理解,在乐观超时中,受时间控制的工作在调用方的代码路径上执行,从而直接将其终止的全部细节表示回调用方。如果超时工作的取消被额外地传递给onTimeout/onTimeoutAsync委托,这将导致它在两个地方能被处理,导致无法清楚的知道要在哪里对他进行处理或终止。)

延伸阅读

关于如何退出无法取消的执行(悲观超时)的详细讨论,请参阅Stephen Toub的如何取消不可取消的异步操作?

推荐配置

每个可能阻塞线程或阻塞等待资源或响应的动作都应该有一个超时。[Michael Nygard:Release It!]。

HttpClient 的调用使用乐观超时

不要对HttpClient 的调用使用悲观策略!因此所有版本的HttpClient都带有CancellationToken参数,因此它应该与乐观超时策略一起使用。

重试结合超时

更多关于使用PolicyWrap排序策略的信息可以在这里找到(https://github.com/App-vNext/Polly/wiki/PolicyWrap#ordering-the-available-policy-types-in-a-wrap)。

整体重试超时的示例可以在这里找到,他其实是把重试策略作为HttpClient的一个委托处处理(可能通过HttpClientFactory配置),请注意HttpClient.Timeout属性也能提供这个整体超时:请参阅我们的HttpClientFactory文档了解更多细节。

线程安全和策略复用

线程安全

TimeoutPolicy的操作是线程安全的:可以通过一个策略实例安全地并发地放置多个调用。

策略复用

TimeoutPolicy实例可以跨多个调用点重用。

在重用策略时,使用“OperationKey”来区分日志和度量中的不同调用点使用情况。

 类似资料: