Search code examples
c#.net-coreasp.net-core-hosted-services

Task.Delay() triggers cancellation of application lifetime


I have faced a situation when Task.Delay() method would trigger an event of cancellation in IApplicationLifetime. Here is the code:

    static async Task Main(string[] args)
    {
        Console.WriteLine("Starting");

       await BuildWebHost(args)
            .RunAsync();
        
        Console.WriteLine("Press any key to exit..");
        Console.ReadKey();
    }

    private static IHost BuildWebHost(string[] args)
    {
        var hostBuilder = new HostBuilder()
            .ConfigureHostConfiguration(config =>
            {
                config.AddEnvironmentVariables();
                config.AddCommandLine(args);
            })
            .ConfigureAppConfiguration((hostContext, configApp) =>
            {
                configApp.SetBasePath(Directory.GetCurrentDirectory());
                configApp.AddCommandLine(args);
            })
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<BrowserWorkerHostedService>();
                services.AddHostedService<EmailWorkerHostedService>();
            })
            .UseConsoleLifetime();

        return hostBuilder.Build();
    }

and here are hosted services which are stopping abnormally:

public class BrowserWorkerHostedService : BackgroundService
{
    private IApplicationLifetime _lifetime;
    private IHost _host;

    public BrowserWorkerHostedService(
        IApplicationLifetime lifetime,
        IHost host)
    {
        this._lifetime = lifetime;
        this._host = host;
    }

    protected override async Task ExecuteAsync(CancellationToken stopToken)
    {
        while (!_lifetime.ApplicationStarted.IsCancellationRequested
            && !_lifetime.ApplicationStopping.IsCancellationRequested
            && !stopToken.IsCancellationRequested)
        {
            Console.WriteLine($"{nameof(BrowserWorkerHostedService)} is working. {DateTime.Now.ToString()}");
            
            //lifetime.StopApplication();
            //await StopAsync(stopToken);

            await Task.Delay(1_000, stopToken);
        }

        Console.WriteLine($"End {nameof(BrowserWorkerHostedService)}");

        await _host.StopAsync(stopToken);
    }
}

public class EmailWorkerHostedService : BackgroundService
{
    private IApplicationLifetime _lifetime;
    private IHost _host = null;

    public EmailWorkerHostedService(
        IApplicationLifetime lifetime,
        IHost host)
    {
        this._lifetime = lifetime;
        this._host = host;
    }

    protected override async Task ExecuteAsync(CancellationToken stopToken)
    {
        while (!_lifetime.ApplicationStarted.IsCancellationRequested
            && !_lifetime.ApplicationStopping.IsCancellationRequested
            && !stopToken.IsCancellationRequested)
        {
            Console.WriteLine($"{nameof(EmailWorkerHostedService)} is working. {DateTime.Now.ToString()}");

            await Task.Delay(1_000, stopToken);
        }

        Console.WriteLine($"End {nameof(EmailWorkerHostedService)}");
        
        await _host.StopAsync(stopToken);
    }
}

I would like my services to be running, unless lifetime.StopApplication() is triggered. However, hosted services are stopped, because lifetime.ApplicationStarted.IsCancellationRequested variable is set to true upon a second itteration.. Even though, in theory, I have no code that explicitly aborts the application.

Log will look like this:

Starting BrowserWorkerHostedService is working. 09.07.2019 17:03:53

EmailWorkerHostedService is working. 09.07.2019 17:03:53

Application started. Press Ctrl+C to shut down.

Hosting environment: Production

Content root path: xxxx

End EmailWorkerHostedService

End BrowserWorkerHostedService

Is there a good explanation why Task.Delay() triggers ApplicationStarted cancellation event?


Solution

  • You are misusing IApplicationLifetime events. Their purpose is to give you the ability to associate some action with them. For example you want to start message queue listening only when you application is fully started. You are going to do it like this:

    _applicationLifetime.ApplicationStarted. Register(StartListenMq);
    

    I think using CancellationTokens here wasn't the best idea, but it the way it was implemented.

    When you want to cancel your HostedService you should check only token received in ExecuteAsync method. The flow will look look this:

    IApplicationLifetime.StopApplication() => will trigger IApplicationLifetime.ApplicationStopping => will trigger IHostedService.StopAsync() => will stopToken

    And now for your question: why it happens on await Task.Delay()? Look again at BackgroundService.StartAsync()

        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;
        }
    

    This code does't await ExecuteAsync. At the moment you call async operation in your code StartAsync() will continue to run. Somewhere in it's callstack it will trigger ApplicationStarted and since you are listening to it you will get _lifetime.ApplicationStarted.IsCancellationRequested = true.