Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Tour d’horizon de techniques pour lancer une tâche de fond en Asp.Net partie II

Poster un commentaire

On a déjà vu un certain nombre de techniques pour faire exécuter des tâches de fond dans un environnement Asp.Net.

https://amethyste16.wordpress.com/2014/09/12/tour-dhorizon-de-techniques-pour-lancer-une-tache-de-fond-en-asp-net-partie-i/

 

Après avoir vu les méthodes « proches du métal », nous allons nous tourner vers des frameworks spécialisés. Le principal intérêt de ces frameworks est d’apporter certains services comme la planification. Donc dès que vous avez besoin d’aller au delà du scénario: « je lance une requête, mais je n’attends pas le traitement complet pour rendre la main », tournez vous vers les Framework. Nous allons tester les environnements suivants:

  • WebBackgrounder
  • FluentScheduler
  • Quartz.Net
  • Hangfire

Un autre intérêts des frameworks présentés est aussi de pouvoir fonctionner au delà des limites de ASP.NET (du moins certains) comme pouvoir s’insérer dans un service Windows ou une application console.

 

Une remarque. Si vous êtes sous Azure, ne vous embêtez pas. Les web jobs et les Scheduler répondront de façon beaucoup plus simple au problème. Il s’agit ici de solution on premises.

Webbackgrounder

Le projet est ici:

https://github.com/NuGet/WebBackgrounder

Pour moi cet outil est un vrai mystère. Le code n’a plus évolué depuis 3 ans, mais son package Nuget est récupéré massivement:

2014-09-13_20-33-05

Et pourtant il n’y a pas de documentation, un code quasi sans explication, mais tout de même un projet de démo. Pire encore il n’y a aucun éco système apparent. Tapez son nom dans Google on ne trouve quasiment rien et je pense que cet article est ce qu’il y a de plus complet à ce jour.

Disons que si Phil Haack n’était pas derrière et si Scott Hanselman ne le proposait pas, je laisserai tomber.

 

N’attendez pas un SDK haut de gamme avec des tonnes de fonctionnalités. Celui-ci ne peut lancer qu’une seule tâche de fond à la fois et la planifier à intervalles réguliers, mais il peut néanmoins  coordonner l’exécution des tâches sur plusieurs serveurs.

 

WebBackgrounder définit la notion de Job qui est simplement la tâche à exécuter en appel de fond. Un Job est une instance de IJob:

2014-09-14_00-37-29

La méthode Execute() fait exactement ce que le nom suggère. Vous remarquerez que Job porte les paramètres qui permettent de planifier dans le temps son exécution: on l’exécute à partir de Interval depuis l’initialisation du Job. La propriété Timeout permet de définir une durée limite d’attente du Job. Cette fonctionnalité n’est exploité qu’avec un coordinateur de type Web Farm (voir plus loin).

Il faut résoudre le problème de « l’oubli » de la tâche de fond lorsque AppDomain recycle. C’est le boulot de la classe JobHost qui est presque identique à la version synchrone développée dans la partie I de cet article. C’est pour cela que l’on ne peut exécuter qu’un seul job à la fois.

Ceci étant le modèle est extensible  puisque un hôte est simplement une classe qui implémente IJobHost.

2014-09-14_01-00-21

On a un Job, on a un hôte, mais qui se charge de lancer l’exécution? C’est IJobCoordinator.

Il en existe en deux couleurs:

  1. SingleServerJobCoordinator  => coordinateur par défaut
  2. WebFarmJobCoordinator

2014-09-14_00-36-14

Le deuxième coordinateur peut coordonner l’exécution des Jobs sur plusieurs serveurs pour assurer qu’à un instant donné, un seul se lance. La coordination nécessite une base de données.

GetWork() lance l’exécution d’un Job passé en paramètre. Dans le cas où il y a plusieurs Jobs dans la liste, chacun avec ses paramètres de planification, on a besoin d’une classe capable de désigner le Job à exécuter à un instant donné. C’est Scheduler qui fait cette recherche. Attention, n’attendez pas de temps réel ni même une grande précision.

 

On voit que si le SDK est simple, il est néanmoins très extensible. Il faut donc une classe pour mettre en relation tout ces éléments, c’est le rôle de JobManager dans lequel on injecte une instance de IJobCoordinator, IJobHost et un tableau de IJob.

2014-09-14_12-37-19

On remarque plusieurs méthodes importantes.

  • Start
  • Stop
  • Fail

Start demande au JobManager de lancer l’exécution de la séquence de Jobs qui lui a été fournit. Il va donc parcourir la liste des Jobs avec DoNextJob() continûment en fonction du planning de chaque Job. Si on veux cesser les traitements, on devra appeler Stop().

La méthode Fail() permet d’insérer un comportement lorsque une exception se produit. Par exemple le loguer.

 

Nous avons ce qu’il faut pour monter un exemple. Le projet Github est livré avec une démo qui lance une séquence de Jobs et affiche sur une page Web continuellement rafraîchie l’historique d’exécution des Jobs. Ca donne ceci:

2014-09-14_13-49-03

C’est trop compliqué pour cet article, je vais donc reprendre l’exemple de la première partie.

On créé d’abord le Job en fait deux:

public sealed class SampleJob : Job
{
    public SampleJob(TimeSpan interval)
    : base("Sample Job", interval)
    {
    }

    public override Task Execute()
    {
        return new Task(
        () =>
            {
                Trace.WriteLine("Démarrage");
                for (int i = 0; i < 5; i++)
                {
                     Trace.WriteLine("Sommeil: " + i.ToString());
                }
                Trace.WriteLine("Terminé");
            });
    }
}

public sealed class EndJob : Job
{
    public EndJob(TimeSpan interval)
    : base("End Job", interval)
    {
    }

    public override Task Execute()
    {
        return new Task(() => Trace.WriteLine("Chef j'ai glissé!"));
    }
}

 

J’ai ensuite créé l’action suivante:

static readonly JobManager _jobManager = CreateJobWorkersManager();

public ActionResult Index()
{
    _jobManager.Start();
    ViewBag.Message = "C'est partit!";
    return View();
}

private static JobManager CreateJobWorkersManager()
{
    var jobs = new IJob[]
    {
        new SampleJob(TimeSpan.FromSeconds(5))
        ,new EndJob(TimeSpan.FromSeconds(15))
    };

    var manager = new JobManager(jobs,new SingleServerJobCoordinator());
    manager.Fail(ex => Trace.WriteLine(ex.Message));
    return manager;
}

 

Lancez l’appli et vous devriez voir des séquences du style:

Démarrage
Sommeil: 0
Sommeil: 1
Sommeil: 2
Sommeil: 3
Sommeil: 4
Terminé
Démarrage
Sommeil: 0
Sommeil: 1
Sommeil: 2
Sommeil: 3
Sommeil: 4
Terminé

Et au bout de 15 secondes on voit apparaître en plus:

Chef j’ai glissé!

On peut aussi créer une action qui appelle JobManager.Stop() pour arrêter les tâches, autrement elles continuent en permanence.

 

FluentScheduler

Fluent vient d’un style d’écriture de Framework proposé par Martin Fowler et Eric Evans. On parle de désignation chaînée en Français. L’idée est de chaîner les méthodes les unes après les autres à partir d’un même objet. Je souligne la fin de la phrase car c’est ce point qui est crucial, on ne change pas d’objet.

On parle aussi de DSL (Domain Specific Language).

 

En C# l’exemple le plus notable c’est Linq.

Personnellement je ne suis pas fan parce que c’est plus compliqué de suivre chaque étape du traitement lors d’une session de débogage. Mais je reconnais la meilleure lisibilité apportée.

 

On installe le Framework avec le package Nuget:

Install-Package FluentScheduler
On définit une ou plusieurs tâches qui doivent hériter de ITask:

2014-09-14_23-34-50

La méthode exécute sera appelée par le Framework pour l’exécuter. Curieusement on n’a pas d’équivalent de JobHost, on doit fournir la plomberie nous même. Par exemple les deux tâches suivantes nous servirons d’exemple:

public class SampleTask : ITask, IRegisteredObject
{
    private readonly object _lock = new object();
    private bool _shuttingDown;

    public SampleTask()
    {
        HostingEnvironment.RegisterObject(this);
    }

    public void Execute()
    {
        lock (_lock)
        {
            if (_shuttingDown)
            {
                return;
            }

            Trace.WriteLine("SampleTask est là");
        }
    }

    public void Stop(bool immediate)
    {
        lock (_lock)
        {
            _shuttingDown = true;
        }
        HostingEnvironment.UnregisterObject(this);
    }
}

public class MyOtherTask : ITask, IRegisteredObject
{
    private readonly object _lock = new object();
    private bool _shuttingDown;

    public MyOtherTask()
    {
        HostingEnvironment.RegisterObject(this);
    }

    public void Execute()
    {
        lock (_lock)
        {
            if (_shuttingDown)
            {
                return;
            }

            Trace.WriteLine("MyOtherTask est passé par là");
        }
    }

    public void Stop(bool immediate)
    {
        lock (_lock)
        {
            _shuttingDown = true;
        }
        HostingEnvironment.UnregisterObject(this);
    }
}

On doit donc fournir nous même pas mal de plomberie.

L’étape suivante est la définition d’un Registry dans lequel on va créer notre planification.

2014-09-14_23-44-30

Schedule est la propriété utilisée pour enregistrer les plannings des tâches à exécuter.

Commençons avec un exemple simple qui démontrera la puissance des interfaces fluents:

public class MyRegistry : Registry
{
    public MyRegistry()
    {
        //Planifie deux tâches à lancer maintenant, puis toutes les minutes
        Schedule<SampleTask>().AndThen<MyOtherTask>().ToRunNow().AndEvery(1).Minutes();
    }
}

C’est ensuite le rôle de TaskManager  de lancer l’exécution. On peut l’initialiser dans une action ou bien Application_Start:

TaskManager.Initialize(new MyRegistry());

L’exécution de cet exemple donne ceci:

2014-09-14_23-51-11

De nombreuses autres combinaisons sont possibles, par exemple:

Schedule<MyTask>().ToRunNow()
Schedule<MyTask>().ToRunNow().And().ToRunEvery()...
Schedule<MyTask>().ToRunEvery(30).Seconds()
Schedule<MyTask>().ToRunEvery(15).Minutes()
Schedule<MyTask>().ToRunEvery(1).Hours().At(15)
Schedule<MyTask>().ToRunEvery(2).Days().At(0, 15)
Schedule<MyTask>().ToRunEvery(1).Months().On(1).OfMonth().At(0, 15)
Schedule<MyTask>().ToRunEvery(1).Months().On(1).Monday().At(0, 15)

On a donc déjà un modèle de planification beaucoup plus sophistiqué que WebBackgrounder.

Il se peut qu’une tâche lève une exception. C’est géré par FluentScheduler.

TaskManager.UnobservedTaskException += TaskManager_UnobservedTaskException;
TaskManager.Initialize(new MyRegistry());

void TaskManager_UnobservedTaskException(TaskExceptionInformation sender, UnhandledExceptionEventArgs e)
{
    // un traitement en cas d'exception
}

 

Quartz.Net

Il s’agit de la version .Net d’un projet Java. Si Quartz s’intègre à des applications Web, il peut aussi trouver sa place dans d’autres types de projets comme des applications Console. Le site du projet est ici:

http://www.quartz-scheduler.net/

Vous verrez qu’il est fournit avec de la vraie documentation et de vrais exemples. On commence à changer de monde et passer en division Pro.

Bien entendu on l’installe avec Nuget:

Install-Package Quartz

 

Les interfaces clefs de Quartz sont les suivantes:

2014-09-16_23-00-51

 

Quartz est construit autour de la notion de IScheduler qui est l’Api d’entrée du Framework.

Le moyen le plus classique de créer un scheduler est celui-ci:

L’instance par défaut que l’on peut paramétrer depuis des fichiers de configuration:

IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();

La fabrique StdSchedulerFactory a des paramètres par défaut que l’on peut surcharger au niveau des paramètres de configuration. Ceux-ci peuvent avoir 3 sources non mutuellement exclusives:

  1. une NameValueCollection passée en paramètres de la fabrique
    http://stackoverflow.com/questions/1455819/configuring-adojobstore-with-quartz-net
  2. Le fichier de configuration standard de l’application
    https://sites.google.com/site/poshanyehwork/windows/aspnetscheduledtaskswithquartznetworkingtested
  3. un fichier de configuration au format Quartz à placer à la racine du site

Une fois un scheduler obtenu on l’initialise à l’aide de sa méthode Start().

Le scheduler est chargé de lancer des IJob compte tenu d’un calendrier sous la forme d’un  ITrigger. Un exemple typique de création de jobs est le suivant:

try
{
    IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
    scheduler.Start();

    // Définition d'un Job
    IJobDetail job = JobBuilder.Create<HelloJob>()
    .WithIdentity("job1", "group1")
    .Build();

    // Le job démarre immédiatement puis toutes les 10 secondes éternellement
    ITrigger trigger = TriggerBuilder.Create()
        .WithIdentity("trigger1", "group1")
        .StartNow()
        .WithSimpleSchedule(x => x
        .WithIntervalInSeconds(10)
        .RepeatForever())
        .Build();

    // planifie le job dans le scheduler
    scheduler.ScheduleJob(job, trigger);
}
catch (SchedulerException se)
{
}

 

Ce code peut être placé dans la méthode Main d’une application Console ou bien dans une action d’une application MVC ou dans Global.asax.  La classe JobBuilder est utilisée pour créer des instances de IJobDetail à partir d’un IJob comme HelloJob:

public class HelloJob : IJob
{
    public void Execute(IJobExecutionContext context)
    {
        Trace.WriteLine("Greetings from HelloJob!");
    }
}

Le ITrigger est ensuite définit par la classe TriggerBuilder avec une interface à la FluentScheduler.

Il est possible d’arrêter à tout moment le Scheduler à l’aide de sa méthode ShutDown().

 

Quartz propose plusieurs variétés de scheduler, SimpleTrigger et CronTrigger sont les plus fréquents mais d’autres extensions de TriggerBuilder existent:

  • WithCalendarIntervalSchedule
    On sélectionne des dates de déclenchement dans un calendrier
  • WithCronSchedule
    Déclenchement sur la base d’un calendrier. Par exemple « chaque mercredi à midi » ou « tous les 15 du mois »
  • WithDailyTimeIntervalSchedule
    Se déclencher toutes les heures entre 8:00 h et 15:00 h
  • WithSimpleSchedule
    Exécution unique à une date donnée ou bien un nombre déterminé de fois ou encore une répétition dans le temps

Quartz sépare complètement les jobs des déclencheurs. L’avantage de cette architecture est que par exemple un même déclencheur peut être affecté à plusieurs jobs. Il devient aussi possible de réaffecter un autre trigger à un job à tout moment sans avoir à le recréer. c’est une architecture très souple.

IJobDetail est une classe importante qui sera transmise au contexte d’exécution du job:

 

Dans le code d’exemple vous avez du remarquez que l’on ne passe pas une instance de IJob pour construire IJobDetail, mais juste son type dans la généricité. Cela signifie que l’on ne peut pas persister des données directement dans le job, ni définir un job avec des constructeurs paramétrés. Il est néanmoins possible de passer des paramètres à un job.

2014-09-16_23-48-57

Une de ses propriétés très utilisée est JobDataMap. Cette classe permet de passer au job des instances de classes sérialisables. Par exemple:

IJobDetail job = JobBuilder.Create<HelloJob>()
    .WithIdentity("myJob", "group1")
    .UsingJobData("jobSays", "Hello World!")
    .UsingJobData("myFloatValue", 3.141f)
    .Build();

Le job accède à ses données en interrogeant:

context.JobDetail.JobDataMap

 

Les déclencheurs ont également un JobDataMap. On peut fusionner les data du job et du déclencheur ainsi:

context.JobDetail.MergedJobDataMap

 

Je ne vais pas aller plus loin dans la description de ce Framework, on a vu l’essentiel. J’encourage fortement les lecteurs de cet article à parcourir le très bon tutoriel fournis sur le site du projet.

Quartz est un outil très complet et taillé pour des applications professionnelles. Il gère en particulier un repository pour les tâches et les déclencheurs, les web farm, les logs

http://www.quartz-scheduler.net/features.html

 

Un petit reproche, Quartz semble être confiné aux applications non Web dans le sens où je n’ai pu trouver nulle trace de reprise automatique d’un job en cas de recyclage de IIS. Bon le repository est capable de sauvegarder un état et cela doit pouvoir s’organiser. Je n’ai pas essayé.

 

Hangfire

Vous trouverez le site du projet ici:

http://hangfire.io/

Ici aussi on a affaire à un projet de niveau professionnel avec une documentation exemplaire. Autre point à mentionner, c’est Owin dans les coulisses ce qui assure sa portabilité dans tout environnement et pas seulement ASP.NET.

Une chose que j’apprécie, c’est la gestion des jobs en cas de recyclage du processus hôte (IIS, console…). Hangfire sait reprendre là où il a été interrompu de façon automatique.

Franchement, Quartz est génial, mais là on tombe dans la grande classe. Le niveau de finition…

 

L’éco système propose bien entendu une palette de packages Nuget, mais le principal est celui-ci:

Install-Package HangFire

 

On se créée une base de données en local. La base de données servira à alimenter le panneau de contrôle et à la surveillance de HangFire.

Par défaut il s’agit de SQL Server, mais d’autres repository comme Redis sont possibles. Par défaut toujours, seule une base locale est supportée, mais on peut le reparamétrer.

On se construit ensuite un projet MVC, puis on ajoute une classe OWIN Startup :

 

using Microsoft.Owin;
using Owin;
using Hangfire;
using Hangfire.SqlServer;

[assembly: OwinStartup(typeof(WebApplication1.Startup))]

namespace WebApplication1
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseHangfire(config =>
            {
                config.UseSqlServerStorage("amethyste");
                config.UseServer();
            });
        }
    }
}

La chaîne de connexion vers la base est « amethyste« . On lance.

Rien de spectaculaire se produit à ce stade. Mais la base devrait voir apparaître quelques tables:

2014-09-17_21-47-00

HangFire a même installé un panneau de contrôle (le Dashboard) accessible via l’url:

http://<votre site>/hangfire

Qui ressemble à ceci:

 2014-09-17_21-49-32

Notez déjà l’onglet Servers. HangFire peut tourner sur une ferme.

Pour l’instant il n’y a pas grand chose à voir, mais le tutoriel ne fait que commencer. Mais en bossant plus ça peut ressembler à ceci:

2014-09-17_22-27-45

 

Il y a plusieurs options pour lancer un job.

  • Lance et oublie
  • Job planifié
  • Job récurrent

 

Mode lance et oublie

Dans une action j’ai ajouté ceci:

public ActionResult Index()
{
    BackgroundJob.Enqueue(() => PremierTest());

    return View();
}

public void PremierTest()
{
    Trace.WriteLine("Début");
    Thread.Sleep(5000);
    Trace.WriteLine("Fin");
}

La méthode doit être publique.

 

Le panneau ressemble alors à ceci:

2014-09-17_22-02-14

Encore plus fort, cliquez sur Succeeded puis sur le job qui s’est exécuté:

2014-09-17_22-05-51

On peut rejouer le job en cliquant sur Requeue et on voit bien la fenêtre de sortie VS se remplir à nouveau:

2014-09-17_22-07-33

Par défaut le job est poussé dans la queue « Default », mais on peut gérer plusieurs queue.

 

On peut ne pas souhaiter ajouter des méthodes publiques dans un contrôleur. On a juste besoin de créer une classe externe:

public class MonPremierJob
{
    public void Emit()
    {
        Trace.WriteLine("Début");
        Thread.Sleep(5000);
        Trace.WriteLine("Fin");
    }
}

 

Puis on lance avec la version générique de Enqueue:

BackgroundJob.Enqueue<MonPremierJob>(x => x.Emit());

 

Job planifié

Le job planifié se déclenche à un moment donné:

BackgroundJob.Schedule(() => Trace.WriteLine("Delayed"), TimeSpan.FromSeconds(2));

Une fois le job lancé il est placé dans un scheduler, puis poussé dans la queue d’où on peut éventuellement le relancer.

Job récurrent

Un job lancé de façon récurrente, par exemple chaque minute:

RecurringJob.AddOrUpdate(() => Trace.Write("Recurring"), Cron.Minutely);

Pour des scénarios spéciaux on peut aussi injecter une expression cron et la librairie NContab de Google:

https://code.google.com/p/ncrontab/

 

Une fois activé un job est exécuté. En cas de problème il peut être réexécuté jusqu’à ce qu’il arrive au out. On peut contrôler finement la stratégie de réexécution à l’aide de l’attribut AutomaticRetryAttribute.

 

Contrairement à Quartz, il existe un moyen de notifier un job que son hôte va recycler avec un jeton d’annulation.

public class MonPremierJob
{
    public void Emit(IJobCancellationToken token)
    {
        Trace.WriteLine("Début");

        try
        {
            for (var i = 1; i <= 100; i++)
            {
                Thread.Sleep(1000);
                token.ThrowIfCancellationRequested();
            }
        }
        catch (OperationCanceledException)
        {
            Trace.WriteLine("Annulation réclamée, on sort...");
            throw;
        }
        Trace.WriteLine("Fin");
    }
}

Une fois lancé, rendez vous dans le panneau:

2014-09-17_23-01-02

Cliquez sur Deleted et le job est supprimé. On peut aussi faire un iisreset. Dans les deux cas le jeton s’active.

Pour annuler manuellement le job il faut par exemple:

MonPremierJob job = new MonPremierJob();
BackgroundJob.Enqueue(() => job.Emit(JobCancellationToken.Null));

JobCancellationToken jeton = new JobCancellationToken(true);
job.Emit(jeton);

On peut aussi récupérer le jobId:

string jobId = BackgroundJob.Enqueue <MonPremierJob>(x => x.Emit(JobCancellationToken.Null));

BackgroundJob.Delete(jobId);

Cette solution est beaucoup plus praticable, l’id peut remonter dans la vue pour pouvoir créer un bouton d’annulation.

Le site du projet propose de nombreux autres exemples. N’hésitez pas à les parcourir ainsi que le tutoriel.

 

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s