Mr. Duc Nguyen
Mr. Duc Nguyen

Reputation: 1069

Windows Service using .net core BackgroundService, how to gracefully stop?

I'm developing a windows service using .Net Core 3.1

Lots of online resource suggesting me to use BackgroundService for it. Problem is I cannot get the service to be stopped gracefully, by just overriding ExecuteAsync method.

The codes setup for testing is very simple:

public class MyService: BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine($"Service started at {DateTimeOffset.Now}");
        await DoLongRunningWork(stoppingToken);
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Service is being stopped...");
        await base.StopAsync(cancellationToken);
        Console.WriteLine("Service stopped at {DateTimeOffset.Now}");
    }

    private static async Task DoLongRunningWork(CancellationToken token)
    {
        Console.WriteLine("I'm working...");
        while (!token.IsCancellationRequested)
        {
            Console.WriteLine($"\t... doing work ...");
            await Task.Delay(1000, token);
        }

        // THE BELOW LINE IS NEVER REACHED. i.e The line "Cancel requested? True" is never logged out...
        Console.WriteLine($"Cancel requested? {token.IsCancellationRequested}");
    }
}

And the Main method


static void Main(string[] args)
{
    Console.WriteLine("Starting My Service...");
    Host.CreateDefaultBuilder(args)
        .UseWindowsService()
        .ConfigureServices(services =>
        {
            services.AddHostedService<MyService>();
        })
        .Build()
        .Run();
}

One thing I can do is implementing my own Stop method and calling it in my override StopAsync. But I'm curious why this simple setup didn't work as expected.

I have read the BackgroundService.cs source code, it looks like it should work the way I expected (i.e. this line Cancel requested? True should be logged out but it didn't...

I have tried both running it as a console app and stopping it using Ctrl+C, as well as installed it as Windows Service and using service.msc to control it. Same behaviour (i.e. no gracefully shutdown...)

Any helps are much appreciated!

Upvotes: 3

Views: 4342

Answers (2)

Jaime Gomez
Jaime Gomez

Reputation: 7067

I was having the same issue while following the official examples, and catching the TaskCanceledException did the trick, here's how my code looks now:

public class Worker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Started");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(60000, stoppingToken);
            }
            catch (TaskCanceledException)
            {
                continue;
            }
        }

        _logger.LogInformation("Stopped");
    }
}

I'm now getting the "Stopped" log :)

Upvotes: 1

Mr. Duc Nguyen
Mr. Duc Nguyen

Reputation: 1069

I forgot about this for such a long time. And when I come back to it, I found out the issue... It was just a confusion

This particular line

await Task.Delay(1000, token);

will throw TaskCancelledException hence the loop will be exited the method will stop... @Jeremy Lakeman did comment about this but I didn't think it through at that time I guessed...

I'll just need to be careful with using await Task.Delay(..., token) with a cancellation token...

So, I created a simple extension

    public static async Task SafeDelay(this Task source)
    {
        try
        {
            await source;
        }
        catch (TaskCanceledException)
        {
            // I don't want a throw on TaskCanceledException...
        }
    }
    private static async Task DoLongRunningWork(CancellationToken token)
    {
        Console.WriteLine("I'm working...");
        while (!token.IsCancellationRequested)
        {
            Console.WriteLine($"\t... doing work ...");
            
            // I simply want the loop to keep running, I don't want an absurd stop...
            await Task.Delay(1000, token).SafeDelay();
        }

        // This line is now reached...
        Console.WriteLine($"Cancel requested? {token.IsCancellationRequested}");
    }

Upvotes: 3

Related Questions