Ένας διανομέας χτυπάει το κουδούνι. Δεν ακούς. Περιμένει λίγο, ξαναχτυπάει και ανοίγεις.
Αυτό είναι το Retry: επαναλαμβάνουμε μια αποτυχημένη ενέργεια όταν υπάρχει πιθανότητα η επόμενη προσπάθεια να πετύχει.
Φυσικά, ο διανομέας χρειάζεται όριο. Ο γείτονας του τρίτου δεν χρωστάει να ακούσει όλα τα retries μας.
Στο .NET, η Polly v8 μάς βοηθά να ορίσουμε πότε ξαναπροσπαθούμε, πόσο περιμένουμε και πότε σταματάμε.
Πότε το χρησιμοποιούμε
Σε πιθανώς προσωρινές αστοχίες: ένα API κάνει restart, μια σύνδεση διακόπτεται ή ο server επιστρέφει 503 Service Unavailable.
Ένα λανθασμένο request ή ένα πρόβλημα δικαιωμάτων χρειάζεται διόρθωση. Όπως και η λάθος διεύθυνση παράδοσης.
Στο παράδειγμα θα επαναλαμβάνουμε μόνο τις κλήσεις που αποτυγχάνουν με 503.
1. Δημιουργία και NuGet
Με εγκατεστημένο .NET 10 SDK:
dotnet new console -n RetryDemo -f net10.0
cd RetryDemo
dotnet add package Polly.Core --version 8.5.2
Χρησιμοποιούμε συγκεκριμένη έκδοση της σειράς v8 για το παράδειγμα. NuGet: Polly.Core
Θα έχουμε τρία αρχεία:
RetryDemo/
├── Program.cs
├── CatalogClient.cs
└── DemoCatalogHandler.cs
2. Η πολιτική — Program.cs
Αντικαθιστούμε το υπάρχον περιεχόμενο:
using System.Net;
using Polly;
using Polly.Retry;
using Polly.Timeout;
var pipeline = new ResiliencePipelineBuilder()
.AddTimeout(TimeSpan.FromSeconds(5))
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>(exception =>
exception.StatusCode == HttpStatusCode.ServiceUnavailable),
MaxRetryAttempts = 2,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = arguments =>
{
Console.WriteLine($"Retry {arguments.AttemptNumber + 1}");
return default;
}
})
.Build();
using var httpClient = new HttpClient(new DemoCatalogHandler())
{
BaseAddress = new Uri("https://catalog.example/")
};
var catalog = new CatalogClient(httpClient, pipeline);
try
{
Console.WriteLine(await catalog.GetProductAsync(CancellationToken.None));
}
catch (Exception exception)
when (exception is HttpRequestException or TimeoutRejectedException)
{
Console.WriteLine("Ο κατάλογος δεν είναι διαθέσιμος.");
}
Οι ρυθμίσεις σημαίνουν:
-
ShouldHandle: retry μόνο για503. -
MaxRetryAttempts = 2: μία αρχική προσπάθεια και μέχρι δύο επαναλήψεις. -
Delay: βασική αναμονή200 ms. -
Exponential: η αναμονή αυξάνεται. -
UseJitter: οι καθυστερήσεις μεταβάλλονται ώστε πολλοί clients να μη συγχρονίζονται.
Η Polly σταματά μόλις πετύχει η κλήση ή εξαντληθούν οι επιτρεπόμενες επαναλήψεις. Polly: Retry
Το εξωτερικό AddTimeout βάζει συνολικό όριο πέντε δευτερολέπτων, μαζί με τις αναμονές. Η ακύρωση λειτουργεί συνεργατικά μέσω του cancellation token. Polly: Timeout
3. Η HTTP κλήση — CatalogClient.cs
Δημιουργούμε το αρχείο:
using Polly;
public sealed class CatalogClient(
HttpClient httpClient,
ResiliencePipeline pipeline)
{
public async Task<string> GetProductAsync(CancellationToken cancellationToken)
{
return await pipeline.ExecuteAsync(async token =>
{
using var response = await httpClient.GetAsync("products/1", token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(token);
}, cancellationToken);
}
}
Το ExecuteAsync εκτελεί την κλήση μέσα στην pipeline.
Το EnsureSuccessStatusCode() μετατρέπει μια αποτυχημένη HTTP απάντηση σε HttpRequestException. Αν το status είναι 503, η πολιτική επιτρέπει επανάληψη.
Κάθε προσπάθεια χρησιμοποιεί το token της pipeline και αποδεσμεύει την απάντησή της με using.
4. Η προσομοίωση — DemoCatalogHandler.cs
Για εύκολη δοκιμή, ένας HTTP handler δημιουργεί τις απαντήσεις τοπικά: δύο φορές 503 και μετά 200.
using System.Net;
public sealed class DemoCatalogHandler : HttpMessageHandler
{
private int _attempts;
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var attempt = Interlocked.Increment(ref _attempts);
var status = attempt < 3
? HttpStatusCode.ServiceUnavailable
: HttpStatusCode.OK;
Console.WriteLine($"HTTP προσπάθεια {attempt}: {(int)status}");
return Task.FromResult(new HttpResponseMessage(status)
{
Content = new StringContent("Mechanical keyboard")
});
}
}
Το URL του παραδείγματος είναι ενδεικτικό. Τις απαντήσεις τις δίνει ο handler.
Για πραγματικό API, χρησιμοποιούμε new HttpClient() στη θέση του new HttpClient(new DemoCatalogHandler()) και ορίζουμε την πραγματική BaseAddress.
5. Εκτέλεση
dotnet run
Αποτέλεσμα:
HTTP προσπάθεια 1: 503
Retry 1
HTTP προσπάθεια 2: 503
Retry 2
HTTP προσπάθεια 3: 200
Mechanical keyboard
Σε κάθε νέα εκκίνηση της εφαρμογής, το σενάριο ξεκινά από την αρχή.
Και η αντικαταβολή;
Αν μια πληρωμή ολοκληρωθεί αλλά χαθεί η απάντηση, ένα timeout δεν αποδεικνύει ότι η χρέωση απέτυχε.
Γι’ αυτό, σε ενέργειες που αλλάζουν δεδομένα, χρειαζόμαστε ασφαλή επανάληψη — για παράδειγμα, idempotency key που αναγνωρίζει και διαχειρίζεται σωστά ο server.
Ο διανομέας ξαναχτυπάει. Η ίδια αντικαταβολή πρέπει να καταχωριστεί μία φορά.
Πού μπαίνει στην αρχιτεκτονική
Οι ευθύνες εδώ είναι μικρές και σαφείς: το Program συνθέτει την εφαρμογή, το CatalogClient διαβάζει δεδομένα και ο δοκιμαστικός handler προσομοιώνει τον πάροχο.
Σε μια εφαρμογή με Clean Architecture, ο HTTP client και η πολιτική resilience ανήκουν στην Infrastructure. Το Application χρησιμοποιεί ένα interface για την ανάγνωση, διατηρώντας τις τεχνικές λεπτομέρειες έξω από το use case.
Top comments (0)