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 I

Poster un commentaire

Sujet à controverse, pour certains installer une tâche de fond dans ASP.NET n’a aucun sens. Sens ou pas, le problème est récurrent dans les forums et nous allons aborder dans cet articles diverses techniques pour y parvenir.

 

Le deuxième article sera consacré aux frameworks spécialisés.

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.

Lancer une tâche de fond, c’est lancer un thread dans un environnement Web, mais sans qu’il soit associé à une requête. Et cela présente diverses difficultés, par exemple:

  • Une exception non gérée fera immédiatement tomber le processus courant
  • Si le site tourne dans une ferme de serveurs, on peut se retrouver avec plusieurs instances de la même tâche au même moment. Il faudra donc gérer la synchronisation des tâches et des ressources utilisées

Ces deux problèmes ne sont pas insurmontables. Par exemple une base de données communes à chaque serveur peut servir à la synchronisation.

 

La première idée que l’on peut avoir est de lancer vaillamment ThreadPool, Thread, Task.Run… Sur le papier c’est la solution la plus simple. Mais elle ne fonctionnera pas.

La raison est que ASP.NET n’a aucune vision sur la tâche que vous lancez ainsi. Pour lui elle n’existe pas. Cela implique quoi?

L’AppDomain  peut se recycler pour diverses raisons et c’est parfaitement normal. En cas de recyclage, votre tâche sera brutalement interrompue, laissant les données dans un état indéfinis. Le recyclage a plusieurs causes et en particulier:

  • IIS se recycle automatiquement toutes les 29 heures
  • Par défaut les pools d’application se mettent en sommeil au bout de 20 minutes d’activité
  • La modification de certains fichiers comme le fichier de configuration recycle le pool
  • De nombreuses raisons externes peuvent faire tomber un AppDomain, un bug notamment

Il est donc indispensable de disposer d’un moyen de gérer ce problème de façon à ce que votre tâche puisse résister un peu et terminer son travail ou au pire sauvegarder proprement ses états.

Nous allons nous concentrer sur ce problème et examiner diverses solutions:

  1. Les objets enregistrés
  2. Les web jobs
  3. Les worker role
  4. QueueBackgroundWorkItem (QBWI)
  5. des frameworks spécialisés
    Sera l’objet de la deuxième partie

 

Les objets enregistrés

Phil Haack propose la méthode suivante dans un article célèbre [Ref 2].

On commence par créer une classe servant à héberger notre future tâche de fond:

public sealed class JobHost : IRegisteredObject
{
    private readonly object _lock = new object();
    private bool _shuttingDown;

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

    public void Stop(bool immediate)
    {
        Trace.WriteLine("Stop demandé");

        lock (_lock)
        {
            _shuttingDown = true;
        }

        if (immediate)
        {
            HostingEnvironment.UnregisterObject(this);
        }
    }

    public void DoWork(Action work)
    {
        lock (_lock)
        {
            if (_shuttingDown)
            {
                return;
            }

            work();
       }
    }
}

On enregistre un objet à l’aide de la méthode HostingEnvironment.RegisterObject().

Cette méthode attend une classe qui implémente IRegisteredObject. La classe en question sera chargée d’exécuter la tâche de fond.

L’interface expose une unique méthode:

void Stop(bool immediate);

 

Lorsqu’un AppDomain envisage de se recycler, la méthode Stop est appelée deux fois par ASP.NET.

  • Une première fois avec false en paramètre
    C’est une alerte qui conseille de terminer dès que possible
  • une deuxième fois avec true en paramètre, 30 secondes après la première notification
    On doit alors immédiatement dé enregistrer notre objet, c’est notre dernière chance de sauvegarder ce qui peut l’être

Je pense que le code est clair par lui-même.

La méthode fonctionne, mais elle est synchrone et ne rendra pas la main au site tant que la tâche n’est pas terminée.

On peut souhaiter profiter des Task pour lancer l’exécution de façon asynchrone et ainsi rendre immédiatement la main au site. Le nouveau code est le suivant:

public sealed class JobHost : IRegisteredObject
{
    private Task task;

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

    public void Stop(bool immediate)
    {
        Trace.WriteLine("Stop demandé");

        if (task.IsCompleted || task.IsCanceled || task.IsFaulted || immediate)
        {
            HostingEnvironment.UnregisterObject(this);
        }
    }

    public void DoWork(Action work)
    {
        try
        {
            task = Task.Run(work);
        }
        catch (AggregateException ex)
        {
            foreach (var innerEx in ex.InnerExceptions)
            {
            }
        }
        catch (Exception ex)
        {
        }
    }
}

 

On lance les opérations de cette façon:

public ActionResult Index()
{
    JobHost jobHost = new JobHost();
    jobHost.DoWork(async () => await WorkAsync());

    ViewBag.Message = "C'est partit!";
    return View();
}

private async Task WorkAsync()
{
    Trace.WriteLine("Démarrage");
    for (int i = 0; i < 10; i++)
    {
        Trace.WriteLine("Sommeil: " + i.ToString());
        await Task.Delay(2000);
    }
    Trace.WriteLine("Terminé");
}

Je vous laisse vérifier que cela fonctionne bien en asynchrone.

Il est possible de lancer une tâche de façon planifiée avec un Timer. On garde la classe JobHost précédente:

protected void Application_Start()
{
    timer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
}

private static void OnTimerElapsed(Object sender)
{
    jobHost.DoWork(() =>
    {
        Trace.WriteLine("Une tâche");
    });
}

private static readonly System.Threading.Timer timer = new System.Threading.Timer(OnTimerElapsed);
private static JobHost jobHost = new JobHost();

 

La tâche est relancée de façon asynchrone toutes les secondes.

Les worker roles

Rien de spécial à dire, il suffit de reprendre les exemples précédents et les placer dans un worker role.

 

Les web jobs

J’ai déjà abordé la question des web jobs dans deux articles de façon je crois très complète:

  1. https://amethyste16.wordpress.com/2014/06/01/taches-planifiees-sous-windows-azure-partie-i/
  2. https://amethyste16.wordpress.com/2014/09/10/taches-planifiees-sous-windows-azure-partie-iii/

 

QueueBackgroundWorkItem

C’est une des nouveautés de .Net 4.5.2. Par conséquent, à la date d’écriture de cet article, ce n’est pas compatible Azure.
C’est une méthode très similaire à IRegisteredObject et de fait dans les coulisses je pense que ce n’est pas un hasard.

 

QueueBackgroundWorkItem (QBWI) est une méthode statique de la classe HostingEnvironment. Elle permet d’inscrire une tâche d’arrière plan non liée à un thread auprès d’ASP.Net qui en garde alors une trace. Ainsi si l’AppDomain tente de recycler, ASP.Net peut tenter de différer l’événement pour laisser le temps à la tâche de se terminer en douceur.
Il y a 2 signatures possibles:

public static void QueueBackgroundWorkItem( Action<CancellationToken> workItem)
public static void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)

 

Comment lit t’on tout cela? C’est la seule difficulté.

Le délégué Action<T> représente une méthode void avec un paramètre unique de type T. La première signature va para exemple être compatible avec:

void Toto(CancellationToken ct) {};

La tâche est considérée comme terminée lorsque la méthode rend la main.

 

Func<T, TResult> représente un délégué avec un paramètre unique de type T et qui retourne un type TResult. La deuxième signature sera donc compatible avec une méthode comme:

Task Toto(CancellationToken ct) {};

La tâche est considérée comme terminée lorsque elle retourne un Task à l’état terminé.

 

Voyons tout de suite un exemple.

Dans une application MVC on créée une action:

public ActionResult LongTask()
{
    HostingEnvironment.QueueBackgroundWorkItem(ct => LongTaskAsync(ct));

    ViewBag.Message = "Opération lancée";
    return View("Index");
}

 

La méthode LongTaskAsync est:

private async Task<CancellationToken> LongTaskAsync(CancellationToken ct)
{
    int iteration = 0;
    Trace.WriteLine("Démarre LongTaskAsync");

    try
    {
        for (int i = 0; i < 10; i++)
        {
            ct.ThrowIfCancellationRequested();

            iteration++;
            string message = string.Format("Itération: {0}, Index: {1}", iteration, i);
            Trace.WriteLine(message);

            Trace.WriteLine("Mise en sommeil");
            await Task.Delay(2000, ct);
        }
    }
    catch (OperationCanceledException)
    {
        Trace.WriteLine("Annulation demandée");
    }

    Trace.WriteLine("Termine LongTaskAsync");
    return ct;
}

Vous noterez qu’elle ne fait pas grand chose de particulier si ce n’est attendre plusieurs fois. Lancez l’application depuis IIS:

2014-09-11_10-54-58

On observe le déroulement des applications dans la fenêtre output de VS:

2014-09-11_10-57-17

Le site de test a repris la main, vous pouvez continuer à travailler dessus.

 

Pour mettre en évidence le rôle du jeton d’annulation, lancez un iisreset pour interrompre l’AppDomain:

2014-09-11_10-59-36

QBWI impose plusieurs contraintes:

  • il ne peut être appelé que dans un AppDomain géré par Asp.Net
  • La tâche de fond ne reçoit pas le contexte d’exécution du code appelant. Si vous avez besoin d’une propriété de HttpContext par exemple, passez sa valeur comme paramètre à la tâche de fond.
    Par contre ne passez pas HttpContext lui-même.

Lorsque le domaine essaye de se terminer il ne peut être repoussé que de 90 seconde maximum… quel que soit le nombre de tâches de fond. Si au bout de ce délai elles n’ont pas pu s’arrêter, c’est perdu.

Les activateurs

Il y a plusieurs façon d’installer les tâches de fond dans une application ASP.NET. On en a montré plusieurs dans cet article. Mais si vous utilisez ASP.NET vous pouvez également utiliser un attribut WebActivator.
Je n’ai pas vraiment exploré cette solution mais Phill haack s’en sert partout. L’idée est d’injecter du code avant même l’exécution de Application_Start.

Il y a un article de Phill Haack ici:

http://haacked.com/archive/2010/05/16/three-hidden-extensibility-gems-in-asp-net-4.aspx/

 

Et un package Nuget qui facilite les choses:

https://www.nuget.org/packages/WebActivatorEx/

Et deux tutos:

http://bartwullems.blogspot.in/2011/06/webactivator-how-to-control-order-of.html

http://blogs.msdn.com/b/davidebb/archive/2010/10/11/light-up-your-nupacks-with-startup-code-and-webactivator.aspx

 

Bibliographie

Si vous lisez les articles qui suivent, vous retrouverez facilement mes sources d’inspiration.

  1. http://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx
  2.  http://haacked.com/archive/2011/10/16/the-dangers-of-implementing-recurring-background-tasks-in-asp-net.aspx/
  3.  http://blogs.msdn.com/b/webdev/archive/2014/06/04/queuebackgroundworkitem-to-reliably-schedule-and-run-long-background-process-in-asp-net.aspx
  4. http://blog.stephencleary.com/2014/06/fire-and-forget-on-asp-net.html

 

 

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