Во многих задачах опоздания или ошибки при передаче данных могут привести к неприятным последствиям, например к штрафам. Чтобы всего этого избежать, нужно использовать повторную отправку запросов (а что делать, если мы не можем переотправлять вечно?), настраивать метрики и логирование, а также действия при ошибках и сбоях. Все это обширные и интересные темы, и я начну с первой. 

Привет, Хабр! Меня зовут Андрей Алексеенко, я техлид оператора рекламных данных (ОРД) «МедиаСкаут». Особенность нашего продукта — взаимодействие с единым реестром интернет-рекламы (ЕРИР), у которого очень жесткие требования. По данным Роскомнадзора, с января по октябрь 2024 года было вынесено 376 постановлений о нарушениях в сфере интернет-рекламы на общую сумму 24,4 млн рублей.

Чтобы обеспечить переотправку запроса, мы используем библиотеку Polly. Она позволяет реализовать стратегию повторной отправки, и в этом материале я расскажу, как ее можно внедрить в свой продукт. 

Взаимодействие ОРД с ЕРИР

Об общих моментах работы «МедиаСкаута» я рассказывал в прошлом материале: «Техноизнанка ОРД: как мы на ходу подстраиваемся под возможности рынка и требования регулятора». Если кратко, то мы находимся меж двух огней: с одной стороны рекламодатели, а с другой — ЕРИР. Мы же должны обеспечить им надежное взаимодействие. 

Предположим, рекламодатель передает нам данные о креативе или площадке. В этом случае все относ��тельно просто, так как для его регистрации достаточно одного запроса. Если в дополнение к ним идет акт, то процесс усложняется. Передача разделяется на несколько этапов, и на любом из них может произойти сбой.

И тут возникает вопрос — что в этом случае делать? Подождать и начать сначала, подождать и попробовать еще раз, а затем еще раз, а может, подождать минуту, потом три минуты — и продолжить с текущего места?.. Вариантов много, а сколько может быть написано кода, даже думать страшно! 

Да, это проблема неуникальна, и уже есть решения, которые прекрасно себя зарекомендовали. 

Polly — библиотека для повышения устойчивости приложений

Уверен, что многие так или иначе слышали об этом замечательном инструменте: он позволяет легко определить сбои при соединениях и применить такие стратегии, как Retry, Circuit Breaker, Hedging, Timeout, Rate Limiter и Fallback. 

Моменты с первым подключением, думаю, можно опустить, так как на официальном сайте все описано довольно четко. Замечу лишь, что мы для наглядности и удобства хотели держать обработку и настройку вместе, поэтому реализовали инициализацию иначе.

 Если следовать примерам из официальной документации, то с развитием проекта код инициализации конечного приложения может стать довольно-таки громоздким.

Настройка Polly для работы 

Начнем с самых массовых случаев:

  • сетевые сбои или обрывы подключения;

  • внезапные и плановые технические работы на целевом сервере.

Это временные проблемы, поэтому тут лучше всего подойдет стратегия повторной отправки. Для ее описания создадим отдельный класс, наследуемый от RetryStrategyOptions:

internal sealed class RetryStrategyApiHlException : RetryStrategyOptions
{
    public RetryStrategyApiHlException()
    {
        MaxRetryAttempts = 12;
        
        DelayGenerator = args =>
        {
            var delay = args.AttemptNumber switch
            {
                0 => TimeSpan.FromMinutes(1),
                1 => TimeSpan.FromMinutes(3),
                2 => TimeSpan.FromMinutes(5),
                3 => TimeSpan.FromMinutes(8),
                4 => TimeSpan.FromMinutes(30),
                5 => TimeSpan.FromHours(1),
                _ => TimeSpan.FromMinutes(30)
            };
            
            return new ValueTask<TimeSpan?>(delay);
        };

        ShouldHandle = new PredicateBuilder()
            .Handle<ApiHl6Exception>(apiHlException => CheckResponseCode(apiHlException.StatusCode))
            .Handle<HttpRequestException>()
            .Handle<TaskCanceledException>()
            .Handle<TimeoutException>();
    }

    private static bool CheckResponseCode(int statusCode)
    {
        switch (statusCode)
        {
            case (int)HttpStatusCode.InternalServerError:
            case (int)HttpStatusCode.NotImplemented:
            case (int)HttpStatusCode.BadGateway:
            case (int)HttpStatusCode.Forbidden:
            case (int)HttpStatusCode.ServiceUnavailable:
            case (int)HttpStatusCode.GatewayTimeout:
                {
                    return true;
                }
            default:
                {
                    return false;
                }
        }
    }
}

В примере мы обрабатываем несколько типов исключений, к которым пришли уже в ходе совместной работы с коллегами из ЕРИР. 

Далее остается регистрация в IoC-контейнере:

serviceCollection.AddResiliencePipeline(ErirResilience.RetryStrategyApiHlExceptionPipeline, (builder, _) => builder.AddRetry(new RetryStrategyApiHlException()));

Так мы решаем сразу две маленькие задачи: выносим обработку стратегий в отдельный класс, а также сокращаем код инициализации самого приложения. 

Если вам вдруг понадобится доступ к IoC-контейнеру  (например, в момент обработки), то его легко организовать: 

serviceCollection  .AddResiliencePipeline(ErirResilience.RetryStrategyApiHlExceptionPipeline, (builder, context) =>
    {
        builder.AddRetry(new RetryStrategyApiHlException(context.ServiceProvider));
    })

В конструктор RetryStrategyApiHlException добавляем либо ссылку на IServiceProvider (как показано в примере,  доступна через контекст Polly) и уже внутри класса получаем нужный нам сервис либо же описываем его на этапе конфигурации и далее через конструктор передаем на него ссылку. 

Тут уже дело вкуса, но, на мой взгляд, лучше все же избегать использования внедрения сервисов, так как это перегружает функциональность. Задача стратегии только одна — повтор операции.

Итак, настройка выполнена — перейдем к непосредственному использованию Polly в работе. 

Связка Polly с MediatR

Логика взаимодействия с ЕРИР у нас выпо��няется через MediatR — этот инструмент в представлении не нуждается, и его плюсы расписывать смысла нет. 

Остановлюсь на сопряжении Polly и MediatR. Как известно, у MediatR есть точка расширения через имплементацию интерфейса IPipelineBehavior<,> — вот ею и воспользуемся:

public sealed class ResilienceCqrsPipelineBehavior <TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
    private readonly ResiliencePipelineProvider<string> resiliencePipelineProvider;
    
    public ResilienceCqrsPipelineBehavior(ResiliencePipelineProvider<string> resiliencePipelineProvider)
    {
        resiliencePipelineProvider = resiliencePipelineProvider;
    }
    
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        TResponse? response;
        
        if (request is IResiliencePipeline resilience && resiliencePipelineProvider.TryGetPipeline(resilience.ResilienceName, out var resiliencePipeline))
        {
            var queryResult = await resiliencePipeline.ExecuteOutcomeAsync<TResponse, object>(async (, _) =>
                {
                    response = await next(cancellationToken);
                    return Outcome.FromResult(response);
                },
                ResilienceContextPool.Shared.Get(),
                request);

            response = queryResult.Result;
        }
        else
        {
            response = await next(cancellationToken);
        }
        
        return response;
    }
}

И не забываем про регистрацию в ServiceCollection:

serviceCollection.AddScoped(typeof(IPipelineBehavior<,>), typeof(ResilienceCqrsPipelineBehavior<,>));

Интерфейс IResiliencePipeline содержит описание свойства для получения наименования конкретной конфигурации стратегии: 

public interface IResiliencePipeline
{
    string ResilienceName { get; }
}

И, как вы уже поняли, добавляется к определению запроса или команды, реализующий интерфейс IRequest<>:\

public sealed class RegistryCreativeCommand : IRequest, IResiliencePipeline
 {
     public string ResilienceName => ErirResilience.RetryStrategyApiHlExceptionPipeline;
    
     public required IReadOnlyCollection CreativeIds { get; init; }
 }

Теперь, вызывая команды или запросы MeditaR, мы получили более отказоустойчивую систему. И конечно, это не конец реализации — будем потихоньку двигаться дальше. 

Знакомые с Polly могут справедливо заметить, что для http-запросов существуют отдельные стратегии. Заноза в том, что мы используем в своей работе nswag для генерации клиента и он берет на себя преобразование http-ответов в исключения. Это удобно, так как коллеги из ЕРИР на своей стороне реализовали очень хорошее описание ошибок.

Конечно же, у вас возникнет вопрос: а что дальше, если переотправка не дала результатов и ошибка осталась на месте? Это уже отдельная тема для разговора, которую я раскрою в следующих материалах. 

Что нам дало внедрение Polly

Благодаря внедрению этой библиотеки мы:

  1. разделили регистрацию и описание стратегий переотправки, чем облегчили дальнейшее расширение функциональности проекта;

  2. повысили стабильность работы системы за счет применения стратегий повторений запросов;

  3. получили возможность простого добавления поддержки стратегий повторений операций в запросы и команды, причем именно там, где это действительно необходимо.

Сама библиотека Polly обладает массой полезных функций, о чем мы, конечно же, еще поговорим. Используя приведенный в этом посте код, вы можете практически бесшовно внедрить у себя аналогичную функциональность. На этом у меня все, готов ответить на ваши вопросы.

Комментарии (1)


  1. IvanoDigital
    17.11.2025 09:18

    Какая страшная картинка.. жуть