Implementar tarefas em segundo plano em microsserviços com IHostedService e a classe BackgroundService

Dica

Esse conteúdo é um trecho do eBook da Arquitetura de Microsserviços do .NET para os Aplicativos .NET em Contêineres, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

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

Tarefas em segundo plano e trabalhos agendados são algo que talvez você precise usar em qualquer aplicativo, seja seguindo ou não o padrão de arquitetura de microsserviços. A diferença ao usar uma arquitetura de microsserviços é que você pode implementar a tarefa em segundo plano em um processo e contêiner separado para hospedagem, para que você possa dimensioná-la para baixo e para cima com base em suas necessidades.

De um ponto de vista genérico, no .NET, chamamos esses tipos de tarefas de Serviços Hospedados, porque são serviços e lógica que você hospeda em seu host/aplicativo/microsserviço. Observe que, neste caso, o serviço hospedado simplesmente significa uma classe com a lógica de tarefa em segundo plano.

Desde o .NET Core 2.0, a estrutura fornece uma nova interface chamada IHostedService, ajudando você a implementar serviços hospedados facilmente. A ideia básica é que você pode registrar várias tarefas de segundo plano (serviços hospedados) que são executadas em segundo plano enquanto o host da Web ou o host está em execução, conforme mostra a imagem 6-26.

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

Figura 6-26. Usando IHostedService em WebHost versus um Host

Há suporte ao ASP.NET Core 1. x e 2. x IWebHost para processos em segundo plano em aplicativos Web. O .NET Core 2.1 e versões posteriores dão suporte IHost aos processos em segundo plano com aplicativos de console sem formatação. Observe a diferença entre WebHost e Host.

Um WebHost (classe base implementando IWebHost) no ASP.NET Core 2.0 é o artefato de infraestrutura que você usa para fornecer recursos de servidor HTTP ao seu processo, como quando você está implementando um aplicativo de MVC da Web ou um serviço de API da Web. Ele fornece todos os benefícios da nova infraestrutura no ASP.NET Core, permitindo que você use a injeção de dependência, insira middleware no pipeline da solicitação e similares. O WebHost usa estes mesmos IHostedServices para tarefas em segundo plano.

Um Host (classe base que implementa IHost) foi introduzido no .NET Core 2.1. Basicamente, um Host permite que você tenha uma infraestrutura semelhante àquela que você tem com WebHost (injeção de dependência, serviços hospedados etc.), mas, nesse caso, você quer apenas ter um processo simples e mais leve como o host, sem nada relacionado ao MVC, à API da Web ou aos recursos de servidor HTTP.

Portanto, você pode escolher e criar um processo de host especializado com IHost para lidar com os serviços hospedados e nada mais, como um microsserviço feito apenas para hospedar o IHostedServices ou você pode, como alternativa, estender um ASP.NET Core WebHost existente, como um aplicativo de MVC ou API da Web do ASP.NET Core existente.

Cada abordagem tem vantagens e desvantagens, dependendo de suas necessidades de negócios e escalabilidade. A conclusão é basicamente que, se as tarefas em segundo plano não tiverem nenhuma relação com o HTTP (IWebHost), você deverá usar o IHost.

Registro de serviços hospedados em seu WebHost ou Host

Vamos analisar detalhadamente a interface IHostedService, já que seu uso é muito semelhante em um WebHost ou em um Host.

SignalR é um exemplo de um artefato usando serviços hospedados, mas você também pode usá-lo para itens muito mais simples, como:

  • Uma tarefa em segundo plano sondando um banco de dados em busca de alterações.
  • Uma tarefa agendada atualizando algum cache periodicamente.
  • Uma implementação de QueueBackgroundWorkItem que permite que uma tarefa seja executada em um thread em segundo plano.
  • Processamento de mensagens de uma fila de mensagens em segundo plano de um aplicativo Web enquanto se compartilham serviços comuns como ILogger.
  • Uma tarefa em segundo plano iniciada com Task.Run().

Você pode basicamente descarregar qualquer uma dessas ações para uma tarefa em segundo plano que implementa arquivos IHostedService.

A maneira como você adiciona um ou vários IHostedServices no WebHost ou Host é registrando-os por meio do AddHostedServicemétodo de extensão em um ASP.NET CoreWebHost (ou em um Host no .NET Core 2.1 e posterior). Basicamente, você precisa registrar os serviços hospedados na inicialização do aplicativo em Program.cs.

//Other DI registrations;

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

Nesse código, o serviço hospedado GracePeriodManagerService é o código real do microsserviço de negócios de Ordenação em eShopOnContainers, enquanto os outros dois são apenas dois exemplos adicionais.

A execução da tarefa em segundo plano IHostedService é coordenada com o tempo de vida do aplicativo (ou seja, host ou microsserviço). Você registra tarefas quando o aplicativo é iniciado e você tem a oportunidade de fazer alguma ação normal ou limpeza quando o aplicativo está sendo desligado.

Sem usar IHostedService, você sempre pode iniciar um thread em segundo plano para executar qualquer tarefa. A diferença está precisamente no tempo de desligamento do aplicativo quando esse thread seria simplesmente seria interrompido sem a oportunidade de executar as ações de limpeza normais.

A interface IHostedService

Quando você registra um IHostedService, o .NET chama os métodos StartAsync() e StopAsync() do tipo IHostedService durante o início e a parada do aplicativo, respectivamente. Para obter mais detalhes, confira interface IHostedService.

Como você pode imaginar, é possível criar várias implementações de IHostedService e registrá-las em Program.cs, como mostrado anteriormente. Todos esses serviços hospedados serão iniciados e interrompidos junto com o aplicativo/microsserviço.

Como desenvolvedor, você é responsável por lidar com a ação de parada de seus serviços quando o método StopAsync() é disparado pelo host.

Implementando IHostedService com uma classe de serviço hospedado personalizado derivado da classe base BackgroundService

Vá em frente e crie sua classe de serviço hospedado personalizada do zero e implemente o IHostedService, como você precisa fazer ao usar o .NET Core 2.0 e posterior.

No entanto, como a maioria das tarefas em segundo plano terá necessidades semelhantes em relação ao gerenciamento de tokens de cancelamento e outras operações típicas, há uma classe base abstrata conveniente da qual você pode derivar, chamada BackgroundService (disponível no .NET Core 2.1).

Essa classe fornece o trabalho principal necessário para configurar a tarefa em segundo plano.

O próximo código é a classe base abstrata do BackgroundService, conforme implementada no .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();
    }
}

Ao derivar da classe base abstrata anterior, graças àquela implementação herdada, você só precisa implementar o método ExecuteAsync() em sua própria classe de serviço hospedado personalizada, como no seguinte código simplificado eShopOnContainers que está sondando um banco de dados e publicando eventos de integração no Barramento de Eventos quando necessário.

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.");
    }

    .../...
}

Neste caso específico para eShopOnContainers, que está executando um método de aplicativo que está consultando uma tabela de banco de dados procurando pedidos com um estado específico e ao aplicar as alterações, ele está publicando eventos de integração por meio de um barramento de evento (sob ele, pode estar usando RabbitMQ ou Barramento de Serviço do Azure).

Obviamente, você pode executar qualquer outra tarefa em segundo plano de negócios em vez disso.

Por padrão, o token de cancelamento é definido com um tempo limite de 5 segundos, embora você possa alterar esse valor ao criar seu WebHost usando a extensão UseShutdownTimeout do IWebHostBuilder. Isso significa que o nosso serviço deve cancelar dentro de 5 segundos, caso contrário, ele será encerrado mais abruptamente.

O código a seguir alteraria esse tempo para 10 segundos.

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

Diagrama de classe de resumo

A imagem a seguir mostra um resumo visual das classes e interfaces envolvidas ao implementar o IHostedServices.

Diagram showing that IWebHost and IHost can host many services.

Figura 6-27. Diagrama de classe mostrando as várias classes e interfaces relacionadas ao IHostedService

Diagrama de classe: o IWebHost e o IHost podem hospedar muitos serviços, herdados de BackgroundService, que implementa o IHostedService.

Pontos importantes e considerações sobre implantação

É importante observar que a maneira como você implanta o ASP.NET Core WebHost ou .NET Host pode afetar a solução final. Por exemplo, se você implantar o WebHost no IIS ou em um Serviço de Aplicativo do Azure normal, o host poderá desligar devido a reciclagens do pool de aplicativos. Porém, se você estiver implantando eu host como um contêiner em um orquestrador como Kubernetes, poderá controlar o número garantido de instâncias ativas do seu host. Além disso, poderá considerar outras abordagens na nuvem feitas especialmente para esses cenários, como Azure Functions. Por fim, se você precisar que o serviço esteja em execução o tempo todo e estiver implantando em um Windows Server, você poderá usar um Serviço Windows.

Porém, mesmo para um WebHost implantado em um pool de aplicativos, há cenários como repopulação ou liberação do cache em memória do aplicativo em que isso ainda seria aplicável.

A interface IHostedService fornece uma maneira conveniente de iniciar tarefas em segundo plano em um aplicativo Web ASP.NET Core (no .NET Core 2.0 e versões posteriores) ou em qualquer processo e host (começando com o .NET Core 2.1 com IHost). O principal benefício é a oportunidade que você obtém com o cancelamento normal para o código de limpeza de suas tarefas em segundo plano quando o host em si está sendo desligado.

Recursos adicionais