Проверьте содержимое строки ответа перед повторной попыткой с Полли
Я работаю с очень нестабильным API. Иногда я получаю 500 Server Error
с Timeout
, в другой раз я тоже получаю 500 Server Error
потому что я дал ему понять, что он не может справиться SqlDateTime overflow. Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM.
,
Оба этих случая дают мне HttpRequestException
но я могу посмотреть ответное сообщение с сервера и определить причину исключения. Если это ошибка тайм-аута, я должен повторить попытку. Если это неверный ввод, я должен повторно выдать исключение, потому что никакое количество повторных попыток не решит проблему неверных данных.
Что я хотел бы сделать с Полли, так это проверить ответное сообщение, прежде чем пытаться повторить попытку. Но все образцы, которые я видел до сих пор, включали только тип исключения.
Я придумал это до сих пор:
HttpResponseMessage response = null;
String stringContent = null;
Policy.Handle<FlakyApiException>()
.WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
async (exception, timeSpan, context) =>
{
response = await client.PostAsync(requestUri, new StringContent(serialisedParameters, Encoding.UTF8, "application/json"));
stringContent = await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.InternalServerError && stringContent.Contains("Timeout"))
{
throw new FlakyApiException(stringContent);
}
});
Есть ли лучший способ сделать такую проверку?
4 ответа
В общем, вы можете настроить политики Polly так, чтобы они реагировали на результаты выполнения (а не только на исключение), например, проверьте HttpResponseMessage.StatusCode
с предикатом. Примеры здесь в readme Полли.
Однако не существует встроенного способа настройки единой политики Polly для дополнительного ответа на содержимое ответного сообщения. Это связано с тем, что (как показывает ваш пример) получение этого контента требует второго асинхронного вызова, который сам может вызвать сетевые ошибки.
Это приводит к сложностям в отношении того, как выразить (в простом синтаксисе) единую политику, которая управляет двумя разными асинхронными шагами с потенциально различной обработкой ошибок для каждого шага. Предыдущее обсуждение Полли Гитхуб: комментарии приветствуются.
Таким образом, когда последовательности требуется два отдельных асинхронных вызова, команда Polly в настоящее время рекомендует выражать это как две отдельные политики, аналогично примеру в конце этого ответа.
Конкретный пример в вашем вопросе может не сработать, потому что onRetryAsync
делегат FlakyApiException
) сама не охраняется политикой. Политика только защищает выполнение делегатов, выполненных через .Execute/ExecuteAsync(...)
,
Одним из подходов может быть использование двух политик: политики повторных попыток, которая повторяет все типичные исключения http и коды состояния, включая 500; затем внутри этого Polly FallbackPolicy, который перехватывает код состояния 500, представляющий SqlDateTime overflow
и исключает повторную попытку повторного броска в качестве некоторого отличительного исключения (CustomSqlDateOverflowException
).
IAsyncPolicy<HttpResponseMessage> rejectSqlError = Policy<HttpResponseMessage>
.HandleResult(r => r.StatusCode == HttpStatusCode.InternalServerError)
.FallbackAsync(async (delegateOutcome, context, token) =>
{
String stringContent = await delegateOutcome.Result.Content.ReadAsStringAsync(); // Could wrap this line in an additional policy as desired.
if (delegateOutcome.Result.StatusCode == HttpStatusCode.InternalServerError && stringContent.Contains("SqlDateTime overflow"))
{
throw new CustomSqlDateOverflowException(); // Replace 500 SqlDateTime overflow with something else.
}
else
{
return delegateOutcome.Result; // render all other 500s as they were
}
}, async (delegateOutcome, context) => { /* log (if desired) that InternalServerError was checked for what kind */ });
IAsyncPolicy<HttpResponseMessage> retryPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => r.StatusCode == HttpStatusCode.InternalServerError)
.OrResult(r => /* condition for any other errors you want to handle */)
.WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
async (exception, timeSpan, context) =>
{
/* log (if desired) retry being invoked */
});
HttpResponseMessage response = await retryPolicy.WrapAsync(rejectSqlError)
.ExecuteAsync(() => client.PostAsync(requestUri, new StringContent(serialisedParameters, Encoding.UTF8, "application/json"), cancellationToken));
Если я правильно понимаю ваш вопрос, вы хотите повторить попытку, только если код состояния равен 500, а тело содержит
Timeout
. Если это так, вы можете определить свою политику так же, как это
Policy<HttpResponseMessage>
.HandleResult(response =>
response.StatusCode == System.Net.HttpStatusCode.InternalServerError
&& response.Content.ReadAsStringAsync().GetAwaiter().GetResult().Contains("Timeout"))
.WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt);
Для Http я решил решить эту проблему, используя шаблон (DH) и polly. Здесь нетHandleResultAsync()
, поэтому проблема все еще существует для обобщенного вопроса.
С Polly я избегаю решения, которое имеет «сцепление».
Я добился большого успеха с использованием политики повторных попыток в соответствии с SRP и обеспечивает хороший SoC (см. этот пост SO ). Вот повторная попытка DH, которую я обычно использую для справки.
Для вашего вопроса есть две вещи: повторная попытка и условия для повторной попытки. Опираясь на повторную попытку DH, я разделил ее на две части.DelegatingHandler
s: повторная попытка DH, которая повторяет попытку по «сигналу», и последняя повторная попытка, сигнализирующая DH, которая сигнализирует о повторной попытке.HttpRequestMessage
х.Properties
(или.Options
) сумка используется для подачи сигнала.
Я считаю, что его легко поддерживать, и он не сложен, поскольку позволяет избежать вложенных политик опроса или блокировки вызова. У меня есть несколько API-интерфейсов, использующих шаблон асинхронного запроса/ответа , поэтому повторная попытка DH (используемая для опроса) может быть повторно использована (нугетизирована), а повторная сигнализация DH отличается в зависимости от API. Очевидно, вы можете объединить их в один, встроив сигнальный код вaction
аргумент
HttpClient CoR (chain of responsibility):
... -> retry on signal DH -> retry signaling DH -> ...
Вот повторная попытка, сигнализирующая DH о ваших условиях для повторной попытки.
public class RetrySignalingOnConditionHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
// tweak conditions accordingly
if (response.StatusCode == (HttpStatusCode)500)
{
request.Properties[RequestProperties.RetrySignal] = true;
return response;
}
var content = await response.Content.ReadAsStringAsync(cancellationToken);
if (content.Contains("Timeout"))
{
request.Properties[RequestProperties.RetrySignal] = true;
return response;
}
return response;
}
}
internal static class RequestProperties
{
internal static string RetrySignal = nameof(RetrySignal);
}
Вот повторная попытка DH, которая повторяет попытку сигнала. Он сбрасывает сигнал перед попыткой.
public class ExponentialBackoffRetryOnSignalHandler : DelegatingHandler
{
private readonly IAsyncPolicy<(HttpRequestMessage request, HttpResponseMessage response)> retryPolicy;
public ExponentialBackoffRetryOnSignalHandler(
IRetrySettings retrySettings)
{
_ = retrySettings
?? throw new ArgumentNullException(nameof(retrySettings));
var sleepDurations = Backoff.ExponentialBackoff(
initialDelay: TimeSpan.FromMilliseconds(retrySettings.RetryDelayInMilliseconds),
retryCount: retrySettings.RetryCount);
retryPolicy = Policy
.HandleResult<(HttpRequestMessage request, HttpResponseMessage response)>(tuple =>
tuple.request.Properties.TryGetValue(RequestProperties.RetrySignal, out var retrySignaledObj) && (bool)retrySignaledObj)
.WaitAndRetryAsync(
sleepDurations: sleepDurations,
onRetry: (responseResult, delay, retryAttempt, context) =>
{
// note: response can be null in case of handled exception
responseResult.Result.response?.Dispose();
});
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var tuple = await retryPolicy.ExecuteAsync(
action: async (ct) =>
{
request.Properties.Remove(RequestProperties.RetrySignal);
var response = await base.SendAsync(request, ct)
.ConfigureAwait(false);
return (request, response);
},
cancellationToken: cancellationToken)
.ConfigureAwait(false);
return tuple.response;
}
}
public interface IRetrySettings
{
int RetryCount { get; }
int RetryDelayInMilliseconds { get; }
}
Вот полный код , который я использую вместе с тестами.
Я бы предложил использовать один класс DelegatingHandler, который выполняет одну IAsyncPolicy в методе SendAsync DelegatingHandler.
Затем тело ответа можно прочитать асинхронно в асинхронной лямбда-выражении, которая передается методу ExecuteAsync IAsyncPolicy.
public class MyRetryDelegatingHandler : DelegatingHandler
{
#region Fields and constructor
private readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy;
/// <param name="retryPolicy">The retryPolicy to execute during the SendAsync operation</param>
public MyRetryDelegatingHandler(IAsyncPolicy<HttpResponseMessage> retryPolicy)
{
_retryPolicy = retryPolicy;
}
#endregion
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Execute retry policy
var response = await _retryPolicy.ExecuteAsync(async () =>
{
// Send the HTTP request using base
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.InternalServerError)
{
// Read the response body
var content = await response.Content.ReadAsStringAsync();
if (content is not null)
{
// Check for timeout in response body
if (!content.Contains("Timeout"))
{
// Throw FlakyApiException if response was not a timeout error
throw new FlakyApiException(content);
}
}
}
return response;
});
return response;
}
}
Затем IAsyncPolicy можно создать в Program.cs следующим образом. IAsyncPolicy также можно создать в DelegatingHandler.
// Create Polly policy
var teamsRetryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(500), retryCount: 5));
// Add TeamsRetryDelegatingHandler to services
builder.Services.AddTransient(provider => new MyRetryDelegatingHandler(teamsRetryPolicy));
// Add a typed HttpClient
// The extension method registers the typed client and type as transient services
builder.Services.AddHttpClient<IMyService>()
.AddHttpMessageHandler<MyRetryDelegatingHandler>();
Типизированный HttpClient затем можно внедрить в класс обслуживания, который использовался в качестве типа типизированного HttpClient. Самый простой пример можно увидеть ниже.
public class MyService
{
#region Fields and constructor
private readonly HttpClient _httpClient;
public MyService(HttpClient httpClient)
{
_httpClient = httpClient;
}
#endregion
public async Task<HttpResponseMessage> SendApiRequest()
{
return await _httpClient.GetAsync("<url>");
}
}
public interface IMyService
{
Task<HttpResponseMessage> SendApiRequest();
}