使用 Polly 重试 HttpClient 请求的正确方法
Correct way to retry HttpClient requests with Polly
我有一个 Azure 函数可以对 webapi 端点进行 http 调用。我正在关注此示例 GitHub Polly RetryPolicy,因此我的代码具有类似的结构。所以在 Startup.cs 我有:
builder.Services.AddPollyPolicies(config); // extension methods setting up Polly retry policies
builder.Services.AddHttpClient("MySender", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});
我的重试政策是这样的:
public static class PollyRegistryExtensions
{
public static IPolicyRegistry<string> AddBasicRetryPolicy(this IPolicyRegistry<string> policyRegistry, IMyConfig config)
{
var retryPolicy = Policy
.Handle<Exception>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(config.ServiceRetryAttempts, retryCount => TimeSpan.FromMilliseconds(config.ServiceRetryBackOffMilliSeconds), (result, timeSpan, retryCount, context) =>
{
if (!context.TryGetLogger(out var logger)) return;
logger.LogWarning(
$"Service delivery attempt {retryCount} failed, next attempt in {timeSpan.TotalMilliseconds} ms.");
})
.WithPolicyKey(PolicyNames.BasicRetry);
policyRegistry.Add(PolicyNames.BasicRetry, retryPolicy);
return policyRegistry;
}
}
我的客户端发件人服务在其构造函数中接收 IReadOnlyPolicyRegistry<string> policyRegistry
和 IHttpClientFactory clientFactory
。我调用客户端的代码如下:
var jsonContent = new StringContent(JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>(PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context($"GetSomeData-{Guid.NewGuid()}", new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(ctx =>
httpClient.SendAsync(requestMessage), context);
当我尝试在没有端点服务的情况下进行测试时 运行 然后对于第一次重试尝试,重试处理程序被触发并且我的记录器记录了第一次尝试。但是,在第二次重试时,我收到一条错误消息:
The request message was already sent. Cannot send the same request
message multiple times
我知道其他人也遇到过类似的问题(参见 并且解决方案似乎是做我正在做的事情(即使用 HttpClientFactory
)。但是,我不会如果我将重试策略定义为启动配置的一部分,则会出现此问题:
builder.Services.AddHttpClient("MyService", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromMilliseconds(1000));
}
然后简单地调用我的服务:
var response = await httpClient.SendAsync(requestMessage);
但是这样做我失去了在重试策略上下文中传递我的记录器的能力(这是我在 IReadOnlyPolicyRegistry<string> policyRegistry
中注入的全部原因 - 我不能在启动时这样做)。另一个好处是用于单元测试——我可以简单地使用相同的策略注入相同的集合,而无需复制和粘贴一大堆代码并使单元测试变得多余,因为我不再测试我的服务。在启动时定义策略使这成为不可能。所以我的问题是,有没有一种方法可以避免使用这种方法出现重复请求错误?
您对 Polly 以及如何配置它有点太着迷了,而忘记了一些基本方面。别担心,这太容易了!
首先,您不能多次发送相同的 HttpRequestMessage
。看到这个广泛的Q&A on the subject. It's also documented officially,尽管文档对原因有点不透明。
其次,正如您编写的代码,您创建的请求被 lambda 捕获一次,然后一遍又一遍地重复使用。
对于您的特殊情况,我会在您传递给 ExecuteAsync
的 lambda 中创建请求。这每次都会给你一个新的请求。
正在修改您的代码,
var jsonContent = new StringContent(
JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context(
$"GetSomeData-{Guid.NewGuid()}",
new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(ctx =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.SendAsync(requestMessage), context);
}
其他捕获:logger、authToken,如果它们不逐个更改请求可能没问题,但您可能还需要将其他变量移入 lambda。
根本不使用 Polly 就不需要大部分的思考过程,但是使用 Polly 时,您必须记住重试和策略是跨时间和上下文发生的。
这是一个替代解决方案(我更喜欢)。
AddPolicyHandler
添加的 PolicyHttpMessageHandler
将创建一个 Polly Context
如果 一个尚未附加。所以你可以添加一个 MessageHandler
来创建一个 Context
并附加记录器:
public sealed class LoggerProviderMessageHandler<T> : DelegatingHandler
{
private readonly ILogger _logger;
public LoggerProviderMessageHandler(ILogger<T> logger) => _logger = logger;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpClientRequestId = $"GetSomeData-{Guid.NewGuid()}";
var context = new Context(httpClientRequestId);
context[PolicyContextItems.Logger] = _logger;
request.SetPolicyExecutionContext(context);
return await base.SendAsync(request, cancellationToken);
}
}
注册的一点扩展方法让它变得很好:
public static IHttpClientBuilder AddLoggerProvider<T>(this IHttpClientBuilder builder)
{
if (!services.Any(x => x.ServiceType == typeof(LoggerProviderMessageHandler<T>)))
services.AddTransient<LoggerProviderMessageHandler<T>>();
return builder.AddHttpMessageHandler<LoggerProviderMessageHandler<T>>();
}
然后你可以这样使用它(注意它必须在 之前 AddPolicyHandler
以便它首先创建 Context
):
builder.Services.AddHttpClient("MyService", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddLoggerProvider<MyService>()
.AddPolicyHandler(GetRetryPolicy());
在运行时,LoggerProviderMessageHandler<MyService>
获取一个 ILogger<MyService>
,创建一个包含该记录器的 Polly Context
,然后调用 PolicyHttpMessageHandler
,它使用现有的 Polly [=15] =], 所以你的重试策略可以成功使用 context.TryGetLogger
.
var jsonContent = new StringContent(
JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context(
$"GetSomeData-{Guid.NewGuid()}",
new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(() =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return httpClient.SendAsync(requestMessage), context);
}
我有一个 Azure 函数可以对 webapi 端点进行 http 调用。我正在关注此示例 GitHub Polly RetryPolicy,因此我的代码具有类似的结构。所以在 Startup.cs 我有:
builder.Services.AddPollyPolicies(config); // extension methods setting up Polly retry policies
builder.Services.AddHttpClient("MySender", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});
我的重试政策是这样的:
public static class PollyRegistryExtensions
{
public static IPolicyRegistry<string> AddBasicRetryPolicy(this IPolicyRegistry<string> policyRegistry, IMyConfig config)
{
var retryPolicy = Policy
.Handle<Exception>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(config.ServiceRetryAttempts, retryCount => TimeSpan.FromMilliseconds(config.ServiceRetryBackOffMilliSeconds), (result, timeSpan, retryCount, context) =>
{
if (!context.TryGetLogger(out var logger)) return;
logger.LogWarning(
$"Service delivery attempt {retryCount} failed, next attempt in {timeSpan.TotalMilliseconds} ms.");
})
.WithPolicyKey(PolicyNames.BasicRetry);
policyRegistry.Add(PolicyNames.BasicRetry, retryPolicy);
return policyRegistry;
}
}
我的客户端发件人服务在其构造函数中接收 IReadOnlyPolicyRegistry<string> policyRegistry
和 IHttpClientFactory clientFactory
。我调用客户端的代码如下:
var jsonContent = new StringContent(JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>(PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context($"GetSomeData-{Guid.NewGuid()}", new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(ctx =>
httpClient.SendAsync(requestMessage), context);
当我尝试在没有端点服务的情况下进行测试时 运行 然后对于第一次重试尝试,重试处理程序被触发并且我的记录器记录了第一次尝试。但是,在第二次重试时,我收到一条错误消息:
The request message was already sent. Cannot send the same request message multiple times
我知道其他人也遇到过类似的问题(参见 并且解决方案似乎是做我正在做的事情(即使用 HttpClientFactory
)。但是,我不会如果我将重试策略定义为启动配置的一部分,则会出现此问题:
builder.Services.AddHttpClient("MyService", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromMilliseconds(1000));
}
然后简单地调用我的服务:
var response = await httpClient.SendAsync(requestMessage);
但是这样做我失去了在重试策略上下文中传递我的记录器的能力(这是我在 IReadOnlyPolicyRegistry<string> policyRegistry
中注入的全部原因 - 我不能在启动时这样做)。另一个好处是用于单元测试——我可以简单地使用相同的策略注入相同的集合,而无需复制和粘贴一大堆代码并使单元测试变得多余,因为我不再测试我的服务。在启动时定义策略使这成为不可能。所以我的问题是,有没有一种方法可以避免使用这种方法出现重复请求错误?
您对 Polly 以及如何配置它有点太着迷了,而忘记了一些基本方面。别担心,这太容易了!
首先,您不能多次发送相同的 HttpRequestMessage
。看到这个广泛的Q&A on the subject. It's also documented officially,尽管文档对原因有点不透明。
其次,正如您编写的代码,您创建的请求被 lambda 捕获一次,然后一遍又一遍地重复使用。
对于您的特殊情况,我会在您传递给 ExecuteAsync
的 lambda 中创建请求。这每次都会给你一个新的请求。
正在修改您的代码,
var jsonContent = new StringContent(
JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context(
$"GetSomeData-{Guid.NewGuid()}",
new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(ctx =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.SendAsync(requestMessage), context);
}
其他捕获:logger、authToken,如果它们不逐个更改请求可能没问题,但您可能还需要将其他变量移入 lambda。
根本不使用 Polly 就不需要大部分的思考过程,但是使用 Polly 时,您必须记住重试和策略是跨时间和上下文发生的。
这是一个替代解决方案(我更喜欢)。
AddPolicyHandler
添加的 PolicyHttpMessageHandler
将创建一个 Polly Context
如果 一个尚未附加。所以你可以添加一个 MessageHandler
来创建一个 Context
并附加记录器:
public sealed class LoggerProviderMessageHandler<T> : DelegatingHandler
{
private readonly ILogger _logger;
public LoggerProviderMessageHandler(ILogger<T> logger) => _logger = logger;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpClientRequestId = $"GetSomeData-{Guid.NewGuid()}";
var context = new Context(httpClientRequestId);
context[PolicyContextItems.Logger] = _logger;
request.SetPolicyExecutionContext(context);
return await base.SendAsync(request, cancellationToken);
}
}
注册的一点扩展方法让它变得很好:
public static IHttpClientBuilder AddLoggerProvider<T>(this IHttpClientBuilder builder)
{
if (!services.Any(x => x.ServiceType == typeof(LoggerProviderMessageHandler<T>)))
services.AddTransient<LoggerProviderMessageHandler<T>>();
return builder.AddHttpMessageHandler<LoggerProviderMessageHandler<T>>();
}
然后你可以这样使用它(注意它必须在 之前 AddPolicyHandler
以便它首先创建 Context
):
builder.Services.AddHttpClient("MyService", client =>
{
client.BaseAddress = config.SenderUrl;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddLoggerProvider<MyService>()
.AddPolicyHandler(GetRetryPolicy());
在运行时,LoggerProviderMessageHandler<MyService>
获取一个 ILogger<MyService>
,创建一个包含该记录器的 Polly Context
,然后调用 PolicyHttpMessageHandler
,它使用现有的 Polly [=15] =], 所以你的重试策略可以成功使用 context.TryGetLogger
.
var jsonContent = new StringContent(
JsonSerializer.Serialize(contentObj),
Encoding.UTF8,
"application/json");
var retryPolicy = _policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>PolicyNames.BasicRetry)
?? Policy.NoOpAsync<HttpResponseMessage>();
var context = new Context(
$"GetSomeData-{Guid.NewGuid()}",
new Dictionary<string, object>
{
{ PolicyContextItems.Logger, _logger }
});
var httpClient = _clientFactory.CreateClient("MySender");
var response = await retryPolicy.ExecuteAsync(() =>
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "SendEndpoint")
{
Content = jsonContent
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return httpClient.SendAsync(requestMessage), context);
}