Într-un sistem distribuit, aplicațiile vor eșua, nu pentru că cineva a greșit sau aplicația a fost scrisă prost. Explicația eșecului este alta. Deoarece rețelele sunt imprevizibile, serviciile rulează pe mașini diferite sau se supraîncarcă, bazele de date pot deveni lente, iar APIurile externe uneori nu mai răspund. Atunci când multe servicii comunică între ele, chiar și o întârziere mică sau o întrerupere temporară poate provoca probleme mult mai mari dacă sistemul nu este pregătit.
De aceea, reziliența trebuie gândită încă de la început. Cu instrumente precum Polly, putem adăuga comportamente inteligente care ajută aplicațiile să se recupereze într-un mod robust, în loc să se blocheze sau să se prăbușească. Construirea rezilienței din timp asigură că sistemele noastre rămân stabile și receptive, oferind o experiență mai bună utilizatorilor.
Polly este librăria de reziliență pe care întregul ecosistem .NET se bazează pentru a gestiona tipurile de erori care apar în mod natural în sistemele distribuite. Ea oferă modele integrate precum retry, timeout, circuit breaker și bulkhead, ajutând aplicațiile să rămână receptive chiar și atunci când serviciile externe devin lente sau nesigure.
Începând cu .NET 8 și continuând cu .NET 10+, Microsoft a integrat Polly direct în infrastructura de reziliență a platformei, în special prin HttpClientFactory, metoda recomandată pentru a efectua apeluri HTTP în aplicațiile moderne .NET. Această integrare le permite dezvoltatorilor să aplice politici de reziliență cu doar câteva linii de configurație, în loc să scrie logică personalizată de retry sau cod complex de tratare a erorilor.
Pentru că Polly este atât de puternică, dar și ușor de folosit, a devenit alegerea standard pentru construirea de microservicii fiabile în .NET, transformând-o într-un instrument esențial pentru inginerii de orice nivel care vor să proiecteze sisteme ce se comportă previzibil chiar și în condiții de stres.
În sistemele distribuite, erorile temporare sunt foarte frecvente. Un serviciu poate fi lent pentru câteva momente, un pachet de rețea poate fi pierdut sau un API poate răspunde pentru scurt timp cu o eroare 500. O politică de retry ajută aplicația să încerce automat operațiunea din nou, în loc să eșueze imediat. Este una dintre cele mai simple și eficiente tehnici de reziliență.
Polly oferă mai multe moduri de a reîncerca operațiunile eșuate, cum ar fi:
reîncercarea după o întârziere fixă;
creșterea treptată a întârzierii la fiecare încercare (exponential backoff);
Atunci când se folosește retry, ar trebui reîncercate doar erorile temporare (tranzitorii) — cum ar fi timeouts sau răspunsurile de tip "too many requests" (408, 429, 502, 503, 504). De asemenea, ar trebui reîncercate doar operațiunile idempotente, adică acele operațiuni care pot fi repetate fără a produce efecte secundare nedorite. Pentru a evita agravarea problemei, de folosit întotdeauna exponential backoff și jitter, astfel încât reîncercările să nu se întâmple toate în același timp.
Modelul Circuit Breaker protejează sistemul de apeluri repetate către un serviciu care deja eșuează. În loc să tot încerci la nesfârșit și să se agraveze problema, circuit breakerul "se deschide" după un anumit număr de erori și blochează temporar apelurile către serviciul nesănătos. Acest lucru oferă timp serviciului defect să își revină și împiedică aplicația să irosească resurse pe apeluri care aproape sigur vor eșua.
O modalitate simplă de a înțelege aceste doua concepte este: Retry ajută atunci când un serviciu este temporar lent. Circuit breaker ajută atunci când un serviciu este constant defect.
Când circuitul este deschis, apelurile eșuează imediat. După o perioadă de răcire, circuitul trece în starea "halfopen", permițând câteva apeluri de test. Dacă acestea reușesc, circuitul se închide din nou și traficul revine la normal.
Acest model este esențial în sistemele distribuite deoarece previne "defecțiunile în cascadă", situații în care un singur serviciu defect provoacă eșecuri în lanț în multe alte servicii din cauza încărcării suplimentare.
O politică de timeout protejează aplicația de situațiile în care se așteaptă prea mult după un răspuns. În sistemele distribuite, un serviciu lent poate fi la fel de dăunător ca unul care eșuează complet. Dacă aplicația așteaptă la nesfârșit, threadurile se blochează, cozile se umplu, iar întregul sistem devine brusc lent.
O politică de timeout rezolvă această problemă prin regula:
"Dacă acest apel nu răspunde în X secunde, oprește așteptarea și eșuează rapid."
Astfel, sistemul rămâne receptiv și previne consumul excesiv de resurse.
Când să folosim timeouturi:
Când se apelează APIuri externe;
Nu lăsa o singură defecțiune să scufunde întreaga "navă". Modelul Bulkhead provine din construcția navelor. Navele sunt împărțite în compartimente ("bulkheads"), astfel încât, dacă unul se inundă, întreaga navă nu se scufundă.
În sistemele distribuite, un bulkhead limitează câte apeluri simultane pot fi făcute către o anumită dependență. Dacă un serviciu devine lent sau supraîncărcat, nu se dorește ca toate threadurile tale să rămână blocate așteptând după el.
Un bulkhead spune practic:
"Doar X apeluri către acest serviciu în același timp. Restul eșuează rapid."
Acest lucru protejează restul sistemului de a fi tras în jos de un singur serviciu problematic.
Platforma .NET 10 include un pipeline de reziliență integrat, bazat pe Polly, care aplică automat politicile în ordinea corectă atunci când folosim HttpClientFactory și AddStandardResilienceHandler.
Într-un sistem distribuit real, o singură politică nu este suficientă.
O politică de retry poate ajuta în cazul erorilor temporare, dar nu te protejează de apeluri lente. Un timeout previne blocarea pe operațiuni care durează prea mult, dar nu împiedică supraîncărcarea unui serviciu deja instabil. Un circuit breaker oprește apelurile către un serviciu care eșuează constant, dar nu reîncearcă atunci când problema este tranzitorie. Iar un bulkhead limitează câte cereri simultane pot intra, prevenind blocarea resurselor.
De aceea, .NET 10 combină automat aceste politici într-un pipeline coerent și predictibil. Practic, platforma "stivuiește" politicile astfel încât fiecare să protejeze sistemul de un alt tip de problemă.
O combinație tipică arată astfel:
Bulkhead → limitează numărul de cereri simultane;
Timeout → nu aștepta prea mult;
Retry → încearcă din nou dacă a fost o problemă temporară;
Ordinea corectă a strategiilor
.NET 10 aplică politicile în această ordine:
Bulkhead (cel mai exterior strat),
Timeout,
Retry,
Această ordine asigură că:
Nu se reîncearcă la nesfârșit.
Nu se reîncearcă apeluri care deja au expirat (timeout).
Circuit breakerul vede eșecurile reale și se poate deschide corect.
În aplicațiile moderne .NET, metoda recomandată pentru a face apeluri HTTP este prin HttpClientFactory, care este integrat direct în sistemul de dependency injection (DI).
Acesta este un mare avantaj: se obține reziliență de nivel enterprise cu doar câteva linii de cod și se evita greșelile comune, cum ar fi crearea excesivă de instanțe HttpClient sau ordonarea greșită a politicilor.
Folosirea HttpClientFactory împreună cu handlerul de reziliență integrat îți oferă:
Integrare automată cu Polly;
Configurație centralizată pentru toți clienții HTTP;
Ordine corectă a politicilor (bulkhead → timeout → retry → circuit breaker);
Telemetrie integrată pentru logare și monitorizare;
Configurare separată pentru fiecare client (fiecare API poate avea propriile reguli);
Aceasta este metoda standard pentru a construi microservicii reziliente în .NET 10+.
Un exemplu simplu de configurare a unui client HTTP rezilient folosind pipelineul integrat Polly:
builder.Services.AddHttpClient("WeatherApi")
.AddStandardResilienceHandler(options =>
{
// Retry cu exponential backoff + jitter
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan
.FromMilliseconds(200);
options.Retry.UseJitter = true;
// Timeout pentru fiecare încercare
options.Timeout.Timeout = TimeSpan
.FromSeconds(3);
// Circuit breaker pentru erori persistente
options.CircuitBreaker.FailureThreshold = 0.5;
options.CircuitBreaker.SamplingDuration =
TimeSpan.FromSeconds(30);
options.CircuitBreaker.MinimumThroughput = 10;
options.CircuitBreaker.BreakDuration = TimeSpan
.FromSeconds(15);
// Bulkhead pentru limitarea concurenței
options.Bulkhead.MaxConcurrentRequests = 10;
options.Bulkhead.QueueLimit = 5;
});
După înregistrare, injectezi clientul în serviciul tău:
public class WeatherService(HttpClient client)
{
public async Task GetForecastAsync()
{
var response = await client.GetAsync("/forecast");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
Și îl înregistrezi astfel:
builder.Services.AddTransient<WeatherService>();
builder.Services.AddHttpClient<WeatherService>("WeatherApi");
Acum fiecare apel către Weather API folosește automat pipelineul de reziliență configurat.
Cum funcționează?
Încearcă din nou apelul dacă eroarea este tranzitorie (ex: 408, 429, 502, 503, 504).
Folosește exponential backoff + jitter, adică:
întârzierea crește la fiecare retry;
Configurația:
3 încercări maxime;
200 ms întârziere de bază;
Rezultat: reîncearcă inteligent, fără să supraîncarce serviciul.
Dacă un apel durează mai mult de 3 secunde, este întrerupt.
previne blocarea threadurilor;
împiedică acumularea de cereri lente;
Dacă serviciul este instabil, clientul nu îl mai bombardează cu cereri, ci așteaptă să își revină.
FailureThreshold = 0.5 Dacă jumătate din apeluri eșuează în perioada de sampling, circuitul se deschide.
SamplingDuration = 30 secunde În această fereastră de timp se calculează rata de eșec.
MinimumThroughput = 10 Circuit breakerul nu ia decizii, dacă nu există cel puțin 10 apeluri (evită "fals pozitive").
MaxConcurrentRequests = 10 - Numărul de cereri simultane permise.
Serviciile terțe (plăți, geolocație, SMS, email, hărți etc.) pot deveni lente în orele de vârf. Fără timeout și retry:
cererile tale pot bloca threaduri;
aplicația devine lentă;
Cu reziliență configurată:
timeout oprește cererile lente;
retry încearcă din nou dacă problema este temporară;
Multe APIuri moderne limitează numărul de cereri pe minut. Când primești 429:
retry cu exponential backoff + jitter ajută la reîncercarea cererii fără a supraîncărca serviciul;
Uneori baza de date sau Redis poate avea spikeuri de latență. În aceste situații:
timeout previne blocarea aplicației;
retry poate ajuta dacă problema este temporară;
citirea datelor;
verificarea unui status;
obținerea unui token;
fiecare retry poate dura foarte mult;
threadurile rămân blocate;
latența explodează;
Acest lucru poate duce la:
dublarea comenzilor;
tranzacții duplicate;
Fără circuit breaker:
serviciul tău continuă să trimită cereri către un API căzut;
latența crește;
threadurile se blochează;
În dezvoltarea de aplicații distribuite, reziliența nu este un "nicetohave", ci o necesitate. Politicile Retry, Circuit Breaker, Timeout și Bulkhead te ajută să construiești servicii stabile, capabile să gestioneze erori, latențe și dependențe imprevizibile. Înțelegerea și aplicarea lor te vor ajuta să scrii aplicații mai robuste, mai rapide și mai ușor de întreținut. Aceste patternuri nu doar rezolvă probleme, ci te pregătesc și pentru a gândi ca un programator orientat spre fiabilitate.