Michael Sandino
Michael Sandino

Reputation: 1958

Hosted service not terminating after Environment.Exit

I've got a .NET core 3.1 app with a hosted service that runs as a console application on Windows.

In case of an error I'm trying to terminate the worker with Environment.Exit(1).

Now the problem is that, if Enviroment.Exit() is called before any await in ExecuteAsync, the application does not terminate. It logs Waiting for the host to be disposed. Ensure all 'IHost' instances are wrapped in 'using' blocks. and then hangs indefinitely.
When I await anything before the call to Enviroment.Exit() it also logs that, but it terminates as expected.

Here is the simplest code that I could come up with to reproduce the problem.
The NotTerminatingWorker hangs forever, the TerminatingWorker terminates. The only difference is a tiny Task.Delay:


  public class Program {
    public static async Task Main(string[] args) {
      using var host = CreateHostBuilder(args).Build();
      await host.RunAsync();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) {
      return Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) => { services.AddHostedService<NotTerminatingWorker>(); });
    }
  }


  public class NotTerminatingWorker : BackgroundService {
    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
      Environment.Exit(1);
    }
  }

  public class TerminatingWorker : BackgroundService {
    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
      await Task.Delay(1);
      Environment.Exit(1);
    }
  }

I would expect that both behave the same way, but that's obviously not the case.
Any explanation for this would be greatly appreciated!

UPDATE: The application should be able to run both as a console application and as a Windows service. The non-zero return code is required to get it restarted if it crashes. And apparently Windows does not restart services that exited with code 0.

Upvotes: 11

Views: 9100

Answers (2)

Mario
Mario

Reputation: 1

Handling the hostLifetime events in the Main method did for me the job. This is working for me on .NET6

    public static int Main(string[] args)
    {
        ExitCode = 0;
        ILogger? logger = null;
        try
        {
            var builder = CreateHostBuilder(args)
                .Build();
            var hostLifetime = builder.Services.GetRequiredService<IHostApplicationLifetime>();
            logger = builder.Services.GetService<ILogger<Program>>();

            // register on hostLifetime events for handling stopping and finalize
            using var hostLtAppStopping = hostLifetime.ApplicationStopping.Register(() =>
            {
                // service is about to stop... do some cleanup stuff here
            });
            using var hostLtAppStopped = hostLifetime.ApplicationStopped.Register(() =>
            {
                 logger?.LogDebug("Service graceful shout down, exit with code {exitCode}!", ExitCode);
                Environment.Exit(ExitCode); // ExitCode is set by the caller of hostApplicationLifetime.StopApplication
            });

            // start the service
            logger?.LogDebug("builder.Run()"); 
            builder.Run();

        }
        catch (Exception e)
        {
            logger?.LogError(e, "Unhandled Exception occurred => exit with exit code 1!");
            ExitCode = 1;
            return ExitCode;
        }
        return ExitCode;  
    }

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 457057

I believe the behavior you're seeing is a side-effect of how the .NET Core runtime does its startup: it calls ExecuteAsync for each background worker and then waits for it to complete. So a synchronous ExecuteAsync can cause problems. I've used Task.Run to work around this.

In case of an error I'm trying to terminate the worker with Environment.Exit(1).

I recommend not using Environment.Exit at all. Instead, do a controlled shutdown by injecting IHostApplicationLifetime and calling StopApplication. This will trigger the stoppingToken for each of your background services, and if they ignore it, they will be forcibly terminated after a timeout.

Upvotes: 22

Related Questions