Resilience (відмовостійкість) — здатність системи продовжувати функціонувати або коректно деградувати під впливом збоїв: мережевих помилок, тайм-аутів, перевантаження зовнішніх сервісів.
Без резильєнтності навіть тимчасова недоступність зовнішнього сервісу може призвести до каскадного збою всього додатку. Polly вирішує цю проблему через набір resilience strategies (стратегій відмовостійкості):
Retry
Circuit Breaker
Timeout
Fallback
Rate Limiter
Hedging
Починаючи з .NET 8, Microsoft разом з командою Polly додали підтримку резильєнтності безпосередньо у framework через Microsoft.Extensions.Http.Resilience. Це об'єднує Polly v8 з DI та HttpClient.
dotnet add package Microsoft.Extensions.Http.Resilience
dotnet add package Polly.Core
dotnet add package Polly
dotnet add package Microsoft.Extensions.Http.Polly
Найшвидший спосіб додати резильєнтність — AddStandardResilienceHandler():
builder.Services.AddHttpClient<IPaymentGateway, PaymentGateway>(client =>
{
client.BaseAddress = new Uri("https://payment-api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddStandardResilienceHandler(); // Вмикає: Retry + Circuit Breaker + Timeout
AddStandardResilienceHandler() автоматично налаштовує:
Retry (повтор) — найпростіша стратегія: якщо запит завершився збоєм, спробуємо ще раз. Логіка: більшість мережевих проблем — тимчасові (transient faults).
using Polly;
using Polly.Retry;
builder.Services.AddHttpClient<IWeatherService, WeatherService>()
.AddResilienceHandler("weather-retry", pipelineBuilder =>
{
pipelineBuilder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
// Максимальна кількість повторів (не рахуючи перший запит)
MaxRetryAttempts = 3,
// Базова затримка між спробами (exponential: 1s, 2s, 4s)
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
// Додаємо випадковий «шум» (jitter) щоб уникнути «thundering herd»
// Усі клієнти одночасно не ретраються у той самий момент
UseJitter = true,
// Які помилки вважати «ретраблями»
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.Handle<TimeoutRejectedException>()
.HandleResult(r => r.StatusCode == HttpStatusCode.ServiceUnavailable)
.HandleResult(r => r.StatusCode == HttpStatusCode.TooManyRequests),
// Колбек при кожній спробі
OnRetry = static args =>
{
Console.WriteLine(
$"Retry #{args.AttemptNumber}. Delay: {args.RetryDelay.TotalSeconds:F1}s");
return ValueTask.CompletedTask;
}
});
});
Проста стратегія «retry відразу» може погіршити ситуацію: якщо 1000 клієнтів одночасно отримали помилку і одночасно ретраються — сервер отримує навантаження в 1000 разів більше. Рішення:
Circuit Breaker (автоматичний вимикач) — це «запобіжник» для HTTP-запитів. В електриці: якщо виникає коротке замикання — запобіжник розриває ланцюг, захищаючи пристрої. В мікросервісах: якщо downstream-сервіс стабільно падає — припиняємо надсилати запити, даємо йому «відпочити».
pipelineBuilder.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
// Мінімальна кількість запитів для розрахунку відсотка помилок
MinimumThroughput = 10,
// Якщо більше 50% запитів за останні 30 секунд завершились збоєм — відкриваємо
FailureRatio = 0.5,
// Вікно спостереження (sampling window)
SamplingDuration = TimeSpan.FromSeconds(30),
// Скільки часу тримати circuit breaker відкритим
BreakDuration = TimeSpan.FromSeconds(15),
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError),
OnOpened = static args =>
{
Console.WriteLine(
$"⚡ Circuit Breaker відкрито! Причина: {args.Outcome.Exception?.Message}");
return ValueTask.CompletedTask;
},
OnClosed = static _ =>
{
Console.WriteLine("✅ Circuit Breaker закрито — сервіс відновився.");
return ValueTask.CompletedTask;
}
});
try
{
var result = await _httpClient.GetAsync("/api/products");
// ...
}
catch (BrokenCircuitException)
{
// Circuit Breaker відкритий — повертаємо cached або дефолтне значення
return _cache.Get<List<Product>>("products") ?? [];
}
pipelineBuilder.AddTimeout(new TimeoutStrategyOptions
{
// Якщо після 5 секунд немає відповіді — скасовуємо
Timeout = TimeSpan.FromSeconds(5),
OnTimeout = static args =>
{
Console.WriteLine(
$"⏱ Timeout! Операція тривала більше {args.Timeout.TotalSeconds}s");
return ValueTask.CompletedTask;
}
});
Fallback (запасний план) — якщо основна операція не вдалася, повертаємо заздалегідь підготований результат:
pipelineBuilder.AddFallback(new FallbackStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<TimeoutRejectedException>()
.Handle<BrokenCircuitException>()
.Handle<HttpRequestException>(),
// Що повертати при збої
FallbackAction = static args =>
{
var fallbackResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
"""{"products": [], "source": "cache", "degraded": true}""",
Encoding.UTF8, "application/json")
};
return ValueTask.FromResult(fallbackResponse);
},
OnFallback = static args =>
{
Console.WriteLine(
$"⚠️ Fallback активовано. Причина: {args.Outcome.Exception?.Message}");
return ValueTask.CompletedTask;
}
});
Стратегії можна об'єднувати в один pipeline. Порядок важливий — виконуються зовні досередини:
builder.Services.AddHttpClient<IPaymentGateway, PaymentGateway>(client =>
client.BaseAddress = new Uri("https://payment-api.example.com"))
.AddResilienceHandler("payment-pipeline", pipeline =>
{
// 1. Fallback (зовнішній — перший що спрацює при будь-якій помилці)
pipeline.AddFallback(new FallbackStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<Exception>(),
FallbackAction = _ =>
{
var r = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
{
Content = new StringContent("""{"error": "Payment service unavailable"}""")
};
return ValueTask.FromResult(r);
}
});
// 2. Circuit Breaker
pipeline.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.5,
MinimumThroughput = 5,
SamplingDuration = TimeSpan.FromSeconds(10),
BreakDuration = TimeSpan.FromSeconds(30)
});
// 3. Retry (внутрішній — виконується до Circuit Breaker)
pipeline.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode == HttpStatusCode.ServiceUnavailable)
});
// 4. Timeout на кожну спробу (найвнутрішніший)
pipeline.AddTimeout(TimeSpan.FromSeconds(5));
});
При запиті порядок виконання: Fallback → CircuitBreaker → Retry → Timeout → Запит → Retry → CircuitBreaker → Fallback. Тобто Timeout застосовується до кожної спроби в Retry, а Circuit Breaker рахує загальну кількість помилок.
Для не-HTTP коду (виклики до Redis, файлової системи) використовуйте ResiliencePipelineProvider<TKey>:
using Polly;
using Polly.Registry;
public class ResilientRedisService
{
private readonly ResiliencePipeline _pipeline;
private readonly IConnectionMultiplexer _redis;
public ResilientRedisService(
ResiliencePipelineProvider<string> pipelineProvider,
IConnectionMultiplexer redis)
{
_pipeline = pipelineProvider.GetPipeline("redis-pipeline");
_redis = redis;
}
public async Task<string?> GetAsync(string key)
{
return await _pipeline.ExecuteAsync(async ct =>
{
var db = _redis.GetDatabase();
var value = await db.StringGetAsync(key);
return (string?)value;
});
}
}
builder.Services.AddResiliencePipeline("redis-pipeline", (pipelineBuilder, _) =>
{
pipelineBuilder
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(100),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder()
.Handle<RedisException>()
.Handle<TimeoutException>()
})
.AddTimeout(TimeSpan.FromSeconds(2));
});
Завдання 1.1. Налаштуйте HttpClient для зовнішнього WeatherAPI з Retry-стратегією: 3 спроби, exponential backoff (1s, 2s, 4s), jitter. Логуйте номер спроби при кожному ретраї.
Завдання 1.2. Додайте Timeout 5 секунд до кожної спроби. Протестуйте поведінку при затримці сервера.
Завдання 2.1. Реалізуйте Circuit Breaker для PaymentService: відкривається після 30% помилок за 20 секунд (мінімум 5 запитів). Break duration — 10 секунд. Логуйте зміни стану.
Завдання 2.2. Додайте Fallback до Payment Circuit Breaker: при відкритому ланцюзі повертайте ErrorOr з помилкою Error.Unavailable("Payment.Unavailable", "Сервіс оплати тимчасово недоступний. Спробуйте через кілька хвилин.").
Завдання 3.1. Побудуйте повну resilience pipeline для NotificationService (SMS gateway): Fallback → CircuitBreaker → Retry (3 спроби, exponential) → Timeout (3s). При Fallback — зберігайте повідомлення в чергу (IQueue) для пізнішого відправлення.
Завдання 3.2. Напишіть тест, що симулює: перша спроба — timeout, друга — 503, третя — успіх. Переконайтесь, що retry відпрацював 3 рази і повернув результат.
Polly перетворює ваш код із «наївного» на «захищений»:
Retry
Circuit Breaker
.NET 8 Integration
AddStandardResilienceHandler() — готова до виробництва конфігурація одним рядком.Composable
Посилання: