Polly强制超时后任务仍运行及重试/超时机制相关问题咨询
我来帮你逐一分析和解决这些问题:
1) 代码是否大致正确?
整体思路是没问题的——你用Polly的TimeoutAsync(悲观超时)和RetryAsync组合,通过WrapAsync把重试策略放在外层、超时策略在内层,这个顺序是合理的(外层负责重试,每次重试都会触发内层的超时控制)。不过有几个小细节可以优化:
- 重试回调里的异常判断逻辑可以适当合并,让代码更简洁;
- 你现在每次在
ExecuteAsync里都创建RestService.For<ICTServerApi>实例,其实可以把这个客户端实例提到外面复用,避免重复创建的开销; - 你把
userCancellationToken传递给ExecuteAsync和API调用的做法是正确的,保证了用户可以主动取消整个流程。
2) 如何取消超时后仍在运行的任务?
出现这个问题的核心原因是:当Polly的超时触发TimeoutRejectedException后,Refit发起的HTTP请求并没有被真正取消——你虽然把userCancellationToken传给了GetUtentiAsync,但Polly的超时策略默认不会自动关联到请求的取消令牌。
解决方法很简单,只要让Polly的超时策略和请求的取消令牌绑定即可,推荐两种实现方式:
方式一:使用Polly执行时传入的ct
在ExecuteAsync的委托参数里,Polly会传入一个ct令牌,这个令牌会被Polly的超时策略自动取消。你只需要把这个令牌传给Refit的API调用,而不是直接用userCancellationToken:
await policyWrap.ExecuteAsync(async (ct) => { sw.Restart(); progress.Report("Execute"); var api = RestService.For<ICTServerApi>("http://localhost:3105/json/reply/ReqUtenti"); // 这里改用Polly传入的ct,超时后会自动取消请求 respUtenti = await api.GetUtentiAsync(reqUtenti, ct).ConfigureAwait(false); sw.Stop(); progress.Report($"Success after {sw.Elapsed}"); }, userCancellationToken).ConfigureAwait(false);
方式二:配置超时策略的令牌取消逻辑(适用于旧版Polly)
如果你的Polly版本较低,可以在超时策略里显式取消执行令牌:
var timeOutPolicy = Policy .TimeoutAsync(10, TimeoutStrategy.Pessimistic, (context, timespan, task) => { if (context.TryGetValue(CancellationTokenExtensions.CancellationTokenKey, out var tokenObj) && tokenObj is CancellationToken token) { ((CancellationTokenSource)token).Cancel(); } return Task.CompletedTask; });
这种方式同样能在超时终止后台的HTTP请求,避免任务继续运行。
3) 如何缩短SocketException的超时时间?
这个超时是底层TCP连接的超时,默认由系统和HttpClient的配置决定。要自定义这个时间,你需要给Refit配置一个自定义的HttpClient,通过SocketsHttpHandler设置连接超时:
具体实现代码
// 创建自定义的SocketsHttpHandler,设置连接超时 var socketHandler = new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromSeconds(2), // 把连接超时设为2秒,根据你的需求调整 PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5) }; // 创建HttpClient并设置基础地址和整体超时 var httpClient = new HttpClient(socketHandler) { BaseAddress = new Uri("http://localhost:3105/json/reply/ReqUtenti"), Timeout = TimeSpan.FromSeconds(10) // 可以和Polly的超时对齐,也可以单独设置 }; // 用自定义HttpClient创建Refit的API实例,复用这个实例即可 var api = RestService.For<ICTServerApi>(httpClient);
把这个api实例提到重试流程外面,每次重试都复用它。这样当服务器不可达时,会在你设置的ConnectTimeout时间后抛出SocketException,而不是等待系统默认的超时时间。
优化后的完整代码示例
// 提前创建复用的API实例 var socketHandler = new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromSeconds(2) }; var httpClient = new HttpClient(socketHandler) { BaseAddress = new Uri("http://localhost:3105/json/reply/ReqUtenti"), Timeout = TimeSpan.FromSeconds(10) }; var api = RestService.For<ICTServerApi>(httpClient); // 定义超时策略 var timeOutPolicy = Policy .TimeoutAsync(10, TimeoutStrategy.Pessimistic); // 定义重试策略 var retryPolicy = Policy .Handle<Exception>() .RetryAsync(2, (exception, attempt) => { sw.Stop(); progress.Report($"Error after {sw.Elapsed}"); progress.Report($"New attempt {attempt}"); // 简化异常判断逻辑 if (exception is Polly.Timeout.TimeoutRejectedException) { progress.Report("Catched: Polly Exception (timeout set by Polly)"); } else if (exception.InnerException is System.Net.Sockets.SocketException) { progress.Report("Catched: SocketException (No server running or not reachable)"); } else if (exception.InnerException is System.IO.IOException) { progress.Report("Catched: IOException (normal timeout)"); } else if (exception.InnerException is ApiException) { progress.Report("Catched: ApiException"); } }); // 组合策略:重试在外层,超时在内层 var policyWrap = Policy.WrapAsync(retryPolicy, timeOutPolicy); try { await policyWrap.ExecuteAsync(async (ct) => { sw.Restart(); progress.Report("Execute"); // 使用Polly传入的令牌,确保超时能取消请求 respUtenti = await api.GetUtentiAsync(reqUtenti, ct).ConfigureAwait(false); sw.Stop(); progress.Report($"Success after {sw.Elapsed}"); }, userCancellationToken).ConfigureAwait(false); } catch (Exception ex) { progress.Report($"最终执行失败:{ex.Message}"); }
内容的提问来源于stack exchange,提问作者Mattia Durli

