Description
Background and Motivation
Many systems use SIGTERM as a way to signal a graceful shutdown. To make this work well in container scenarios, we need to start the shutdown process and wait for some timeframe until it's done. To coordinate that, Microsoft.Extensions.Hosting sets up a bunch of events to allow the main thread to exit before continuing the process exit handler. Here's what that would look like (without the dependencies):
using System;
using System.Threading;
var waitForProcessShutdownStart = new ManualResetEventSlim();
var waitForMainExit = new ManualResetEventSlim();
AppDomain.CurrentDomain.ProcessExit += (sender, e) =>
{
// We got a SIGTERM, signal that graceful shutdown has started
waitForProcessShutdownStart.Set();
Console.WriteLine("Waiting for main");
// Don't unwind until main exists
waitForMainExit.Wait();
};
Console.WriteLine("Waiting for shutdown");
// Wait for shutdown to start
waitForProcessShutdownStart.Wait();
// This is where the application performs graceful shutdown
// Now we're done with main, tell the shutdown handler
waitForMainExit.Set();
The above code shows how a user could set this up today. It's much more complex than the windows equivalent (handling Ctrl+C) for a couple of reasons:
- SIGTERM is the signal used to communicate starting a graceful shutdown, this means we need to stop the app from shutting down until some point.
- We want the main thread to continue executing for as long as it can until it has unwound. This lets us run any clean up code or logging before exit (see IHost.RunAsync() never completes #44086)
- It can result in deadlocks if someone calls
Environment.Exit
at the wrong point in the application.
This is what waiting for CTRL+C looks like:
var waitForProcessShutdownStart = new ManualResetEventSlim();
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true;
waitForProcessShutdownStart.Set();
};
Console.WriteLine("Waiting for shutdown");
// Wait for shutdown to start
waitForProcessShutdownStart.Wait();
// Do graceful shutdown here
The runtime itself handles e.Cancel = true
and doesn't shut down the process after the event handler runs. Instead the application can use this to coordinate letting the application gracefully unwind from the main thread.
The request here is to treat SIGTERM like Ctrl+C and support e.Cancel.
cc @janvorli @stephentoub @kouvel
Proposed API
namespace System.Runtime.InteropServices
{
+ public enum Signal // This may need more unique name
+ {
+ SIGHUP = 1,
+ SIGINT = 2,
+ SIGQUIT = 3,
+ SIGTERM = 15,
+ }
+ public class SignalContext
+ {
+ public Signal Signal { get; }
+ public bool Cancel { get; set; }
+ }
+ public struct SignalHandlerRegistration : IDisposable
+ {
+ public static SignalHandlerRegistration Create(Signal signal, Action<SignalContext> handler);
+ }
}
NOTES:
SignalContext.Signal
provides the signals that fired to cause the event to trigger so that can be checked in the callback to take action if the same handler was used for different registrations.SignalContext.Cancel
cancels default processing.
We will map windows behavior to linux signal names:
- CTRL_C_EVENT = SIGINT
- CTRL_BREAK_EVENT = SIGINT
- CTRL_SHUTDOWN_EVENT - SIGTERM
- CTRL_CLOSE_EVENT - SIGTERM
- CTRL_LOGOFF_EVENT - Not mapped
Usage Examples
using var reg = SignalHandlerRegistration.Register(Signal.SIGINT, context =>
{
context.Cancel = true;
// Start graceful shutdown
});
Waiting synchronously on shutdown to start:
var waitForProcessShutdownStart = new ManualResetEventSlim();
using var reg = SignalHandlerRegistration.Register(Signal.SIGINT, context =>
{
context.Cancel = true;
waitForProcessShutdownStart.Set();
});
Console.WriteLine("Waiting for shutdown");
// Wait for shutdown to start
waitForProcessShutdownStart.Wait();
The hosting model in Microsoft.Extensions.Hosting will look like this in .NET 6:
public class ConsoleLifetime : IHostLifetime, IDisposable
{
...
private IHostApplicationLifetime ApplicationLifetime { get; }
private SignalHandlerRegistration _sigterm;
private SignalHandlerRegistration _sigInt;
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
Action<SignalContext> handler = OnShutdownSignal;
_sigInt = SignalHandlerRegistration.Register(Signal.SIGINT, handler);
_sigterm = SignalHandlerRegistration.Register(Signal.SIGTERM, handler);
// Console applications start immediately.
return Task.CompletedTask;
}
private void OnShutdownSignal(SignalContext context)
{
context.Cancel = true;
ApplicationLifetime.StopApplication();
}
public void Dispose()
{
_sigterm.Dispose();
_sigInt.Dispose();
}
...
}
Alternative Designs
Don't add new API but overload the existing Console.CancelKeyPress. This would cover one specific scenario but wouldn't handle the arbitrary signals.
Risks
None