Search code examples
c#.netbackground-service

BackgroundService Graceful Shutdown - Complete work and write to DB


I have a Background Worker implementing the BackgroundService (provided by MS).

See this simple implementation:

public class MyService : BackgroundService {

    private readonly MyDbContext _context;

    public MyService(MyDbContext context) {
        //...
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try {
            while (true)
            {
                stoppingToken.ThrowIfCancellationRequested();
                // Do some work
            }
        } catch(OperationCancelledException) {
            _context.Add(new MyLogMessage(){ Error = "MyService cancelled!" });
            _context.SaveChanges();
        }
        // ...
    }
}

When the graceful shutdown (in console: CTRL+C) is requested the catch block is triggered, and also the SaveChanges() seems to be executed. But, sometimes the error is stored into the database and the most of the time it is not. Also the EntityFramework is printing an insert statement on the console, but the log is not in the db. I assume that the shutdown is happening faster then writting the data to the DB? Can anyone give me a hint how to handle this situation and store the error into the database?


Solution

  • It seems like the stoppingToken isn't cancelled as expected when the application shuts down. I managed to get around this using IHostApplicationLifetime and a new field where I can store if a shutdown is in progress.

    public class TestService : BackgroundService {
        private readonly IHostApplicationLifetime _lifetime;
        private readonly ILogger<TestService> _logger;
    
        private bool _shutownRequested;
    
        public TestService(IHostApplicationLifetime lifetime, ILogger<TestService> logger) {
            _lifetime = lifetime;
            _logger = logger;
        }
    
        public override Task StartAsync(CancellationToken cancellationToken) {
            _lifetime.ApplicationStopping.Register(OnShutdown);
            return Task.CompletedTask;
        }
    
        private void OnShutdown() {
            _shutdownRequested = true;
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
            try {
                while(true) {
                    stoppingToken.ThrowIfCancellationRequested();
                    if(_shutdownRequested) {
                        throw new OperationCanceledException();
                    }
    
                    await Task.Delay(100, CancellationToken.None);
                }
            } catch(OperationCanceledException) {
                _logger.LogWarning("TestService canceled");
            }
        }
    }
    

    Now it might be better to now throw a new exception there, but as an example it will do.