Ошибка получения сообщения "Запрос уже отправлен" при использовании Polly
В настоящее время я использую Polly для ограничения количества отправляемых запросов. Это политика, которую я в настоящее время имею:
private AsyncPolicyWrap<HttpResponseMessage> DefineAndRetrieveResiliencyStrategy()
{
HttpStatusCode[] retryCodes = {
HttpStatusCode.InternalServerError,
HttpStatusCode.BadGateway,
HttpStatusCode.GatewayTimeout
};
var waitAndRetryPolicy = Policy
.HandleResult<HttpResponseMessage>(e => e.StatusCode == HttpStatusCode.ServiceUnavailable || e.StatusCode == (HttpStatusCode)429)
.WaitAndRetryAsync(10,
attempt => TimeSpan.FromSeconds(5), (exception, calculatedWaitDuration) =>
{
_log.Info($"Bitfinex API server is throttling our requests. Automatically delaying for {calculatedWaitDuration.TotalMilliseconds}ms");
}
);
var circuitBreakerPolicyForRecoverable = Policy
.Handle<HttpResponseException>()
.OrResult<HttpResponseMessage>(r => retryCodes.Contains(r.StatusCode))
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(3),
onBreak: (outcome, breakDelay) =>
{
_log.Info($"Polly Circuit Breaker logging: Breaking the circuit for {breakDelay.TotalMilliseconds}ms due to: {outcome.Exception?.Message ?? outcome.Result.StatusCode.ToString()}");
},
onReset: () => _log.Info("Polly Circuit Breaker logging: Call ok... closed the circuit again"),
onHalfOpen: () => _log.Info("Polly Circuit Breaker logging: Half-open: Next call is a trial")
);
return Policy.WrapAsync(waitAndRetryPolicy, circuitBreakerPolicyForRecoverable);
}
У меня есть следующий отправитель запроса:
private async Task<string> SendRequest(GenericRequest request, string httpMethod, string publicKey, string privateKey)
{
var resiliencyStrategy = DefineAndRetrieveResiliencyStrategy();
using (var client = new HttpClient())
using (var httpRequest = new HttpRequestMessage(new HttpMethod(httpMethod), request.request))
{
string json = JsonConvert.SerializeObject(request);
string json64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
byte[] data = Encoding.UTF8.GetBytes(json64);
client.BaseAddress = new Uri(Properties.Settings.Default.BitfinexUri);
var hashMaker = new HMACSHA384(Encoding.UTF8.GetBytes(privateKey));
byte[] hash = hashMaker.ComputeHash(data);
string signature = GetHexString(hash);
httpRequest.Headers.Add("X-BFX-APIKEY", publicKey);
httpRequest.Headers.Add("X-BFX-PAYLOAD", json64);
httpRequest.Headers.Add("X-BFX-SIGNATURE", signature);
var message = await resiliencyStrategy.ExecuteAsync(() => client.SendAsync(httpRequest));
var response = message.Content.ReadAsStringAsync().Result;
return response;
}
}
Как только код попадает в waitAndRetryPolicy и ожидает необходимое количество времени, я получаю следующую ошибку:
System.InvalidOperationException: 'Сообщение с запросом уже отправлено. Невозможно отправить одно и то же сообщение с запросом несколько раз.'
Я понимаю, что это происходит, потому что я снова отправляю тот же запрос HttpRequest, но разве библиотека Polly не должна решать такую проблему?
3 ответа
Это исключение:
System.InvalidOperationException: 'Сообщение с запросом уже отправлено. Невозможно отправить одно и то же сообщение с запросом несколько раз.'
брошен внутренними HttpClient
если вы вызываете напрямую в любую перегрузку.SendAsync(...) с HttpRequestMessage, которая уже была отправлена.
Если вы используете.NET Core, рекомендуется использовать Polly с HttpClientFactory: это устраняет указанное выше исключение, выполняя политику (например, повтор) через DelegatingHandler
в HttpClient
, Это также решает проблему истощения сокетов, которая может быть вызвана частым созданием / удалением HttpClient
, к которому может быть уязвим код, размещенный в вопросе.
Если вы используете.NET Framework, рекомендуемые решения:
- повторить способ, которым HttpClientFactory размещает политику в DelegatingHandler; или же
- рефакторинг вашего кода для производства нового экземпляра
HttpRequestMessage
(или клонировать существующий экземпляр) в коде, выполняемом с помощью политики.
В этом вопросе о стекопереработке подробно обсуждается проблема и множество вариантов описанных выше решений.
Если бы вы разделили свой код на следующие две функции:
private HttpRequestMessage CreateRequest(GenericRequest request, string httpMethod, string publicKey, string privateKey)
{
var httpRequest = new HttpRequestMessage(new HttpMethod(httpMethod), request.request);
httpRequest.Headers.Add("X-BFX-APIKEY", publicKey);
string json = JsonConvert.SerializeObject(request);
string json64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
httpRequest.Headers.Add("X-BFX-PAYLOAD", json64);
byte[] data = Encoding.UTF8.GetBytes(json64);
var hashMaker = new HMACSHA384(Encoding.UTF8.GetBytes(privateKey));
byte[] hash = hashMaker.ComputeHash(data);
httpRequest.Headers.Add("X-BFX-SIGNATURE", GetHexString(hash));
return httpRequest;
}
private async Task<string> SendRequest(GenericRequest request, string httpMethod, string publicKey, string privateKey)
{
var message = await DefineAndRetrieveResiliencyStrategy().ExecuteAsync(async () =>
{
var httpRequest = CreateRequest(request, httpMethod, publicKey, privateKey);
await client.SendAsync(httpRequest);
});
return await message.Content.ReadAsStringAsync();
}
тогда ваша проблема исчезнет.
- The
CreateRequest
несет ответственность за создание новогоHttpRequestMessage
всякий раз, когда он вызывается (либо при первоначальном запросе, либо при любой последующей попытке повтора) - The
SendRequest
отвечает за украшение нисходящей связи предопределенной стратегией и анализ результата любого - The
HttpClient
настройка должна быть выполнена только один раз и повторно использоваться много раз
с .NET Framework в качестве альтернативы, чтобы сделать его универсальным, вы можете клонировать запрос и сохранять его в contextData, используяIAsyncPolicy.ExecuteAsync
второй параметр функцииIDictionary<string, object> contextData