Matteo Torromacco
Matteo Torromacco

Matteo Torromacco

Come gestire gli errori temporanei con Polly

Come gestire gli errori temporanei con Polly

Sempre più applicazioni sfruttano servizi di terze parti, vediamo come utilizzare Polly per gestire eventuali interruzioni di servizio

Matteo Torromacco's photo
Matteo Torromacco
·Jul 11, 2022·

5 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

Introduzione

Servizi come database, API, server SMTP, librerie di terze parti, etc. sono inaffidabili, fuori dal nostro controllo e soggetti ad errori temporanei. E' quindi necessario gestire la possibilità di malfunzionamenti evitando che la nostra applicazione abbia comportamenti imprevisti.

Qui entra in gioco Polly, una libreria di resilienza e gestione di errori transienti, scritta utilizzando la piattaforma .NET Standard in modo da essere compatibile sia con .NET Framework che con .NET Core.

Quando si tratta di errori transienti si da per scontato che siano temporanei e che quindi dopo un certo intervallo tendano a risolversi automaticamente. Polly ci consente di eseguire nuovamente la chiamata che è andata in errore astraendo la logica di retry e permettendo allo sviluppatore di concentrarsi al 100% sul proprio codice.

Esempio di utilizzo

RetryPolicy policy = Policy.Handle<MyException>()
                           .WaitAndRetry(5, _ => TimeSpan.FromMilliseconds(100));

policy.Execute(() => ThrowAnException());

void ThrowAnException() {
  Console.WriteLine("Chiamata al metodo ThrowAnException()");
  throw new MyException();
}

Nell'esempio indichiamo che nel caso in cui l'eccezione MyException sia intercettata allora, a distanza di 100 millisecondi, verrà eseguita nuovamente la chiamata fino ad un massimo di 5 volte, dopo le quali l'eccezione verrà propagata nell'applicazione.

Eseguendo l'applicazione avremo il seguente output:

$ dotnet run
Chiamata al metodo ThrowAnException()
Chiamata al metodo ThrowAnException()
Chiamata al metodo ThrowAnException()
Chiamata al metodo ThrowAnException()
Chiamata al metodo ThrowAnException()
Chiamata al metodo ThrowAnException()
Unhandled exception. MyException: Exception of type 'MyException' was thrown.
...

Per le prima 5 chiamate, Polly gestisce l'eccezione sollevata mentre l'ultima chiamata, essendo fuori dal range di gestione, viene effettivamente sollevata.

Installazione

Polly è disponibile come pacchetto NuGet.

$ dotnet add package Polly

Generare la policy di retry

La policy di retry è la configurazione della gestione degli errori di Polly nella quale impostiamo le eccezioni da gestire, il numero di tentativi da effettuare e l'intervallo di tempo che deve intercorrere tra un tentativo e l'altro.

Attendere e ripetere

RetryPolicy policy = Policy.Handle<MyException>()
                           .WaitAndRetry(5, _ => TimeSpan.FromMilliseconds(100));

Possiamo andare a definire la logica di retry utilizzando la classe statica Policy che la libreria ci mette a disposizione, sfruttando il metodo Handle<TException>() per indicare l'eccezione da gestire ed il metodo WaitAndRetry(int, Func<int, TimeSpan>) per specificare il numero di tentativi ed il tempo d'attesa.
Quest'ultimo metodo accetta due parametri:

  • int → Il numero massimo di tentativi da eseguire
  • Func<int, TimeSpan> → L'intervallo di tempo da attendere tra una esecuzione e l'altra. In questa lambda riceviamo in ingresso il numero del tentativo e ritorniamo un TimeSpan indicante proprio l'intervallo.

Incrementare il tempo d'attesa tra un tentativo e l'altro

RetryPolicy policy = Policy.Handle<MyException>()
                           .WaitAndRetry(5, retry => TimeSpan.FromMilliseconds(100 * retry));

Grazie al valore di input ricevuto dalla lambda per l'impostazione dell'intervallo (che rappresenta il numero del tentativo) possiamo andare ad aumentare il tempo d'attesa che intercorre tra una richiesta e l'altra ad ogni nuovo tentativo.

Ripetere senza attendere

RetryPolicy policy = Policy.Handle<MyException>()
                           .Retry(5);

Abbiamo anche la possibilità di rieseguire la chiamata immediatamente in caso di errore senza dover attendere grazie al metodo Retry(int) che accetta un unico parametro, ovvero il numero di tentativi da eseguire.

Indicare più eccezioni da gestire

RetryPolicy policy = Policy.Handle<MyFirstException>()
                           .Or<MySecondException>()
                           .Or<MyThirdException>()
                           .WaitAndRetry(5, _ => TimeSpan.FromMilliseconds(100));

Grazie al metodo Or<TException>() possiamo indicare più eccezioni da gestire nella stessa policy.

Gestire il risultato

RetryPolicy<string> policy = Policy.HandleResult<string>(result => String.IsNullOrEmpty(result))
                                   .WaitAndRetry(5, _ => TimeSpan.FromMilliseconds(100));

Invece di gestire un'eccezione possiamo andare a gestire il risultato dell'espressione e ripetere la logica nel caso in cui questo non ci soddisfi.
Utilizziamo il metodo HandleResult<TResult>(Func<TResult, bool>) per indicare il tipo che ritornerà l'espressione ed una lambda per impostare una condizione di errore che se verificata consenta un nuovo tentativo.
Nell'esempio indichiamo a Polly che l'espressione che andremo a verificare ritornerà una stringa e nel caso in cui quest'ultima sia null o vuota allora sarà necessario andare a rieseguire la chiamata.

Indicare più risultati da gestire

RetryPolicy<string> policy = Policy.HandleResult<string>(result => String.IsNullOrEmpty(result))
                                   .OrResult(result => result.ToUpper() == "ERR")
                                   .WaitAndRetry(5, _ => TimeSpan.FromMilliseconds(100));

Lo stesso concetto visto per le eccezioni vale, allo stesso modo, anche per i risultati con l'unica differenza che invece di utilizzare il metodo Or<TException>() utilizziamo OrResult(Func<TResult, bool>) .
E' importante notare che il metodo OrResult(Func<TResult, bool>) in questo caso non accetta un generico che rappresenta il tipo di ritorno dell'espressione dato che questo è già stato specificato nel metodo precedente.

Gestire eccezioni e risultati insieme

RetryPolicy<string> policy = Policy.Handle<MyFirstException>()
                                   .Or<MySecondException>()
                                   .Or<MyThirdException>()
                                   .OrResult<string>(result => String.IsNullOrEmpty(result))
                                   .OrResult(result => result.ToUpper() == "ERR")
                                   .WaitAndRetry(5, _ => TimeSpan.FromMilliseconds(100));

Ovviamente possiamo andare ad unire la gestione delle eccezioni con quella dei risultati per ottenere una policy decisamente più potente.
Rispetto al codice visto precedentemente vi è un'unica differenza, ovvero che la prima chiamata al metodo OrResult<TResult>(Func<TResult, bool>) questa volta accetta un generico per poter definire il tipo di ritorno che ci si aspetta.

Policy di retry asincrona

AsyncRetryPolicy policy = Policy.Handle<MyException>()
                                .WaitAndRetryAsync(5, _ => TimeSpan.FromMilliseconds(100));

Nel caso in cui la logica da controllare sia asincrona allora dobbiamo sviluppare la policy come tale e possiamo farlo semplicemente utilizzando la versione asincrona dei metodi di retry come WaitAndRetryAsync(int, Func<int, TimeSpan>) oppure RetryAsync(int).

Esecuzione

policy.Execute(() => MyMethod());

Una volta creata la policy di retry il lavoro è completo e non ci resta che sfruttarla utilizzando su di essa il metodo Execute(Action).

await policy.ExecuteAsync(() => MyAsyncMethod());

La controparte asincrona del metodo Execute(Action) è ovviamente ExecuteAsync(Func<Task>) ed è utilizzabile nel caso in cui la nostra policy di retry sia stata configurata per essere tale.

Conclusione

Ovviamente queste non sono tutte le funzionalità di Polly ma quelle più utilizzate, per avere una conoscenza di tutte le altre possibilità vi rimando al repository ufficiale. Non sorprende che una libreria così potente e semplice abbia avuto una crescita esponenziale fino ad entrare a far parte della .NET Foundation.

That's it, happy coding!

 
Share this