You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Polly强制超时后任务仍运行及重试/超时机制相关问题咨询

使用Polly+Refit实现重试/超时流程的问题解答

我来帮你逐一分析和解决这些问题:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:57:31