Skip to content

Make improvements to signal handling on .NET applications #50527

Closed
@davidfowl

Description

@davidfowl

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions