Implémenter des tâches en arrière-plan dans les microservices avec IHostedService et la classe BackgroundService

Conseil

Ce contenu est un extrait du livre électronique « .NET Microservices Architecture for Containerized .NET Applications », disponible sur .NET Docs ou sous forme de PDF téléchargeable gratuitement et pouvant être lu hors ligne.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Vous pouvez être amené à utiliser des tâches en arrière-plan et des travaux planifiés dans n’importe quelle application, qu’elle suive ou non le modèle d’architecture de microservices. La différence, quand vous utilisez une architecture de microservices, est que vous pouvez implémenter la tâche en arrière-plan dans un processus/conteneur distinct pour l’hébergement et ainsi lui appliquer un scale down/up en fonction de vos besoins.

D’un point de vue générique, dans .NET, nous avons appelé les tâches de ce type services hébergés, car il s’agit de services/d’une logique que vous hébergez dans votre hôte/application/microservice. Notez que dans ce cas, le service hébergé représente simplement une classe avec la logique de tâche en arrière-plan.

Depuis .NET Core 2.0, le framework fournit une nouvelle interface nommée IHostedService, qui vous aide à implémenter facilement les services hébergés. L’idée de base est que vous pouvez inscrire plusieurs tâches en arrière-plan (services hébergés), qui s’exécutent en arrière-plan pendant que votre hôte ou votre hôte web est en cours d’exécution comme le montre l’image 6-26.

Diagram comparing ASP.NET Core IWebHost and .NET Core IHost.

Figure 6-26. Utilisation d’IHostedService dans un WebHost et un Host

ASP.NET Core (1.x et 2.x) prend en charge IWebHost pour les processus en arrière-plan dans les applications web. .NET Core 2.1 (et versions ultérieures) prend en charge IHost pour les processus en arrière-plan avec des applications console simples. Notez la différence faite entre WebHost et Host.

Dans ASP.NET Core 2.0, un WebHost (classe de base implémentant IWebHost) est l’artefact d’infrastructure qui vous permet de fournir des fonctionnalités de serveur HTTP à votre processus, par exemple quand vous implémentez une application web MVC ou un service d’API web. Il offre tous les nouveaux avantages en termes d’infrastructure dans ASP.NET Core : vous pouvez utiliser l’injection de dépendances, insérer des middlewares (intergiciels) dans le pipeline de requête, etc. WebHost utilise exactement les mêmes IHostedServices pour les tâches en arrière-plan.

Un Host (classe de base implémentant IHost) a été introduit dans .NET Core 2.1. En fait, Host vous permet d’avoir une infrastructure similaire à celle que vous avez avec WebHost (injection de dépendances, services hébergés, etc.), mais dans ce cas précis, vous souhaitez simplement avoir un processus simple et plus léger en tant qu’hôte, sans lien avec les fonctionnalités MVC, d’API web ou de serveur HTTP.

Ainsi, vous pouvez créer un processus hôte spécialisé avec IHost pour prendre en charge les services hébergés et rien d’autre, par exemple un microservice destiné uniquement à héberger les IHostedServices, ou vous pouvez étendre un WebHost ASP.NET Core existant, par exemple une API web ou une application MVC ASP.NET Core existante.

Chaque approche a des avantages et des inconvénients selon vos besoins professionnels et de scalabilité. En fin de compte, si vos tâches en arrière-plan n’ont rien à voir avec le protocole HTTP (IWebHost), utilisez IHost.

Inscription de services hébergés sur WebHost ou Host

Examinons plus en détail l’interface IHostedService, car son utilisation est assez similaire dans un WebHost ou un Host.

SignalR est un exemple d’artefact qui utilise des services hébergés, mais vous pouvez également l’employer pour des choses beaucoup plus simples, par exemple :

  • Tâche en arrière-plan qui interroge une base de données à la recherche des changements survenus
  • Tâche planifiée qui met à jour un cache périodiquement
  • Implémentation de QueueBackgroundWorkItem qui permet l’exécution d’une tâche sur un thread d’arrière-plan
  • Traitement des messages d’une file d’attente de messages en arrière-plan d’une application web, avec un partage des services communs tels que ILogger
  • Tâche en arrière-plan démarrée avec Task.Run()

Vous pouvez tout simplement déplacer l’une de ces actions vers une tâche en arrière-plan qui implémente IHostedService.

Pour ajouter un ou plusieurs IHostedServices dans votre WebHost ou Host, vous devez les inscrire à l’aide de la méthode d’extension AddHostedService dans un WebHost ASP.NET Core (ou dans un Host dans .NET Core versions 2.1 et supérieures). Vous devez inscrire les services hébergés dans la section de démarrage de l’application dans Program.cs.

//Other DI registrations;

// Register Hosted Services
builder.Services.AddHostedService<GracePeriodManagerService>();
builder.Services.AddHostedService<MyHostedServiceB>();
builder.Services.AddHostedService<MyHostedServiceC>();
//...

Dans ce code, le service hébergé GracePeriodManagerService correspond au code réel du microservice de commandes d’eShopOnContainers, alors que les deux autres ne sont que deux exemples supplémentaires.

L’exécution de la tâche en arrière-plan IHostedService est coordonnée avec la durée de vie de l’application (hôte ou microservice). Vous inscrivez des tâches quand l’application démarre, et vous pouvez effectuer des actions normales ou de nettoyage quand l’application s’arrête.

Sans utiliser IHostedService, vous pouvez tout de même démarrer un thread d’arrière-plan pour exécuter une tâche. La différence se situe précisément au moment de l’arrêt de l’application, quand le thread est simplement tué sans avoir eu l’occasion d’exécuter des actions de nettoyage normales.

Interface IHostedService

Quand vous inscrivez un IHostedService, .NET appelle les méthodes StartAsync() et StopAsync() de votre type IHostedService durant le démarrage et l’arrêt de l’application, respectivement. Pour obtenir des informations détaillées, consultez Interface IHostedService.

Naturellement, vous pouvez créer plusieurs implémentations d’IHostedService et inscrire chacune d’elles dans Program.cs, comme nous l’avons déjà indiqué. Tous ces services hébergés démarrent et s’arrêtent avec l’application/le microservice.

En tant que développeur, vous êtes responsable de la prise en charge de l’action d’arrêt ou de vos services quand la méthode StopAsync() est déclenchée par l’hôte.

Implémentation d’IHostedService avec une classe de service hébergé personnalisé dérivant de la classe de base BackgroundService

Vous pourriez donc créer entièrement votre classe de service hébergé personnalisé et implémenter le IHostedService comme vous le faites quand vous utilisez .NET Core 2.0 (ou une version ultérieure).

Toutefois, comme la plupart des tâches en arrière-plan ont des besoins similaires en ce qui concerne la gestion des jetons d’annulation et d’autres opérations classiques, il existe une classe de base abstraite pratique, nommée BackgroundService (disponible depuis .NET Core 2.1).

Cette classe fournit le travail principal nécessaire à la configuration de la tâche en arrière-plan.

Le code suivant est la classe de base BackgroundService abstraite implémentée dans .NET.

// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts =
                                                   new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it,
        // this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                          cancellationToken));
        }

    }

    public virtual void Dispose()
    {
        _stoppingCts.Cancel();
    }
}

Quand vous effectuez une dérivation à partir de la classe de base abstraite précédente, grâce à l’implémentation héritée, vous devez simplement implémenter la méthode ExecuteAsync() dans votre propre classe de service hébergé personnalisé, comme dans le code simplifié suivant d’eShopOnContainers. Ce code interroge une base de données et publie des événements d’intégration dans le bus d’événements, le cas échéant.

public class GracePeriodManagerService : BackgroundService
{
    private readonly ILogger<GracePeriodManagerService> _logger;
    private readonly OrderingBackgroundSettings _settings;

    private readonly IEventBus _eventBus;

    public GracePeriodManagerService(IOptions<OrderingBackgroundSettings> settings,
                                     IEventBus eventBus,
                                     ILogger<GracePeriodManagerService> logger)
    {
        // Constructor's parameters validations...
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogDebug($"GracePeriodManagerService is starting.");

        stoppingToken.Register(() =>
            _logger.LogDebug($" GracePeriod background task is stopping."));

        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogDebug($"GracePeriod task doing background work.");

            // This eShopOnContainers method is querying a database table
            // and publishing events into the Event Bus (RabbitMQ / ServiceBus)
            CheckConfirmedGracePeriodOrders();

            try {
                    await Task.Delay(_settings.CheckUpdateTime, stoppingToken);
                }
            catch (TaskCanceledException exception) {
                    _logger.LogCritical(exception, "TaskCanceledException Error", exception.Message);
                }
        }

        _logger.LogDebug($"GracePeriod background task is stopping.");
    }

    .../...
}

Dans ce cas de figure spécifique à eShopOnContainers, le code exécute une méthode d’application qui interroge une table de base de données à la recherche de commandes ayant un état spécifique. Quand des changements sont appliqués, il publie les événements d’intégration via le bus d’événements (il peut s’agir de RabbitMQ ou d’Azure Service Bus).

Bien entendu, vous pouvez exécuter une autre tâche en arrière-plan à la place.

Par défaut, le jeton d’annulation est défini avec un délai d’expiration de 5 secondes. Toutefois, vous pouvez changer cette valeur quand vous générez votre WebHost à l’aide de l’extension UseShutdownTimeout de IWebHostBuilder. Cela signifie que notre service est censé être annulé dans les 5 secondes, sinon il est tué de manière brusque.

Le code suivant change ce délai en le portant à 10 secondes.

WebHost.CreateDefaultBuilder(args)
    .UseShutdownTimeout(TimeSpan.FromSeconds(10))
    ...

Diagramme de classes récapitulatif

L’image suivante montre un récapitulatif visuel des classes et des interfaces impliquées dans l’implémentation d’IHostedServices.

Diagram showing that IWebHost and IHost can host many services.

Figure 6-27. Diagramme de classes montrant les multiples classes et interfaces liées à IHostedService

Diagramme de classes : IWebHost et IHost peuvent héberger de nombreux services, qui héritent de BackgroundService, lequel implémente IHostedService.

Éléments importants à retenir et considérations relatives au déploiement

Il est important de noter que la façon dont vous déployez votre WebHost ASP.NET Core ou votre Host .NET peut impacter la solution finale. Par exemple, si vous déployez votre WebHost sur IIS ou sur un Azure App Service normal, votre hôte peut être arrêté en raison des recyclages de pools d’applications. Mais si vous déployez votre hôte comme conteneur dans un orchestrateur comme Kubernetes, vous pouvez contrôler le nombre garanti d’instances actives de l’hôte. En outre, vous pouvez envisager d’autres approches liées au cloud, spécialement conçues pour ces scénarios, par exemple Azure Functions. Enfin, si vous avez besoin que le service soit exécuté tout le temps et si vous déployez sur un serveur Windows, vous pouvez utiliser un service Windows.

Toutefois, même pour un WebHost déployé dans un pool d’applications, il existe des scénarios tels que le repeuplement ou le vidage du cache en mémoire de l’application, qui sont toujours applicables.

L’interface IHostedService offre un moyen pratique de démarrer des tâches en arrière-plan dans une application web ASP.NET Core (dans .NET Core, versions 2.0 et ultérieures) ou dans n’importe quel processus/hôte (à partir de .NET Core 2.1 avec IHost). Le principal avantage est le suivant : grâce à l’annulation normale, vous pouvez nettoyer le code de vos tâches en arrière-plan quand l’hôte s’arrête.

Ressources supplémentaires