【问题标题】:Retrying C# HttpClient Unsuccessful Requests and Timeouts重试 C# HttpClient 不成功的请求和超时
【发布时间】:2014-09-21 22:33:55
【问题描述】:

我正在尝试在 HttpClient DelegatingHandler 中构建重试,以便将 503 Server Unavailable 和超时等响应视为暂时失败并自动重试。

我从http://blog.devscrum.net/2014/05/building-a-transient-retry-handler-for-the-net-httpclient/ 的代码开始,它适用于403 Server Unavailable 的情况,但不会将超时视为暂时性故障。尽管如此,我还是喜欢使用 Microsoft 瞬态故障处理块来处理重试逻辑的总体思路。

这是我当前的代码。它使用自定义的Exception 子类:

public class HttpRequestExceptionWithStatus : HttpRequestException {
    public HttpRequestExceptionWithStatus(string message) : base(message)
    {
    }
    public HttpRequestExceptionWithStatus(string message, Exception inner) : base(message, inner)
    {
    }
    public HttpStatusCode StatusCode { get; set; }
    public int CurrentRetryCount { get; set; }
}

这是瞬态故障检测器类:

public class HttpTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy {
    public bool IsTransient(Exception ex)
    {
        var cex = ex as HttpRequestExceptionWithStatus;
        var isTransient = cex != null && (cex.StatusCode == HttpStatusCode.ServiceUnavailable
                          || cex.StatusCode == HttpStatusCode.BadGateway
                          || cex.StatusCode == HttpStatusCode.GatewayTimeout);
        return isTransient;
    }
}

这个想法是超时应该变成ServiceUnavailable异常,就好像服务器已经返回了那个HTTP错误代码一样。这是DelegatingHandler 子类:

public class RetryDelegatingHandler : DelegatingHandler {
    public const int RetryCount = 3;

    public RetryPolicy RetryPolicy { get; set; }

    public RetryDelegatingHandler(HttpMessageHandler innerHandler) : base(innerHandler)
    {
        RetryPolicy = new RetryPolicy(new HttpTransientErrorDetectionStrategy(), new ExponentialBackoff(retryCount: RetryCount,
            minBackoff: TimeSpan.FromSeconds(1), maxBackoff: TimeSpan.FromSeconds(10), deltaBackoff: TimeSpan.FromSeconds(5)));
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var responseMessage = (HttpResponseMessage)null;
        var currentRetryCount = 0;

        EventHandler<RetryingEventArgs> handler = (sender, e) => currentRetryCount = e.CurrentRetryCount;
        RetryPolicy.Retrying += handler;

        try {
            await RetryPolicy.ExecuteAsync(async () => {
                try {
                    App.Log("Sending (" + currentRetryCount + ") " + request.RequestUri +
                        " content " + await request.Content.ReadAsStringAsync());
                    responseMessage = await base.SendAsync(request, cancellationToken);
                } catch (Exception ex) {
                    var wex = ex as WebException;
                    if (cancellationToken.IsCancellationRequested || (wex != null && wex.Status == WebExceptionStatus.UnknownError)) {
                        App.Log("Timed out waiting for " + request.RequestUri + ", throwing exception.");
                        throw new HttpRequestExceptionWithStatus("Timed out or disconnected", ex) {
                            StatusCode = HttpStatusCode.ServiceUnavailable,
                            CurrentRetryCount = currentRetryCount,
                        };
                    }

                    App.Log("ERROR awaiting send of " + request.RequestUri + "\n- " + ex.Message + ex.StackTrace);
                    throw;
                }
                if ((int)responseMessage.StatusCode >= 500) {
                    throw new HttpRequestExceptionWithStatus("Server error " + responseMessage.StatusCode) {
                        StatusCode = responseMessage.StatusCode,
                        CurrentRetryCount = currentRetryCount,
                    };
                }
                return responseMessage;
            }, cancellationToken);

            return responseMessage;
        } catch (HttpRequestExceptionWithStatus ex) {
            App.Log("Caught HREWS outside Retry section: " + ex.Message + ex.StackTrace);
            if (ex.CurrentRetryCount >= RetryCount) {
                App.Log(ex.Message);
            }
            if (responseMessage != null) return responseMessage;
            throw;
        } catch (Exception ex) {
            App.Log(ex.Message + ex.StackTrace);
            if (responseMessage != null) return responseMessage;
            throw;
        } finally {
            RetryPolicy.Retrying -= handler;
        }
    }
}

问题在于,一旦发生第一次超时,随后的重试会立即超时,因为所有内容都共享一个取消令牌。但是,如果我创建一个新的 CancellationTokenSource 并使用它的令牌,则不会发生超时,因为我无权访问原始 HttpClient 的取消令牌源。

我考虑过继承HttpClient 并覆盖SendAsync,但它的主要重载不是虚拟的。我可能只是创建一个不称为 SendAsync 的新函数,但它不是一个直接替换,我必须替换所有类似 GetAsync 的案例。

还有其他想法吗?

【问题讨论】:

标签: c# enterprise-library dotnet-httpclient


【解决方案1】:

您可能只想继承(或包装)HttpClient;在HttpClient 级别而不是在处理程序级别重试请求对我来说似乎更干净。如果这不合适,那么您需要拆分“超时”值。

由于您的处理程序实际上是在一个处理多个结果,HttpClient.Timeout 适用于整个过程,包括重试。您可以将另一个超时值添加到您的处理程序,这将是每个请求的超时,并将其与链接的取消令牌源一起使用:

public class RetryDelegatingHandler : DelegatingHandler {
  public TimmeSpan PerRequestTimeout { get; set; }
  ...
  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    cts.CancelAfter(PerRequestTimeout);
    var token = cts.Token;
    ...
        responseMessage = await base.SendAsync(request, token);
    ...
  }
}

【讨论】:

  • 我最终对您的链接取消令牌建议做了一个变体。它工作得很好。谢谢!
  • 但是 DelegatingHandler 是重试的正确位置吗?有一个类似问题的答案指出,HttpRequestMessage 不能被重用:stackoverflow.com/a/26278462/567000.
  • 此外,似乎无法区分取消和超时,因为 HttpClient 显然会取消传递给 SendAsync 的令牌。
  • @SørenBoisen:我不使用DelegatingHandler 进行重试。引用我的回答,“对我来说,在HttpClient 级别而不是在处理程序级别重试请求似乎更干净。”
  • @AnthonyMills 我已经尝试过这个答案,超时后,所有后续请求仍然不想被处理。我试图将此 PerRequestTimeout 设置为小于 HttpClient.Timeout 并且它不起作用。你想出解决方案了吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-21
  • 1970-01-01
  • 1970-01-01
  • 2015-10-11
  • 2022-01-11
相关资源
最近更新 更多