Jim Aho
Jim Aho

Reputation: 11867

Why is IsCancellationRequested not set to true on stopping a BackgroundService in .NET Core 3.1?

I've read most articles I can find about IHostApplicationLifetime and CancellationToken's in .NET Core 3.1, but I cannot find a reason why this is not working.

I have a simple BackgroundService which look like the following:

    public class AnotherWorker : BackgroundService
    {
        private readonly IHostApplicationLifetime _hostApplicationLifetime;

        public AnotherWorker(IHostApplicationLifetime hostApplicationLifetime)
        {
            _hostApplicationLifetime = hostApplicationLifetime;
        }

        public override Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine($"Process id: {Process.GetCurrentProcess().Id}");
            _hostApplicationLifetime.ApplicationStarted.Register(() => Console.WriteLine("Started"));
            _hostApplicationLifetime.ApplicationStopping.Register(() => Console.WriteLine("Stopping"));
            _hostApplicationLifetime.ApplicationStopped.Register(() => Console.WriteLine("Stopped"));

            return Task.CompletedTask;
        }

        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            Console.WriteLine("Executing");
            return Task.CompletedTask;
        }

        public override async Task StopAsync(CancellationToken cancellationToken)
        {
        // This actually prints "Stop. IsCancellationRequested: False". Why?
            Console.WriteLine($"Stop. IsCancellationRequested: {cancellationToken.IsCancellationRequested}");
            await base.StopAsync(cancellationToken);
        }
    }

The ConsoleLifetime is added by default, which listens to Ctrl+C and SIGTERM and informs the IHostApplicationLifetime. I guess IHostApplicationLifetime in turn should then cancel all CancellationTokens? Here's a good article on the subject. So why is the output from the above code snippet the following?

Hosting starting
Started
Hosting started
(sends SIGTERM with `kill -s TERM <process_id>`)
Applicationis shuting down...
Stop. IsCancellationRequested: False
Stopped
Hosting stopped

I would expect it to log Stop. IsCancellationRequested: True

I want to be able to pass this token around, to other service calls, for them to have the capability to shutdown gracefully.

Upvotes: 11

Views: 11439

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456342

There are a lot of different cancellation tokens here, and several different abstractions (IHostApplicationLifetime, IHostedService, BackgroundService). It takes a while to untangle everything. The blog post you linked to is fantastic, but doesn't go into detail on the CancellationTokens.

First, if you're going to use BackgroundService, I recommend reading the code. Also, I strongly recommend not overriding StartAsync and StopAsync; BackgroundService uses these in a very particular way.

IHostedService has two methods. StartAsync starts the service running (possibly asynchronously); it takes a CancellationToken that indicates the "start" operation should be cancelled (I haven't checked, but I assume this token is only triggered if the app is shutdown almost immediately). Note that StartAsync needs to complete before the hosted service is considered in the "started" or "running" state. Similarly, StopAsync stops the service (possibly asynchronously). StopAsync is invoked when the application begins its graceful shutdown. There's a timeout for the graceful shutdown period, after which the application begins its "I'm serious now" shutdown. The CancellationToken for StopAsync represents the transition from "graceful" to "I'm serious now". So it's not set during that graceful shutdown timeout window.

If you use BackgroundService instead of IHostedService directly (like most people do), you get a different CancellationToken in ExecuteAsync. This one is set when BackgroundService.StopAsync is invoked - i.e., when the application has started its graceful shutdown. So it's roughly equivalent to IHostApplicationLifetime.ApplicationStopping, but scoped to a single hosted service. You can expect the BackgroundWorker.ExecuteAsync CancellationToken to be set shortly after IHostApplicationLifetime.ApplicationStopping is set.

Note that all of these CancellationTokens represent something different:

  • IHostedService.StartAsync's CancellationToken means "abort the starting of this service".
  • IHostedService.StopAsync's CancellationToken means "stop this service right now; you're out of the grace period".
  • IHostApplicationLifetime.ApplicationStopping means "the graceful shutdown sequence for this entire application has started; everyone please stop what you are doing".
    • As part of the graceful shutdown sequence, all IHostedService.StopAsync methods are invoked.
  • BackgroundService.ExecuteAsync's CancellationToken means "stop this service".

An interesting note is that BackgroundService types don't normally see the "I'm serious now" signal; they only see the "stop this service" signal. This is likely because the "I'm serious now" signal represented by a CancellationToken is somewhat confusing.

If you look into the code for Host, the shutdown sequence has even more cancellation tokens used in its shutdown sequence:

  1. IHost.StopAsync takes a CancellationToken meaning "the stop should no longer be graceful".
  2. It then starts a CancellationToken-based timeout for the graceful timeout period.
  3. ... and another linked CancellationToken that is fired if either the IHost.StopAsync token is fired or if the timer elapsed. So this one also means "the stop should no longer be graceful".
  4. Next it calls IHostApplicationLifetime.StopApplication, which cancels the IHostApplicationLifetime.ApplicationStopping CancellationToken.
  5. It then invokes StopAsync for each IHostedService, passing the "stop should no longer be graceful" token.
    • All BackgroundService types have their own CancellationToken (which was passed to ExecuteAsync during startup), and those cancellation tokens are cancelled by StopAsync.
  6. Finally, it invokes IHostApplicationLifetime.NotifyStopped, which cancels the IHostApplicationLifetime.ApplicationStopped CancellationToken.

I count 3 for the "no longer graceful" signal (one passed in, one timer, and one linking those two), plus 2 on IHostApplicationLifetime, plus 1 for each BackgroundService, for a total of 5 + n cancellation tokens used during shutdown. :)

Upvotes: 42

spidyx
spidyx

Reputation: 1117

The CancellationToken passed to the StopAsync indicate if the BackgroundService must execute a gracefull shutdown or a hard one.

You stop the process using the kill -s TERM so it send the SIGTERM signal asking the application to shutdown gracefully. Therefore the IsCancellationRequested property is still at false.

To pass a token to other services calls, you have to provide your own CancellationToken. You can use a CancellationTokenSource to manage token creation and cancellation.

Upvotes: 0

Related Questions