Hosted Service

16 de janeiro de 2023
Bryan Lima

Coder and Tech Consultant

  • Twitter
  • Facebook
  • LinkedIn
Hosted Service

Um Hosted Service é uma classe com a lógica de uma tarefa que executa em segundo plano da aplicação e que implementa a interface IHostedService

Permite executar tarefas que podem ser desacopladas do fluxo normal da funcionalidade, permitindo a funcionalidade focar na lógica de negócio e evitando gerenciar outras tarefas dentro do próprio fluxo. Alguns exemplos que podem ser desacoplados e movidos para um Hosted Service são reagir a eventos ou mensagens externas, processamentos de dados, etc.

Os tipos mais conhecidos de Hosted Service são a classe de ajuda BackgroundService e o template Worker Service.

Antes de explicar o que é um Background Task e Worker Service é necessário entender sobre o que é um Host no contexto de Hosted Service.

Generic Host

É um objeto que encapsula os recursos de ciclo de vida de uma aplicação como:

  • Injeção de Dependência
  • Logging
  • Configuração
  • Implementações de IHostedService

Uma das várias responsabilidades do Host é inicializar ou encerrar todos os Hosted Services registrados chamando IHostedService.StartAsync ou IHostedService.StopAsync.

Nota

Como o artigo não é sobre Generic Hosted não será aprofundado no tema nisso, mas caso tenha interesse em saber mais sobre o assunto segue o link sobre Generic Host

O exemplo abaixo é muito parecido com o que é utilizado no ASP.NET no momento de registrar serviços, logging, configuração etc.

C#
IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services)=>
    {
        services.AddSingleton<IWeatherApi, WeatherApi>();

        services.Configure<MyExternalConfig>(context.Configuration.GetSection("MyExternalConfig")); // Configuration Options

        services.AddHostedService<Worker>();
    })
    .Build();

await host.RunAsync();

IHostedService

Implementação

Tudo começa pela implementação dos métodos StartAsync e StopAsync da interface IHostedService que está no pacote nuget Microsoft.Extensions.Hosting.

StartAsync

Reage na inicialização da aplicação. É utilizado para alocar os recursos necessários para a execução da tarefa. Esses recursos podem ser de diversas formas como abrir conexões, preparar valores em variáveis que serão utilizados, alocar memória, etc.

Dica

StartAsync deve ser de curta duração para não bloquear a inicialização de outras tarefas.

Dica

É importante que não bloqueie a chamada desse método porque caso isso aconteça a aplicação não será inicializada.

StopAsync

Reage no encerramento correto da aplicação, isso significa quando o encerramento pode ser controlado, mas caso a aplicação seja finalizada de forma inesperada o StopAsync não será chamado. StopAsync é utilizado para desalocar recursos ou fazer pequenas limpezas em locais ou dados que foram utilizados durante a tarefa, exemplos são limpeza de arquivos ou pastas, fechar conexões, desalocar memória, etc.

Dica

Se um erro for gerado durante a execução da tarefa em segundo plano, Dispose deverá ser chamado mesmo se StopAsync não for chamado. [Microsoft]

C#
public class Worker : IHostedService, IDisposable
{
    private readonly ILogger<Worker> logger;
    public Worker(ILogger<Worker> logger)
    {
        this.logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Conexão com banco de dados foi aberta");
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Conexão com banco de dados foi fechada");
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        logger.LogInformation($"Recursos foram descartados");
    }
}

Tempo de execução no encerramento

StopAsync tem um detalhe onde por padrão tem que ser executado em no máximo 5 segundos para indicar que teve sucesso no encerramento, mas caso isso não aconteça os serviços restantes não serão parados mesmo se ainda não tiverem concluído o processamento. Caso os serviços precisem de mais tempo para pararem pode alterar o tempo limite definindo um novo valor em ShutdownTimeout nas opções do Host.

Na configuração dos serviços é definido o valor do tempo no ShutdownTimeout

C#
IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.Configure<HostOptions>(options =>
        {
            // Define um tempo maior para a finalização
            options.ShutdownTimeout = TimeSpan.FromSeconds(40);
        });
        services.AddHostedService<Worker>();
    })
    .Build();

await host.RunAsync();

Background Task

É uma tarefa que é fisicamente acoplada e executada em segundo plano em uma aplicação. Por ser acoplada a uma aplicação, ela compartilha recursos e pode gerenciá-los para evitar que uma funcionalidade se preocupe com isso no seu próprio fluxo. Um exemplo é o cache de dados externos que a tarefa pode obter os dados e adicionar ao cache para outras funcionalidades da aplicação utilizarem. O ciclo de vida de um Background Task é o mesmo que o da aplicação que ela está acoplada.

Classe BackgroundService

BackgroundService é uma classe abstrata que implementa IHostedService Ela foca na execução de uma tarefa através do método ExecuteAsync que é obrigatório implementar. Já os métodos StartAsync e StopAsync são encapsulados, permitindo escolher implementar apenas um ou outro caso precise, já que em alguns casos pode não ser importante controlar a inicialização(StartAsync) ou encerramento(StopAsync) de um Hosted Service.

Importante

Um Background Task não necessariamente precisa implementar a classe abstrata BackgroundService ele pode ser qualquer classe que implemente IHostedService e seja feito o deploy junto com a aplicação.

ExecuteAsync

É o local onde contém a lógica para executar a tarefa.

Cuidado

Como o ExecuteAsync é chamado dentro de StartAsync e ele também não pode bloquear a execução porque a aplicação não irá iniciar.

No exemplo abaixo é simulado o desacoplamento do fluxo de alguma funcionalidade a necessidade de fazer o trabalho que a tarefa está fazendo.

C#
public class DadosExternosCacheService : BackgroundService
{
    private readonly ILogger<DadosExternosCacheService> _logger;

    // Injeta as dependências para a tarefa executar
    public DadosExternosCacheService(ILogger<DadosExternosCacheService> logger)
    {
        _logger = logger;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Verifica se não houve cancelamento.
        while (!stoppingToken.IsCancellationRequested)
        {
            // 1. Obtém os dados externos da api ou banco de dados;
            // 2. Trata os dados;
            // 3. Adicionar ao cache;

            _logger.LogInformation("Dados externos atualizados: {time}", DateTimeOffset.Now);
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

Aplicabilidade

Exemplos de quando utilizar são:

  • Responder a mensagens ou eventos externos;
  • Executar tratamento de dados, fora do fluxo da requisição;
  • Obter e tratar dados externos para adicionar ao cache para outras funcionalidades usarem;
  • Agrupar dados por tempo, quantidade ou tamanho e enviar tudo de uma vez em uma requisição. (Batch request);
Cuidado

Cuidado ao adicionar muitos Background Tasks em um mesmo projeto porque isso pode significar que existe um problema na arquitetura do projeto onde tem muitas responsabilidades e talvez seja necessário utilizar Worker Process em uma abordagem no estilo micro-serviços para resolver o problema.

Worker Service

É uma aplicação Console com um Host que contém uma ou mais tarefas(implementações de IHostedService que foram fisicamente desacopladas da aplicação principal. Essas tarefas por estarem desacopladas da aplicação principal tem o ciclo de vida diferente, onde são feitos os deploys, inicializações e encerramentos dos Hosted Services de forma independente. Um exemplo de como essa diferença ajuda é na forma do deploy que evita que caso o Worker Service precise ser alterado tenha que fazer um deploy da aplicação principal junto.

Para utilizar um Worker Service é necessário escolher o tipo “Worker Service” no momento de criar um novo projeto ou executar a linha de comando dotnet new worker -o NomeDoProjeto

Ao criar o projeto pode perceber que contém um exemplo chamado Worker.cs ele é uma tarefa que implementa BackgroundService.

Aplicabilidade

  • Executar tarefas que podem ser desacopladas da aplicação principal;
  • Executar tarefas agendadas;
  • Reagir a mensagens ou eventos externos;
  • Separar a responsábilidades para micro-serviços;
  • Formatação e limpeza de dados para IA/ML;

Dicas sobre Hosted Services

Escopo (Injeção de Dependência)

Os Hosted Services são registrados como singleton, isso significa que eles que caso queira outro escopo além de singleton será necessário injetar IServiceProvider para obter outras instâncias.

C#
// Program.cs
IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.Configure<HostOptions>(options =>
        {
            options.ShutdownTimeout = TimeSpan.FromSeconds(6);
        });
        services.AddHostedService<Worker>();
        services.AddTransient<IWeatherApi, WeatherApi>(); // Exemplo registrado como Transient
    })
    .Build();

await host.RunAsync();

// Worker
public class Worker : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<Worker> _logger;

    public Worker(IServiceProvider serviceProvider,
                    ILogger<Worker> logger) => (_serviceProvider, _logger) = (serviceProvider, logger);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _serviceProvider.CreateScope();
            var weatherApi = scope.ServiceProvider.GetService<IWeatherApi>();

            var result = await weatherApi.GetWeatherAsync(stoppingToken);
            _logger.LogInformation($"Weather: {result}");

            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

public interface IWeatherApi
{
    Task<string> GetWeatherAsync(CancellationToken stoppingToken);
}

public class WeatherApi : IWeatherApi
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching","Fun"
    };

    public Task<string> GetWeatherAsync(CancellationToken stoppingToken)
    {
        var rng = new Random();
        return Task.FromResult(Summaries[rng.Next(Summaries.Length)]);
    }
}

Ordem de execução

Os serviços são inicializados na ordem que são registrados, mas são encerrados na ordem reversa.

Caso registre os serviços ServicoA, ServicoB e ServicoC. Durante a inicialização IHostedService.StartAsync do ServicoA será executado primeiro, mas durante o encerramento o último serviço registrado será executado primeiro, no caso IHostedService.StopAsync do ServicoC.

Worker Service como Windows Service ou Linux Daemon

Um Worker Service pode ser instalado como um Windows Service ou Linux Daemon apenas usando extension methods UseWindowsService para Windows Service ou UseSystemd para Linux Deamon no Host.

É necessário instalar o pacote nuget equivalente para o tipo de ambiente que vai executar;

  • Windows Service: Microsoft.Extensions.Hosting.WindowsService
  • Linux Daemon: Microsoft.Extensions.Hosting.Systemd
Dica

É possível usar UseWindowsService e UseSystemd juntos porque ambos só funcionam baseado no contexto do sistema operacional que estão preparados para funcionar.

C#
IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.Configure<HostOptions>(options =>
        {
            options.ShutdownTimeout = TimeSpan.FromSeconds(6);
        });
        services.AddHostedService<Worker2>();
        services.AddHostedService<Worker>();
    })
    .UseWindowsService() // Windows Service
    .UseSystemd() // Linux Daemon
    .Build();

await host.RunAsync();

Referências

PUBLICADO EM: