8

I have a default generic host builder as below:

public static IHostBuilder CreateDefaultBuilder(string[] args)
{
    var builder = Host.CreateDefaultBuilder(args);

    builder
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.ClearProviders();
            logging.AddConsole();
            if (hostingContext.HostingEnvironment.IsDevelopment() == true)
                logging.AddDebug();
        })
        .ConfigureHostConfiguration(configurationBuilder =>
        {
            configurationBuilder.AddCommandLine(args);
        })
        .ConfigureAppConfiguration((hostingContext, configApp) =>
        {
            var env = hostingContext.HostingEnvironment;
            Console.WriteLine(env.EnvironmentName);
        })
        .UseConsoleLifetime();

    return builder;
}

public static async Task Main(string[] args)
{
     var host = CreateDefaultBuilder(args)
                        .ConfigureServices((hostContext, services) =>
                        {
                            services.Configure<HostOptions>(option =>
                            {
                                option.ShutdownTimeout = System.TimeSpan.FromSeconds(20);
                            });

                            services.AddLogging();

                            services.AddSimpleInjector(container, options =>
                            {
                                // Hooks hosted services into the Generic Host pipeline while resolving them through Simple Injector
                                options.AddHostedService<Worker>();

                                // Allows injection of ILogger & IStringLocalizer dependencies into application components
                                options.AddLogging();
                                // options.AddLocalization();
                            });
                        })
                        .Build()
                        .UseSimpleInjector(container);

     config.Build();
     await host.RunAsync();
}

I am using UseConsoleLifetime and docker to run the application, it runs, but i do not seem to be able to gracefully shutdown.

I gracefully send a stop signal to my application via the docker stop command in PowerShell.

However, unlike my console app where i press control+C, the app doesn't get this stop command and after 10 seconds is killed forcibly.

My questions are please:

  1. How can i detect the docker shutdown command?
  2. How can i register both methods of shutdown (via console or docker) depending on the method by which i am calling (at times I may debug directly in Visual Studio as a console, whereas mostly i would use via docker, but would be nice to auto-register for both scenarios).

EDIT AND UPDATE

I have seen replies, thank you, mentioning to trap on AppDomain.CurrentDomain.ProcessExit.

For my worker BackgroundService the interface is such that it has the function:

public class Worker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
             logger.LogInformation("running at: {time} - stoppingToken {stoppingToken}", DateTimeOffset.Now, stoppingToken.IsCancellationRequested);
             await Task.Delay(1000, stoppingToken);
        }
    }
}

Using the UseConsoleLifetime whenever ctrl+c is sent, the stoppingToken is set.

When using the same code (including UseConsoleLifetime same code as is) within docker, the stoppingToken is not set.

Note i cant see that i have access to this token source in order to perform the cancellation.

If i register to AppDomain.CurrentDomain.ProcessExit how can i use this to set the stoppingToken (as opposed to making my own stopping token source, as whilst can be done, would like to avoid if possible).

docker stop

morleyc
  • 2,169
  • 10
  • 48
  • 108
  • Have you tried [this](https://stackoverflow.com/a/47474602/2501279)? – Guru Stron Jul 08 '20 at 20:38
  • Looks good thank you, `Domain.CurrentDomain.ProcessExit` can that run in parallel with `UseConsoleLifetime()` or any specific way to detect if running as console or via docker to account for either registration? – morleyc Jul 08 '20 at 20:39
  • Not sure, but you can try subscribing to `System.Console.CancelKeyPress` event also. – Guru Stron Jul 08 '20 at 20:41
  • You don't need to register for `ProcessExit` or any other event. `UseConsoleLifetime()` listens for Ctrl+C, SIGINT and SIGTERM - it already monitors process termination. When you run `docker stop` the SIGTERM is sent to the root process of the container. Is that your application or another one? How do you build and run the host? `.Build().RunAsync()` ? Something else? – Panagiotis Kanavos Jul 08 '20 at 20:53
  • What does your BackgroundService code look like? When termination is signalled, the `CancellationToken` passed through `ExecuteAsync` is signalled. Your code should monitor it and exit quickly if it's raised – Panagiotis Kanavos Jul 08 '20 at 20:58
  • @PanagiotisKanavos it was indeed with `await Build().RunAsync()` have updated my question to reflect the code. With RunAsync and also using `void Main()` and using `host.Run()`, unfortunatly with my code i do not get the stoppingToken set (where as the same code when run as a console works ok) – morleyc Jul 09 '20 at 13:09

1 Answers1

5

docker stop sends the SIGTERM signal to the primary process in the container. To handle this and gracefully shutdown, you need to trap this signal and execute the appropriate termination code. This SO question describes how you can attach an event handler that responds to this signal.

Note that if you don't trap this signal, docker will forcibly kill the container after a timeout (10 seconds by default).

To handle both, you can extract any clean up code you run to gracefully shutdown to a function, and invoke it from both handlers.

Orphid
  • 2,722
  • 2
  • 27
  • 41
  • Thanks Orphid, once that signal is trapped any idea on how I can set the CancellationToken stoppingToken that is passed into the worker run function? – morleyc Jul 17 '20 at 13:37