Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consume PosixSignal in Hosting's ConsoleLifetime #56057

Merged
merged 5 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Hosting.Systemd;
using Xunit;

namespace Microsoft.Extensions.Hosting
Expand All @@ -19,7 +19,8 @@ public void DefaultsToOffOutsideOfService()
using (host)
{
var lifetime = host.Services.GetRequiredService<IHostLifetime>();
Assert.IsType<ConsoleLifetime>(lifetime);
Assert.NotNull(lifetime);
Assert.IsNotType<SystemdLifetime>(lifetime);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
<Import Project="..\Directory.Build.props" />
<PropertyGroup>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<IncludePlatformAttributes>true</IncludePlatformAttributes>
</PropertyGroup>
</Project>
</Project>
68 changes: 68 additions & 0 deletions src/libraries/Microsoft.Extensions.Hosting/docs/HostShutdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Host Shutdown

A Hosted Service process can be stopped in the following ways:

1. If someone doesn't call `Run` or `WaitForShutdown` and the app exits normally with "Main" completing
2. A crash occurs
3. The app is forcefully shut down, e.g. SIGKILL (i.e. CTRL+Z)
4. When ConsoleLifetime is used it listens for the following signals and attempts to stop the host gracefully
1. SIGINT (i.e. CTRL+C)
2. SIGQUIT (i.e. CTRL+BREAK on Windows, CTRL+`\` on Unix)
3. SIGTERM (sent by other apps, e.g. `docker stop`)
5. The app calls `Environment.Exit(code)`

Scenarios (1), (2), and (3) aren't handled directly by the Hosting code. The owner of the process needs to deal with
them the same as any application.

Scenarios (4) and (5) are handled specially by the built-in Hosting logic. Specifically by the ConsoleLifetime
class. ConsoleLifetime tries to handle the "shutdown" signals listed in (4) and allow for a graceful exit to the
application.

Before .NET 6, there wasn't a way for .NET code to gracefully handle SIGTERM. To work around this limitation,
ConsoleLifetime would subscribe to `AppDomain.CurrentDomain.ProcessExit`. When `ProcessExit` was raised,
ConsoleLifetime would signal the Host to stop, and block the `ProcessExit` thread, waiting for the Host to stop.
This would allow for the clean up code in the application to run - for example, `IHostedService.StopAsync` and code after
`Host.Run` in the Main method.

This caused other issues because SIGTERM wasn't the only way `ProcessExit` was raised. It is also raised by code
in the application calling `Environment.Exit`. `Environment.Exit` isn't a graceful way of shutting down a process.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
in the application calling `Environment.Exit`. `Environment.Exit` isn't a graceful way of shutting down a process.
in the application calling `Environment.Exit`. `Environment.Exit` isn't a graceful way of shutting down a process in the `Microsoft.Extensions.Hosting` app model.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a graceful way of shutting down a process in any app model?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment.Exit is a graceful program termination from the runtime point of view.

App models typically want to wrap exit with their own logic. I agree that it is probably not considered the right way to exit the process in any app model, except the "no app model" simple apps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll fix this up in a follow up PR tomorrow so I don’t need to reset CI.

It raises the `ProcessExit` event and then exits the process. The end of the Main method doesn't get executed. However,
since ConsoleLifetime blocked `ProcessExit` waiting for the Host to shutdown, this behavior also lead to [deadlocks][deadlocks]
due to `Environment.Exit` also blocking waiting for `ProcessExit` to return. Additionally, since SIGTERM handling was attempting
to gracefully shut down the process, ConsoleLifetime would set the ExitCode to `0`, which [clobbered][clobbered] the user's
exit code passed to `Environment.Exit`.

[deadlocks]: https://github.com/dotnet/runtime/issues/50397
[clobbered]: https://github.com/dotnet/runtime/issues/42224

In .NET 6, we added new support to handle [POSIX signals][POSIX signals]. This allows for ConsoleLifetime to specifically
handle SIGTERM gracefully, and no longer get involved when `Environment.Exit` is invoked. For .NET 6+, ConsoleLifetime no longer
has logic to handle scenario (5) above. Apps that call `Environment.Exit`, and need to do clean up logic, can subscribe to
`ProcessExit` themselves. Hosting will no longer attempt to gracefully stop the Host in this scenario.

[POSIX signals]: https://github.com/dotnet/runtime/issues/50527

### Hosting Shutdown Process

The following sequence model shows how the signals in (4) above are handled internally in the Hosting code. It isn't necessary
for the majority of users to understand this process. But for developers that need a deep understanding, this may help them
get started.

After the Host has been started, when a user calls `Run` or `WaitForShutdown`, a handler gets registered for
`ApplicationLifetime.ApplicationStopping`. Execution is paused in `WaitForShutdown`, waiting for the `ApplicationStopping`
event to be raised. This is how the "Main" method doesn't return right away, and the app stays running until
`Run`/`WaitForShutdown` returns.

When a signal is sent to the process, it kicks off the following sequence.

![image](images/HostShutdownSequence.png)

The control flows from `ConsoleLifetime` to `ApplicationLifetime` to raise the `ApplicationStopping` event. This signals
`WaitForShutdownAsync` to unblock the "Main" execution code. In the meantime, the POSIX signal handler returns with
`Cancel = true` since this POSIX signal has been handled.

The "Main" execution code starts executing again and tells the Host to `StopAsync()`, which in turn stops all the Hosted
Services and raises any other stopped events.

Finally, `WaitForShutdown` exits, allowing for any application clean up code to execute, and for the "Main" method
to exit gracefully.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,29 @@ public static partial class HostingHostBuilderExtensions
public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureLogging(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Hosting.HostBuilderContext, Microsoft.Extensions.Logging.ILoggingBuilder> configureLogging) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureLogging(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Logging.ILoggingBuilder> configureLogging) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder ConfigureServices(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.DependencyInjection.IServiceCollection> configureDelegate) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public static System.Threading.Tasks.Task RunConsoleAsync(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Hosting.ConsoleLifetimeOptions> configureOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public static System.Threading.Tasks.Task RunConsoleAsync(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public static Microsoft.Extensions.Hosting.IHostBuilder UseConsoleLifetime(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public static Microsoft.Extensions.Hosting.IHostBuilder UseConsoleLifetime(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.Hosting.ConsoleLifetimeOptions> configureOptions) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder UseContentRoot(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, string contentRoot) { throw null; }
public static Microsoft.Extensions.Hosting.IHostBuilder UseDefaultServiceProvider(this Microsoft.Extensions.Hosting.IHostBuilder hostBuilder, System.Action<Microsoft.Extensions.DependencyInjection.ServiceProviderOptions> configure) { throw null; }
Expand All @@ -78,6 +98,11 @@ public void NotifyStarted() { }
public void NotifyStopped() { }
public void StopApplication() { }
}
[System.Runtime.Versioning.UnsupportedOSPlatform("android")]
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
[System.Runtime.Versioning.UnsupportedOSPlatform("ios")]
[System.Runtime.Versioning.UnsupportedOSPlatform("maccatalyst")]
[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
public partial class ConsoleLifetime : Microsoft.Extensions.Hosting.IHostLifetime, System.IDisposable
{
public ConsoleLifetime(Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Hosting.ConsoleLifetimeOptions> options, Microsoft.Extensions.Hosting.IHostEnvironment environment, Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Hosting.HostOptions> hostOptions) { }
Expand Down
6 changes: 4 additions & 2 deletions src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace Microsoft.Extensions.Hosting
/// <summary>
/// A program initialization utility.
/// </summary>
public class HostBuilder : IHostBuilder
public partial class HostBuilder : IHostBuilder
{
private List<Action<IConfigurationBuilder>> _configureHostConfigActions = new List<Action<IConfigurationBuilder>>();
private List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppConfigActions = new List<Action<HostBuilderContext, IConfigurationBuilder>>();
Expand Down Expand Up @@ -245,7 +245,9 @@ private void CreateServiceProvider()
services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
services.AddSingleton<IHostLifetime, ConsoleLifetime>();

AddLifetime(services);

services.AddSingleton<IHost>(_ =>
{
return new Internal.Host(_appServices,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting.Internal;

namespace Microsoft.Extensions.Hosting
{
public partial class HostBuilder
{
private static void AddLifetime(ServiceCollection services)
{
if (!OperatingSystem.IsAndroid() && !OperatingSystem.IsBrowser() && !OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst() && !OperatingSystem.IsTvOS())
{
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
}
else
{
services.AddSingleton<IHostLifetime, NullLifetime>();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting.Internal;

namespace Microsoft.Extensions.Hosting
{
public partial class HostBuilder
{
private static void AddLifetime(ServiceCollection services)
{
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -225,7 +226,12 @@ public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[]
})
.ConfigureLogging((hostingContext, logging) =>
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
bool isWindows =
#if NET6_0_OR_GREATER
OperatingSystem.IsWindows();
#else
RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif

// IMPORTANT: This needs to be added *before* configuration is loaded, this lets
// the defaults be overridden by the configuration.
Expand All @@ -236,7 +242,12 @@ public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[]
}

logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
#if NET6_0_OR_GREATER
if (!OperatingSystem.IsBrowser())
eerhardt marked this conversation as resolved.
Show resolved Hide resolved
#endif
{
logging.AddConsole();
}
logging.AddDebug();
logging.AddEventSourceLogger();

Expand Down Expand Up @@ -274,6 +285,11 @@ public static IHostBuilder ConfigureDefaults(this IHostBuilder builder, string[]
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("tvos")]
public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices(collection => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());
Expand All @@ -286,6 +302,11 @@ public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <param name="configureOptions">The delegate for configuring the <see cref="ConsoleLifetime"/>.</param>
/// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("tvos")]
public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder, Action<ConsoleLifetimeOptions> configureOptions)
{
return hostBuilder.ConfigureServices(collection =>
Expand All @@ -301,6 +322,11 @@ public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder, Act
/// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the console.</param>
/// <returns>A <see cref="Task"/> that only completes when the token is triggered or shutdown is triggered.</returns>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("tvos")]
public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
{
return hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken);
Expand All @@ -313,6 +339,11 @@ public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationTo
/// <param name="configureOptions">The delegate for configuring the <see cref="ConsoleLifetime"/>.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the console.</param>
/// <returns>A <see cref="Task"/> that only completes when the token is triggered or shutdown is triggered.</returns>
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("maccatalyst")]
[UnsupportedOSPlatform("tvos")]
public static Task RunConsoleAsync(this IHostBuilder hostBuilder, Action<ConsoleLifetimeOptions> configureOptions, CancellationToken cancellationToken = default)
{
return hostBuilder.UseConsoleLifetime(configureOptions).Build().RunAsync(cancellationToken);
Expand Down
Loading