DEV Community

nikosst
nikosst

Posted on Edited on

Retry με Polly v8 στο .NET: πόσες φορές χτυπάμε το κουδούνι;

Ένας διανομέας χτυπάει το κουδούνι. Δεν ακούς. Περιμένει λίγο, ξαναχτυπάει και ανοίγεις.

Αυτό είναι το 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
Enter fullscreen mode Exit fullscreen mode

Χρησιμοποιούμε συγκεκριμένη έκδοση της σειράς v8 για το παράδειγμα. NuGet: Polly.Core

Θα έχουμε τρία αρχεία:

RetryDemo/
├── Program.cs
├── CatalogClient.cs
└── DemoCatalogHandler.cs
Enter fullscreen mode Exit fullscreen mode

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("Ο κατάλογος δεν είναι διαθέσιμος.");
}
Enter fullscreen mode Exit fullscreen mode

Οι ρυθμίσεις σημαίνουν:

  • 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Το 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")
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Το URL του παραδείγματος είναι ενδεικτικό. Τις απαντήσεις τις δίνει ο handler.

Για πραγματικό API, χρησιμοποιούμε new HttpClient() στη θέση του new HttpClient(new DemoCatalogHandler()) και ορίζουμε την πραγματική BaseAddress.

5. Εκτέλεση

dotnet run
Enter fullscreen mode Exit fullscreen mode

Αποτέλεσμα:

HTTP προσπάθεια 1: 503
Retry 1
HTTP προσπάθεια 2: 503
Retry 2
HTTP προσπάθεια 3: 200
Mechanical keyboard
Enter fullscreen mode Exit fullscreen mode

Σε κάθε νέα εκκίνηση της εφαρμογής, το σενάριο ξεκινά από την αρχή.

Και η αντικαταβολή;

Αν μια πληρωμή ολοκληρωθεί αλλά χαθεί η απάντηση, ένα timeout δεν αποδεικνύει ότι η χρέωση απέτυχε.

Γι’ αυτό, σε ενέργειες που αλλάζουν δεδομένα, χρειαζόμαστε ασφαλή επανάληψη — για παράδειγμα, idempotency key που αναγνωρίζει και διαχειρίζεται σωστά ο server.

Ο διανομέας ξαναχτυπάει. Η ίδια αντικαταβολή πρέπει να καταχωριστεί μία φορά.

Πού μπαίνει στην αρχιτεκτονική

Οι ευθύνες εδώ είναι μικρές και σαφείς: το Program συνθέτει την εφαρμογή, το CatalogClient διαβάζει δεδομένα και ο δοκιμαστικός handler προσομοιώνει τον πάροχο.

Σε μια εφαρμογή με Clean Architecture, ο HTTP client και η πολιτική resilience ανήκουν στην Infrastructure. Το Application χρησιμοποιεί ένα interface για την ανάγνωση, διατηρώντας τις τεχνικές λεπτομέρειες έξω από το use case.


nikosstit@gmail.com

Top comments (0)