Polly для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое Polly и зачем она нужна?

Polly — это мощная и гибкая библиотека для .NET (C#, F#, VB.NET), предназначенная для обработки временных сбоев и повышения отказоустойчивости приложений. Она реализует паттерны устойчивости, такие как повторные попытки (Retry), размыкание цепи (Circuit Breaker), тайм-ауты (Timeout), изоляция (Bulkhead Isolation) и кэширование (Cache).

В современном мире микросервисной архитектуры и облачных вычислений приложения постоянно взаимодействуют с внешними ресурсами: базами данных, веб-сервисами, API. Эти взаимодействия могут быть ненадёжными — сетевые ошибки, временная недоступность сервиса, превышение лимитов запросов. Polly позволяет обрабатывать такие ситуации элегантно, без написания громоздкого кода с try-catch блоками.

Основные сценарии использования:

  • Retry (Повторные попытки): Автоматически повторять операцию при временной ошибке.
  • Circuit Breaker (Размыкание цепи): Прекращать попытки вызова, если сервис постоянно выдаёт ошибки, чтобы дать ему время восстановиться.
  • Timeout (Тайм-аут): Ограничивать время выполнения операции.
  • Bulkhead (Изоляция): Ограничивать количество параллельных операций, чтобы одна "плохая" операция не заблокировала весь пул потоков.
  • Cache (Кэширование): Кэшировать результаты успешных операций.
  • Fallback (Запасной вариант): Выполнять альтернативное действие в случае сбоя.

Установка Polly

Polly распространяется через NuGet — менеджер пакетов для .NET. Установить её можно несколькими способами:

Через Package Manager Console (Visual Studio)

Install-Package Polly

Через .NET CLI

dotnet add package Polly

Через PackageReference (в .csproj файле)

<PackageReference Include="Polly" Version="8.2.0" />

После установки добавьте using-директиву в ваш код:

using Polly;
using Polly.Retry;
using Polly.CircuitBreaker;
using Polly.Timeout;
using Polly.Bulkhead;
using Polly.Caching;
using Polly.Fallback;
using Polly.Wrap;

Быстрый старт с примером кода

Рассмотрим простейший пример: у нас есть метод, который вызывает внешний API, и мы хотим повторить вызов 3 раза, если возникает исключение.

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Polly;
using Polly.Retry;

public class ApiClient
{
    private readonly HttpClient _httpClient;
    private readonly AsyncRetryPolicy _retryPolicy;

    public ApiClient()
    {
        _httpClient = new HttpClient();
        
        // Создаём политику повторных попыток
        _retryPolicy = Policy
            .Handle<HttpRequestException>() // Какие исключения обрабатывать
            .Or<TimeoutException>()          // Или тайм-аут
            .WaitAndRetryAsync(
                3,                           // Количество попыток
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // Экспоненциальная задержка
                onRetry: (exception, timeSpan, retryCount, context) =>
                {
                    Console.WriteLine($"Попытка {retryCount} не удалась. Повтор через {timeSpan.Seconds} сек. Ошибка: {exception.Message}");
                }
            );
    }

    public async Task<string> GetDataAsync(string url)
    {
        // Выполняем запрос с использованием политики
        return await _retryPolicy.ExecuteAsync(async () =>
        {
            var response = await _httpClient.GetAsync(url);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        });
    }
}

// Использование
public class Program
{
    public static async Task Main()
    {
        var client = new ApiClient();
        try
        {
            var data = await client.GetDataAsync("https://api.example.com/data");
            Console.WriteLine($"Получены данные: {data}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Все попытки исчерпаны. Ошибка: {ex.Message}");
        }
    }
}

Основные методы и функции Polly

Polly предоставляет богатый набор методов для создания политик устойчивости. Рассмотрим 10 ключевых методов с примерами.

1. Handle<TException>()

Что делает: Определяет, какие типы исключений должна обрабатывать политика. Это базовый метод для всех политик.

Параметры: Generic-параметр TException — тип исключения.

// Обрабатываем только HttpRequestException
var policy = Policy.Handle<HttpRequestException>();

// Обрабатываем несколько типов исключений
var policy = Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .Or<TaskCanceledException>();

2. HandleResult<TResult>()

Что делает: Определяет, какие результаты (не исключения) должна обрабатывать политика. Полезно, когда API возвращает код ошибки, а не выбрасывает исключение.

Параметры: Func<TResult, bool> — функция-предикат, которая проверяет результат.

// Обрабатываем результат, если он равен null или содержит ошибку
var policy = Policy
    .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .RetryAsync(3);

// Использование
var result = await policy.ExecuteAsync(() => httpClient.GetAsync("https://api.example.com"));

3. Retry() / RetryAsync()

Что делает: Создаёт политику повторных попыток. При возникновении исключения (или неудовлетворительного результата) операция будет повторена указанное количество раз.

Параметры: retryCount (int) — количество попыток; onRetry (Action) — действие, выполняемое при каждой попытке.

// Простая политика: 3 попытки без задержки
var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .RetryAsync(3);

// С действием при каждой попытке
var retryPolicyWithAction = Policy
    .Handle<HttpRequestException>()
    .RetryAsync(3, onRetry: (exception, retryCount) =>
    {
        Console.WriteLine($"Попытка {retryCount} не удалась: {exception.Message}");
    });

// Синхронная версия
var syncRetryPolicy = Policy
    .Handle<Exception>()
    .Retry(3);

4. WaitAndRetry() / WaitAndRetryAsync()

Что делает: Создаёт политику повторных попыток с задержкой между ними. Задержка может быть фиксированной, экспоненциальной или вычисляемой динамически.

Параметры: retryCount (int); sleepDurationProvider (Func<int, TimeSpan>) — функция, определяющая задержку; onRetry (Action).

// Фиксированная задержка: 2 секунды между попытками
var fixedDelayPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(2));

// Экспоненциальная задержка: 2^1=2с, 2^2=4с, 2^3=8с
var exponentialDelayPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

// Динамическая задержка с джиттером (случайное отклонение)
var jitterPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(3, retryAttempt =>
    {
        var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));
        var jitter = TimeSpan.FromMilliseconds(new Random().Next(0, 1000));
        return baseDelay + jitter;
    });

5. CircuitBreaker() / CircuitBreakerAsync()

Что делает: Создаёт политику размыкания цепи. Если количество ошибок превышает порог за определённое время, цепь "размыкается" и последующие вызовы сразу завершаются ошибкой, давая сервису время восстановиться.

Параметры: exceptionsAllowedBeforeBreaking (int) — количество ошибок для размыкания; durationOfBreak (TimeSpan) — время, на которое цепь размыкается; onBreak, onReset, onHalfOpen (Action) — колбэки.

// Политика размыкания цепи: 3 ошибки за 30 секунд, размыкание на 1 минуту
var circuitBreakerPolicy = Policy
    .Handle<HttpRequestException>()
    .CircuitBreakerAsync(
        3,                              // Количество ошибок для размыкания
        TimeSpan.FromMinutes(1),        // Время размыкания
        onBreak: (exception, duration) =>
        {
            Console.WriteLine($"Цепь разомкнута на {duration.TotalSeconds} сек. Ошибка: {exception.Message}");
        },
        onReset: () =>
        {
            Console.WriteLine("Цепь восстановлена.");
        },
        onHalfOpen: () =>
        {
            Console.WriteLine("Цепь в полуоткрытом состоянии. Тестируем...");
        }
    );

// Использование
try
{
    var result = await circuitBreakerPolicy.ExecuteAsync(() => SomeUnreliableMethod());
}
catch (BrokenCircuitException ex)
{
    Console.WriteLine("Цепь разомкнута. Вызов не выполнен.");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Ошибка HTTP: {ex.Message}");
}

6. Timeout() / TimeoutAsync()

Что делает: Создаёт политику тайм-аута. Операция будет прервана, если она выполняется дольше указанного времени.

Параметры: seconds (int) или timeout (TimeSpan); timeoutStrategy (TimeoutStrategy) — Optimistic (оптимистичный) или Pessimistic (пессимистичный).

// Простой тайм-аут: 5 секунд
var timeoutPolicy = Policy
    .TimeoutAsync(5);

// Тайм-аут с TimeSpan
var timeoutPolicyWithSpan = Policy
    .TimeoutAsync(TimeSpan.FromSeconds(5));

// Пессимистичный тайм-аут (прерывает поток)
var pessimisticTimeout = Policy
    .TimeoutAsync(5, TimeoutStrategy.Pessimistic);

// Использование с обработкой исключения
try
{
    var result = await timeoutPolicy.ExecuteAsync(async () =>
    {
        await Task.Delay(10000); // Долгая операция
        return "Done";
    });
}
catch (TimeoutRejectedException ex)
{
    Console.WriteLine("Операция превысила тайм-аут.");
}

7. Bulkhead() / BulkheadAsync()

Что делает: Создаёт политику изоляции (Bulkhead). Ограничивает максимальное количество параллельных операций и количество ожидающих операций в очереди.

Параметры: maxParallelization (int) — максимальное количество параллельных операций; maxQueuingActions (int) — максимальный размер очереди; onBulkheadRejected (Action) — действие при отклонении.

// Изоляция: не более 2 параллельных операций, очередь не более 4
var bulkheadPolicy = Policy
    .BulkheadAsync(
        2,                              // Максимум параллельных операций
        4,                              // Максимум в очереди
        onBulkheadRejected: context =>
        {
            Console.WriteLine("Операция отклонена из-за переполнения очереди.");
        }
    );

// Использование
var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
    tasks.Add(bulkheadPolicy.ExecuteAsync(async () =>
    {
        Console.WriteLine($"Начало операции {i}");
        await Task.Delay(1000);
        Console.WriteLine($"Завершение операции {i}");
    }));
}
await Task.WhenAll(tasks);

8. Fallback() / FallbackAsync()

Что делает: Создаёт политику запасного варианта. Если основная операция завершается ошибкой, выполняется альтернативное действие.

Параметры: fallbackAction (Func<CancellationToken, Task<TResult>>) — действие, выполняемое при сбое; onFallback (Action) — действие при активации запасного варианта.

// Запасной вариант: возвраща                    

Рекомендаций