Μαθαίνουμε Retry με μια παράδοση αντικαταβολής και υλοποιούμε Polly v8 βήμα βήμα, με NuGet, backoff, jitter, timeouts και Clean Architecture.

Ένας διανομέας φτάνει στην πόρτα σου με ένα δέμα και αντικαταβολή.

Χτυπάει το κουδούνι.

Καμία απάντηση.

Είσαι μέσα, αλλά φοράς ακουστικά. Ο διανομέας περιμένει λίγο και ξαναχτυπάει. Αυτή τη φορά τον ακούς, ανοίγεις και παραλαμβάνεις.

Η δεύτερη προσπάθεια έλυσε το πρόβλημα.

Αυτό είναι το Retry: επαναλαμβάνουμε μια ενέργεια που απέτυχε, όταν υπάρχει πιθανότητα η επόμενη προσπάθεια να πετύχει.

Τώρα φαντάσου τον ίδιο διανομέα με το δάχτυλο κολλημένο στο κουδούνι:

«Ντριν. Ντριν. Ντριν. Ντριν.»

Ο γείτονας του τρίτου δεν χρωστάει να ακούσει όλα τα retries μας.

Στο λογισμικό, κάθε επιπλέον προσπάθεια καταναλώνει χρόνο και πόρους. Γι’ αυτό χρειαζόμαστε κανόνες:

  • Πότε αξίζει να ξαναπροσπαθήσουμε;
  • Πόσο περιμένουμε;
  • Πόσες φορές επαναλαμβάνουμε;
  • Πότε σταματάμε;
  • Μπορεί η επανάληψη να προκαλέσει διπλή ενέργεια;

Θα ξεκινήσουμε από αυτές τις αποφάσεις και θα προχωρήσουμε σε ένα πλήρες HTTP παράδειγμα με Polly v8 στο .NET, από την εγκατάσταση του NuGet μέχρι την εκτέλεση.

1. Τι ανάγκη καλύπτει το Retry

Μια εφαρμογή μπορεί να είναι σωστά γραμμένη και μια εξωτερική κλήση της να αποτύχει.

Ένα API κάνει restart. Μια σύνδεση διακόπτεται προσωρινά. Ένας server δέχεται περισσότερα requests από όσα μπορεί να εξυπηρετήσει.

Αυτές οι προσωρινές αστοχίες ονομάζονται transient failures.

Αν επαναλάβουμε την ενέργεια μετά από κατάλληλη αναμονή, μπορεί να πετύχει.

Έστω ότι διαβάζουμε ένα προϊόν από εξωτερικό Catalog API:

Το API μας → Catalog API → Πληροφορίες προϊόντος 

Χωρίς retry:

1η προσπάθεια → 503 → αποτυχία προς τον χρήστη 

Με περιορισμένο retry:

1η προσπάθεια → 503 Αναμονή 2η προσπάθεια → 503 Αναμονή 3η προσπάθεια → 200 Επιστροφή προϊόντος 

Κερδίζουμε μια ευκαιρία επιτυχίας, καταναλώνοντας επιπλέον χρόνο και requests.

Το κατάλληλο όριο εξαρτάται από τη λειτουργία. Μια αναζήτηση που περιμένει ο χρήστης και μια νυχτερινή εισαγωγή δεδομένων έχουν διαφορετικές χρονικές απαιτήσεις. Microsoft: Retry pattern

2. Πότε ξαναχτυπάμε και πότε διορθώνουμε τη διεύθυνση

Αν ο παραλήπτης δεν άκουσε το πρώτο κουδούνι, μια επόμενη προσπάθεια μπορεί να βοηθήσει.

Αν έχουμε λάθος διεύθυνση, χρειάζεται διόρθωση της διεύθυνσης.

Το ίδιο σκεπτικό εφαρμόζουμε στα APIs:

Περίπτωση Συνήθης αντιμετώπιση
Προσωρινή διακοπή σύνδεσης Περιορισμένο retry, εφόσον η πράξη επαναλαμβάνεται με ασφάλεια
502, 503, 504 Υποψήφιες προσωρινές αστοχίες, σύμφωνα με το συμβόλαιο του API
429 Too Many Requests Αναμονή σύμφωνα με το Retry-After, όταν παρέχεται
408 Request Timeout Πιθανό retry, με έλεγχο ασφάλειας της πράξης
400 Bad Request Διόρθωση του request
401 Unauthorized Αντιμετώπιση authentication
403 Forbidden Αντιμετώπιση authorization
404 Not Found Συνήθως επιστροφή «δεν βρέθηκε»
Business validation failure Διόρθωση δεδομένων ή επιχειρησιακή απόφαση

Η HTTP resilience υποδομή της Microsoft αναγνωρίζει συγκεκριμένα status codes και exceptions ως υποψήφιες προσωρινές αστοχίες. Η επιλογή χρειάζεται προσαρμογή στον εκάστοτε πάροχο. Microsoft: HTTP resilience

Για παράδειγμα, ένα 500 Internal Server Error μπορεί να προέρχεται από προσωρινή βλάβη ή από σταθερό bug.

Αν ο server αποτυγχάνει πάντα με το ίδιο input λόγω ενός bug, η επανάληψη του ίδιου request προσθέτει φόρτο χωρίς να αλλάζει το αποτέλεσμα.

Αντίστοιχα, ένα συγκεκριμένο API μπορεί να τεκμηριώνει ότι ένα νέο resource εμφανίζεται με καθυστέρηση και αρχικά επιστρέφει 404. Εκεί μπορούμε να σχεδιάσουμε ειδική πολιτική.

Η απόφαση βασίζεται στο πραγματικό συμβόλαιο της εξάρτησης.

3. Το timeout δεν αποδεικνύει ότι το σπίτι είναι άδειο

Μετά από μερικά χτυπήματα, ο διανομέας φεύγει και καταγράφει:

Η παράδοση δεν ολοκληρώθηκε: δεν υπήρξε απάντηση.

Μπορεί να έλειπες. Μπορεί να μην άκουσες. Μπορεί να είχε χαλάσει το κουδούνι.

Ο διανομέας γνωρίζει το αποτέλεσμα της προσπάθειας, όχι οπωσδήποτε την αιτία.

Το ίδιο ισχύει σε μια δικτυακή κλήση:

Ένα timeout σημαίνει ότι δεν λάβαμε έγκαιρα αποτέλεσμα. Η ενέργεια στον απομακρυσμένο server μπορεί να έχει ολοκληρωθεί.

Αυτή η διάκριση γίνεται κρίσιμη όταν η ενέργεια αλλάζει δεδομένα.

4. Η αντικαταβολή μάς φέρνει στο idempotency

Έστω ότι παραλαμβάνεις το δέμα και πληρώνεις.

Ο διανομέας καταχωρίζει την ολοκλήρωση στη συσκευή του, αλλά η επιβεβαίωση από το σύστημα χάνεται.

Η συσκευή εμφανίζει timeout.

Το δέμα παραδόθηκε. Η επιβεβαίωση δεν έφτασε.

Αν το σύστημα επαναλάβει την καταχώριση, χρειάζεται να αναγνωρίζει την ίδια παράδοση, ώστε να αποφύγει διπλή καταγραφή ή χρέωση.

Εδώ μας ενδιαφέρει το idempotency.

Μια ενέργεια είναι idempotent όταν η επανάληψή της έχει το ίδιο επιδιωκόμενο αποτέλεσμα στον server με μία εκτέλεση.

Το HTTP ορίζει ως idempotent τις safe μεθόδους, όπως το GET, καθώς και τις PUT και DELETE. Οι απαντήσεις μπορούν να διαφέρουν μεταξύ προσπαθειών. RFC 9110: Idempotent Methods

Για commands όπως «δημιούργησε πληρωμή», μια συνηθισμένη σχεδιαστική λύση είναι ένα idempotency key:

http POST /payments Idempotency-Key: payment-order-12345

Για να λειτουργήσει σωστά:

  • Το ίδιο λογικό command διατηρεί το ίδιο key σε όλες τις προσπάθειες.
  • Ο server συνδέει με ασφαλή τρόπο το key με την επεξεργασία και το αποτέλεσμα.
  • Ταυτόχρονα requests με το ίδιο key δεν δημιουργούν διπλές πράξεις.
  • Το συμβόλαιο ορίζει τι συμβαίνει αν το ίδιο key χρησιμοποιηθεί με διαφορετικό payload.

Η ταυτότητα από μόνη της δεν αρκεί. Ο server πρέπει να διαχειρίζεται σωστά τις επαναλαμβανόμενες αιτήσεις.

Ίδια παράδοση, ίδια ταυτότητα, μία ολοκλήρωση.

Στο παράδειγμα του άρθρου θα χρησιμοποιήσουμε ανάγνωση προϊόντος με GET, ώστε η μονάδα επανάληψης να είναι σαφής.

5. Από το κουδούνι στις ρυθμίσεις

Έννοια Στην πόρτα Στον κώδικα
Αρχική προσπάθεια Πρώτο χτύπημα Αρχικό HTTP request
Retry Επόμενο χτύπημα Επανάληψη του request
Delay Αναμονή πριν ξαναχτυπήσει Καθυστέρηση πριν το retry
Backoff Μεγαλύτερη αναμονή μεταξύ προσπαθειών Σταδιακή αύξηση καθυστέρησης
Attempt timeout Όριο αναμονής για το συγκεκριμένο χτύπημα Χρονικό όριο μίας προσπάθειας
Συνολικό timeout Συνολικός διαθέσιμος χρόνος επίσκεψης Όριο για προσπάθειες και αναμονές
Τελική αποτυχία Καταγραφή ότι η παράδοση δεν ολοκληρώθηκε Επιστροφή αποτυχίας στον caller

Στην Polly v8 οργανώνουμε αυτή τη συμπεριφορά μέσα σε ένα ResiliencePipeline, συνδυάζοντας strategies όπως Retry και Timeout.

Το σύγχρονο API χρησιμοποιεί builders και options. Παραδείγματα με Policy.Handle(...).WaitAndRetryAsync(...) ανήκουν στο API της v7. Polly: Migration guide

Για το Retry, οι βασικές επιλογές είναι:

Option Τι αποφασίζει
ShouldHandle Ποια αποτελέσματα ή exceptions δικαιολογούν επανάληψη
MaxRetryAttempts Πόσες επιπλέον προσπάθειες επιτρέπονται
Delay Ποια είναι η βασική καθυστέρηση
BackoffType Πώς εξελίσσονται οι καθυστερήσεις
UseJitter Αν μεταβάλλονται οι καθυστερήσεις ώστε να αποφεύγεται ο συγχρονισμός clients
OnRetry Τι εκτελείται όταν προγραμματίζεται επανάληψη

Προσοχή στην αρίθμηση:

MaxRetryAttempts = 3; 

σημαίνει:

1 αρχική προσπάθεια + έως 3 retries = έως 4 εκτελέσεις 

Η διαδικασία μπορεί να σταματήσει νωρίτερα λόγω επιτυχίας, μη επιλέξιμης αποτυχίας, cancellation ή εξωτερικού timeout. Polly: Retry strategy

Backoff και jitter

Με exponential backoff, βάση 200 ms και χωρίς jitter ή άλλη προσαρμογή, οι καθυστερήσεις είναι:

Πριν το 1ο retry: 200 ms Πριν το 2ο retry: 400 ms Πριν το 3ο retry: 800 ms 

Το jitter μεταβάλλει τις αναμονές, ώστε πολλοί clients που απέτυχαν μαζί να μην ξαναστείλουν όλοι μαζί.

Με ενεργό jitter, οι πραγματικές τιμές διαφέρουν από την παραπάνω ακολουθία. Μια επόμενη καθυστέρηση μπορεί να είναι μικρότερη από την προηγούμενη. Polly: Exponential backoff

Το Retry-After

Ένας server μπορεί να επιστρέψει:

HTTP/1.1 429 Too Many Requests Retry-After: 15 

Το header ζητά αναμονή πριν την επόμενη προσπάθεια.

Το HttpRetryStrategyOptions υποστηρίζει την αξιοποίησή του μέσω του ShouldRetryAfterHeader. Όταν υπάρχει έγκυρη τιμή, χρησιμοποιείται για την καθυστέρηση. Microsoft: HttpRetryStrategyOptions

Αν η λειτουργία διαθέτει μικρότερο χρονικό budget, πρέπει να σταματήσει όταν αυτό εξαντληθεί.

6. Η αρχιτεκτονική του παραδείγματος

Θα δημιουργήσουμε:

GET /products/{id} 

Το endpoint θα ζητά ένα προϊόν από εξωτερικό Catalog API.

Χωρίζουμε τις ευθύνες ως εξής:

Μέρος Ευθύνη
Application Ορίζει τη λειτουργία και το interface πρόσβασης στον κατάλογο
Infrastructure Υλοποιεί την HTTP επικοινωνία και το resilience
API Δέχεται το request και μεταφράζει το αποτέλεσμα σε HTTP response

Ένα interface εκφράζει το συμβόλαιο μιας λειτουργίας. Στην περίπτωσή μας, το Application θα ζητά «διάβασε αυτό το προϊόν», χωρίς να γνωρίζει πώς πραγματοποιείται η επικοινωνία.

Με το dependency injection συνδέουμε το interface με τη συγκεκριμένη υλοποίησή του κατά το startup.

Οι εξαρτήσεις στον κώδικα είναι:

API ───────────────────────► Application Infrastructure ────────────► Application Το API συνδέει τις υλοποιήσεις της Infrastructure στο startup. 

Η ροή εκτέλεσης είναι:

Endpoint ↓ GetProductQuery ↓ ICatalogGateway ↓ HttpCatalogGateway ↓ HTTP resilience pipeline ↓ Catalog API 

Η κατεύθυνση των εξαρτήσεων προς τον πυρήνα αποτελεί βασικό στοιχείο της Clean Architecture.

Για το εκπαιδευτικό demo θα χρησιμοποιήσουμε φακέλους σε ένα project. Σε μεγαλύτερη λύση, ξεχωριστά projects και κατάλληλα project references μπορούν να επιβάλουν αυτά τα όρια.

Ένα Domain layer αποκτά ουσία όταν υπάρχουν επιχειρησιακοί κανόνες και invariants. Η συγκεκριμένη απλή ανάγνωση χρειάζεται μόνο ένα Application contract.

7. Εγκατάσταση βήμα βήμα

Οι εντολές που ακολουθούν μπορούν να εκτελεστούν σε PowerShell.

Βήμα 1: έλεγχος του SDK

dotnet --list-sdks 

Για το παράδειγμα χρειαζόμαστε εγκατεστημένο .NET 10 SDK.

Βήμα 2: δημιουργία φακέλου και project

mkdir PollyRetryTutorial cd PollyRetryTutorial dotnet new web -n RetryV8Demo -f net10.0 cd RetryV8Demo 

Βήμα 3: εγκατάσταση του NuGet

Μέσα στον φάκελο που περιέχει το RetryV8Demo.csproj, εκτελούμε:

dotnet add package Microsoft.Extensions.Http.Resilience --version 10.0.0 

Η εντολή προσθέτει το package και τις εξαρτήσεις του στο project.

Το package παρέχει HTTP resilience integration πάνω στην Polly. Η συγκεκριμένη έκδοση χρησιμοποιείται για σταθερό σημείο αναφοράς στο παράδειγμα. NuGet: Microsoft.Extensions.Http.Resilience 10.0.0

Στο .csproj θα υπάρχει πλέον η αναφορά:

<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" /> 

Βήμα 4: δημιουργία φακέλων

mkdir Application mkdir Infrastructure 

Η τελική δομή θα είναι:

PollyRetryTutorial/ ├── RetryV8Demo/ │ ├── Application/ │ │ ├── ProductDetails.cs │ │ ├── ICatalogGateway.cs │ │ ├── CatalogUnavailableException.cs │ │ └── GetProductQuery.cs │ ├── Infrastructure/ │ │ ├── HttpCatalogGateway.cs │ │ └── CatalogRegistration.cs │ ├── Program.cs │ ├── appsettings.json │ └── RetryV8Demo.csproj └── FakeCatalog/ ├── Program.cs └── FakeCatalog.csproj 

Το FakeCatalog θα το δημιουργήσουμε στο τέλος, για να παρατηρήσουμε ελεγχόμενα το σενάριο 503 → 503 → 200.

8. Τα αρχεία του Application, ένα ένα

Αρχείο 1: Application/ProductDetails.cs

namespace RetryV8Demo.Application; public sealed record ProductDetails( Guid Id, string Name, decimal Price, string Currency); 

Αυτό είναι το αποτέλεσμα που χρειάζεται η εφαρμογή μας: τα στοιχεία του προϊόντος.

Το record μάς επιτρέπει να εκφράσουμε συνοπτικά ένα αντικείμενο δεδομένων.

Αρχείο 2: Application/ICatalogGateway.cs

namespace RetryV8Demo.Application; public interface ICatalogGateway { // null: το προϊόν δεν υπάρχει. // CatalogUnavailableException: η ανάγνωση απέτυχε τεχνικά. // Η ακύρωση του caller διαδίδεται κανονικά. Task<ProductDetails?> GetByIdAsync( Guid id, CancellationToken cancellationToken); } 

Το συμβόλαιο διαχωρίζει τέσσερις περιπτώσεις:

Υπάρχει προϊόν → ProductDetails Δεν υπάρχει προϊόν → null Απέτυχε η επικοινωνία → CatalogUnavailableException Ακύρωσε ο caller → cancellation 

Αυτή η διάκριση αποτρέπει ένα συχνό λάθος: να εμφανίζεται μια βλάβη ως «το προϊόν δεν υπάρχει».

Αρχείο 3: Application/CatalogUnavailableException.cs

namespace RetryV8Demo.Application; public sealed class CatalogUnavailableException( string message, Exception innerException) : Exception(message, innerException) { } 

Το Application διαθέτει δικό του exception για την τεχνική αποτυχία ανάγνωσης.

Έτσι, οι καταναλωτές του δεν χρειάζεται να γνωρίζουν exceptions της Polly ή λεπτομέρειες του HTTP transport.

Το innerException διατηρεί την αρχική αιτία για τη διερεύνηση του προβλήματος.

Αρχείο 4: Application/GetProductQuery.cs

namespace RetryV8Demo.Application; public sealed class GetProductQuery(ICatalogGateway catalog) { public Task<ProductDetails?> ExecuteAsync( Guid id, CancellationToken cancellationToken) { return catalog.GetByIdAsync(id, cancellationToken); } } 

Το use case εδώ εκτελεί μια απλή ανάγνωση.

Το ICatalogGateway παρέχεται μέσω constructor injection. Το dependency injection container θα δημιουργήσει το query με την κατάλληλη υλοποίηση.

9. Η HTTP υλοποίηση

Αρχείο 5: Infrastructure/HttpCatalogGateway.cs

using System.Net; using System.Net.Http.Json; using System.Text.Json; using Polly.Timeout; using RetryV8Demo.Application; namespace RetryV8Demo.Infrastructure; public sealed class HttpCatalogGateway(HttpClient httpClient) : ICatalogGateway { public async Task<ProductDetails?> GetByIdAsync( Guid id, CancellationToken cancellationToken) { using var deadline = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken); deadline.CancelAfter(TimeSpan.FromSeconds(10)); try { using var response = await httpClient.GetAsync( $"products/{id}", HttpCompletionOption.ResponseHeadersRead, deadline.Token); if (response.StatusCode == HttpStatusCode.NotFound) { return null; } response.EnsureSuccessStatusCode(); var product = await response.Content.ReadFromJsonAsync<CatalogResponse>( cancellationToken: deadline.Token) ?? throw new JsonException( "The catalog returned a null product."); return new ProductDetails( product.Id, product.Name, product.Price, product.Currency); } catch (OperationCanceledException exception) when (!cancellationToken.IsCancellationRequested) { throw new CatalogUnavailableException( "The catalog operation exceeded its time budget.", exception); } catch (Exception exception) when (exception is HttpRequestException or TimeoutRejectedException or JsonException) { throw new CatalogUnavailableException( "The catalog could not return a valid product.", exception); } } private sealed record CatalogResponse( Guid Id, string Name, decimal Price, string Currency); } 

Ο adapter αναλαμβάνει να:

  1. Στείλει το HTTP request.
  2. Μεταφράσει το 404 σε απουσία προϊόντος.
  3. Διαβάσει το JSON.
  4. Μετατρέψει το εξωτερικό αποτέλεσμα σε ProductDetails.
  5. Μεταφράσει γνωστές τεχνικές αποτυχίες στο συμβόλαιο του Application.

Το CatalogResponse αναπαριστά το εξωτερικό JSON συμβόλαιο και παραμένει μέσα στην Infrastructure.

Το using φροντίζει για την αποδέσμευση της τελικής HTTP απάντησης.

Το συνδεδεμένο cancellation token επιτρέπει δύο τρόπους διακοπής:

  • Να ακυρώσει ο caller.
  • Να εξαντληθεί το deadline του gateway.

Αν ακυρώσει ο caller, το cancellation διαδίδεται. Αν εξαντληθεί το δικό μας deadline, επιστρέφουμε την προβλεπόμενη τεχνική αποτυχία.

Το exception filter περιορίζει τη μετατροπή σε συγκεκριμένες κατηγορίες σφαλμάτων. Ένα άσχετο programming error παραμένει ορατό ως τέτοιο.

10. Η ρύθμιση του Retry

Αρχείο 6: Infrastructure/CatalogRegistration.cs

using System.Net; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http.Resilience; using Microsoft.Extensions.Logging; using Polly; using Polly.Timeout; using RetryV8Demo.Application; namespace RetryV8Demo.Infrastructure; public static class CatalogRegistration { public static IServiceCollection AddCatalogInfrastructure( this IServiceCollection services, Uri baseAddress) { services .AddHttpClient<ICatalogGateway, HttpCatalogGateway>(client => { client.BaseAddress = baseAddress; // Το gateway και η pipeline ορίζουν τα χρονικά όρια. client.Timeout = Timeout.InfiniteTimeSpan; }) .AddResilienceHandler( "catalog-read", (pipeline, context) => { var logger = context.ServiceProvider .GetRequiredService<ILogger<HttpCatalogGateway>>(); var retry = new HttpRetryStrategyOptions { MaxRetryAttempts = 3, Delay = TimeSpan.FromMilliseconds(200), MaxDelay = TimeSpan.FromSeconds(2), BackoffType = DelayBackoffType.Exponential, UseJitter = true, ShouldRetryAfterHeader = true, ShouldHandle = new PredicateBuilder<HttpResponseMessage>() .Handle<HttpRequestException>(exception => exception.HttpRequestError is HttpRequestError.ConnectionError or HttpRequestError.NameResolutionError or HttpRequestError.ResponseEnded) .Handle<TimeoutRejectedException>() .HandleResult(response => response.StatusCode is HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests or HttpStatusCode.BadGateway or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout), OnRetry = arguments => { logger.LogInformation( "Catalog retry {RetryNumber}; " + "delay {DelayMs} ms; status {StatusCode}", arguments.AttemptNumber + 1, arguments.RetryDelay.TotalMilliseconds, arguments.Outcome.Result is { } response ? (int?)response.StatusCode : null); return default; } }; pipeline .AddTimeout(TimeSpan.FromSeconds(8)) .AddRetry(retry) .AddTimeout(TimeSpan.FromSeconds(2)); }); return services; } } 

Το AddHttpClient συνδέει το ICatalogGateway με το HttpCatalogGateway και παρέχει τον κατάλληλα ρυθμισμένο HttpClient.

Το AddResilienceHandler προσθέτει την Polly pipeline σε αυτόν τον client. Η ρύθμιση παραμένει συγκεντρωμένη, έξω από τον κώδικα της κάθε ανάγνωσης. Microsoft: AddResilienceHandler

Οι αποφάσεις της συγκεκριμένης πολιτικής είναι:

  • Έως τρία retries.
  • Βασική καθυστέρηση 200 ms.
  • Exponential backoff και jitter.
  • Αξιοποίηση του Retry-After.
  • Επανάληψη για συγκεκριμένα status codes και κατηγορίες transport errors.
  • Επανάληψη όταν λήξει το εσωτερικό timeout της Polly.

Οι αποτυχίες αυτές είναι υποψήφιες προσωρινές αστοχίες. Για παράδειγμα, μια αποτυχία DNS μπορεί τελικά να οφείλεται σε σταθερό λάθος configuration. Γι’ αυτό διατηρούμε περιορισμένο πλήθος προσπαθειών και χρονικό budget.

Το 500 απουσιάζει σκόπιμα. Μπορούμε να το προσθέσουμε όταν γνωρίζουμε ότι έχει νόημα για τον συγκεκριμένο πάροχο.

Ο client προορίζεται για αυτή την ανάγνωση με GET. Αν προστεθούν commands, χρειάζονται δική τους αξιολόγηση ως προς την ασφάλεια επανάληψης.

11. Γιατί έχει σημασία η σειρά των timeouts

Η κατασκευή:

pipeline .AddTimeout(TimeSpan.FromSeconds(8)) .AddRetry(retry) .AddTimeout(TimeSpan.FromSeconds(2)); 

αντιστοιχεί σε:

Συνολικό timeout handler: 8s └── Retry: έως 3 επαναλήψεις └── Timeout κάθε προσπάθειας: 2s └── Αποστολή HTTP request 

Στην ιστορία μας:

  • Ο διανομέας περιμένει μέχρι ένα όριο μετά από κάθε χτύπημα.
  • Έχει και συνολικό διαθέσιμο χρόνο για την επίσκεψη.
  • Οι αναμονές ανάμεσα στα χτυπήματα καταναλώνουν αυτόν τον συνολικό χρόνο.

Το εσωτερικό timeout μπορεί να προκαλέσει TimeoutRejectedException, το οποίο χειρίζεται το Retry.

Το εξωτερικό timeout περικλείει τις προσπάθειες και τις αναμονές. Όταν εξαντληθεί, σταματά ολόκληρη τη διαδικασία. Polly: συνδυασμός Timeout, Retry και Timeout

Η ακύρωση είναι συνεργατική: ο εκτελούμενος κώδικας πρέπει να χρησιμοποιεί και να σέβεται τα cancellation tokens. Polly: Timeout strategy

Στο gateway χρησιμοποιήσαμε:

HttpCompletionOption.ResponseHeadersRead 

Με αυτή την επιλογή, το GetAsync ολοκληρώνεται όταν ληφθούν τα headers, πριν διαβαστεί ολόκληρο το body. Γι’ αυτό μεταφέρουμε το deadline και στην ανάγνωση JSON. Microsoft: HttpCompletionOption

Τα όρια του παραδείγματος είναι:

Όριο Τι καλύπτει
2s Μία προσπάθεια μέσα στον HTTP handler
8s Όλες τις προσπάθειες και αναμονές του handler
10s Τη λειτουργία του gateway, μαζί με την ανάγνωση body

Αν ο caller ακυρώσει νωρίτερα, το συνδεδεμένο token μεταφέρει την ακύρωση.

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

Μια ακόμη λεπτομέρεια: το MaxDelay περιορίζει τις καθυστερήσεις που υπολογίζει η retry strategy. Δεν περιορίζει τιμές που επιστρέφει το DelayGenerator, όπως αυτές που προκύπτουν από το Retry-After. Το συνολικό timeout εξακολουθεί να ισχύει. Polly: RetryStrategyOptions

12. Η σύνδεση στο API

Αρχείο 7: Program.cs

Αντικαθιστούμε το περιεχόμενο του Program.cs στο RetryV8Demo:

using RetryV8Demo.Application; using RetryV8Demo.Infrastructure; var builder = WebApplication.CreateBuilder(args); var catalogBaseUrl = builder.Configuration["Catalog:BaseUrl"] ?? throw new InvalidOperationException( "Catalog:BaseUrl is required."); builder.Services.AddCatalogInfrastructure( new Uri(catalogBaseUrl, UriKind.Absolute)); builder.Services.AddScoped<GetProductQuery>(); var app = builder.Build(); app.MapGet( "/products/{id:guid}", async ( Guid id, GetProductQuery query, ILogger<GetProductQuery> logger, CancellationToken cancellationToken) => { try { var product = await query.ExecuteAsync( id, cancellationToken); return product is null ? Results.NotFound() : Results.Ok(product); } catch (CatalogUnavailableException exception) { logger.LogError( exception, "Product retrieval failed for {ProductId}", id); return Results.Problem( title: "Ο κατάλογος δεν είναι διαθέσιμος.", statusCode: StatusCodes.Status503ServiceUnavailable); } }); app.Run(); 

Η γραμμή που συνδέει την Infrastructure με την εφαρμογή είναι:

Related posts