我正在尝试在 HttpClient
DelegatingHandler
中构建重试,以便将诸如 503 Server Unavailable
和超时之类的响应视为 transient 失败并自动重试。
我从 http://blog.devscrum.net/2014/05/building-a-transient-retry-handler-for-the-net-httpclient/ 处的代码开始它适用于 403 Server Unavailable
情况,但不会将超时视为暂时性故障。尽管如此,我还是喜欢使用 Microsoft transient 故障处理 block 来处理重试逻辑的总体思路。
这是我当前的代码。它使用自定义的 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; }
}
这里是 transient 故障检测器类:
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;
}
}
}
问题是,一旦第一次超时发生,随后的重试立即超时,因为一切都共享一个取消 token 。但是,如果我创建一个新的 CancellationTokenSource
并使用其 token ,则不会发生超时,因为我无权访问原始 HttpClient
的取消 token 源。
我考虑过子类化 HttpClient
并覆盖 SendAsync
但它的主要重载不是虚拟的。我可能只创建一个不称为 SendAsync
的新函数,但它不是一个直接替换,我必须替换所有类似 GetAsync
的情况。
还有其他想法吗?
最佳答案
您可能只想子类化(或包装)HttpClient
;在我看来,在 HttpClient
级别而不是在处理程序级别重试请求似乎更清晰。如果这不合胃口,那么您将需要拆分“超时”值。
由于您的处理程序实际上是将多个结果合二为一,HttpClient.Timeout
适用于整个过程,包括 重试。您可以向您的处理程序添加另一个超时值,这将是每个请求的超时值,并将其与链接的取消 token 源一起使用:
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);
...
}
}
关于c# - 重试 C# HttpClient 不成功的请求和超时,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25964569/