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
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 eseguireFunc<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 unTimeSpan
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!