
Durante il consueto refactoring su un progetto di un nostro cliente, abbiamo convertito il precedente codice sincrono in una versione asincrona, sfruttando i Task e il pattern async-await del framework .NET. Sull’argomento vi consiglio di leggere l’articolo del mio collega Francesco Vastarella.
Uno dei motivi della richiesta era relativo ad una non corretta risposta dell’applicazione alle richieste di shutdown, con conseguente fallimento dei primi tentativi di deploy di nuove versioni.
Nel cercare i motivi di questo lento spegnimento, ci siamo accorti che alcune operazioni rimanevano in “attesa” proprio dopo la richiesta di spegnimento. Abbiamo pensato di sfruttare i CancellationToken del .NET Framework per gestire le richieste di stop dei Job.
Il primo passo è stato l’aggiornamento della versione della libreria Quartz. Per chi non la conoscesse, Quartz è una libreria che permette di gestire job schedulati, eseguiti in background. Problema superato se utilizzate nelle vostre applicazioni .NET Core, grazie agli Hosted Services.
Nella sua versione 3.0, Quartz ha introdotto la possibilità di gestire i job in modo asincrono. Se non conoscete nel dettaglio la libreria, un Job non è altro che una classe che implementa l’interfaccia IJob:
namespace Quartz
{
public interface IJob
{
Task Execute(JobExecutionContext context);
}
}
Se in passato avete utilizzato la versione 2.0 di Quartz, ricorderete sicuramente che per poter interrompere un Job, lo stesso doveva implementare l’interfaccia IInterruptableJob:
public interface IInterruptableJob
{
void Interrupt();
}
L’implementazione dell’interruzione di un Job era quindi libera e in alcuni esempi mi è capitato di vedere utilizzato un bel Thread.Abort(). Si tratta di un Approccio assolutamente da evitare, in quanto si rischia di lasciare l’applicazione in uno stato non gestito. Questo metodo non fa altro che sollevare un’eccezione di tipo ThreadAbortException (che non può essere soppressa con un catch perché è automaticamente rilanciata) che stoppa immediatamente il thread in corso.
Ciò potrebbe innescare una serie di problemi, come ad esempio avere qualche oggetto IDisposable su cui non si è riusciti a chiamare Dispose perché presente in un blocco finally che non è stato raggiunto.
Con la nuova versione di Quartz questa interfaccia è sparita. Adesso, l’interruzione di un Job viene richiesta tramite lo Scheduler, l’API principale della libreria che gestisce l’esecuzione e la schedulazione dei Job:
var scheduler = await factory.GetScheduler();
await scheduler.Start();
IJobDetail job = JobBuilder.Create<hellojob>()
.WithIdentity("myJob", "group1")
.Build();
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("myTrigger", "group1")
.StartNow()
.WithSimpleSchedule(x =>
x .WithRepeatCount(1).WithIntervalInSeconds(40)).Build();
await scheduler.ScheduleJob(job, trigger);
// Configure the cancellation of the schedule job with jobkey
await Task.Delay(TimeSpan.FromSeconds(1));
await scheduler.Interrupt(job.Key);
</hellojob>
Chi ha già lavorato con la programmazione asincrona, noterà subito che in questo esempio manca un CancellationToken. Come possiamo utilizzarne uno per cancellare eventuali altri Task chiamati dal nostro Job?
Il pattern utilizzato per l’utilizzo del CancellationToken nel .NET framework consiste di 4 passi:
- Istanziare un oggetto CancellationTokenSource, che gestisce ed invia notifiche di cancellazione ai singoli Token
- Passare il token ritornato dalla proprietà CancellationTokenSource.Token ad ogni Task di cui si voglia gestire la cancellazione
- Predisporre un meccanismo con il quale ogni Task risponda alla cancellazione
- Richiamare il metodo CancellationToken.Cancel per inviare una richiesta di cancellazione
Questo pattern viene implementato dalla libreria di Quartz internamente al context del Job. L’invocazione dell’interrupt dallo scheduler non fa altro che chiamare un Cancel() del context del Job che a sua volta richiama il Cancel() del CancellationTokenSource istanziato. Potete osservare questo meccanismo direttamente nel codice sorgente della libreria presente su GitHub: https://github.com/quartznet/quartznet/blob/master/src/Quartz/Core/QuartzScheduler.cs.
A questo punto non ci resta che propagare il cancellation token del Job a tutti i Task di cui vogliamo gestire la cancellazione:
public async Task Execute(IJobExecutionContext context) {
await MyAsyncTask(context.CancellationToken);
}
Spero di avervi dato qualche spunto interessante da utilizzare nei vostri progetti.
Al prossimo articolo!