From 05d1a6eb0e051a0cbadc64246f77fe9b836d33a6 Mon Sep 17 00:00:00 2001 From: "Chris Ross (ASP.NET)" Date: Wed, 14 Mar 2018 16:23:50 -0700 Subject: [PATCH] Refactor Generic Host lifetimes to work better with ServiceBase #1347 --- .../GenericHostSample/ServiceBaseLifetime.cs | 43 +++++++---- .../IHostLifetime.cs | 15 +--- .../HostBuilder.cs | 2 +- .../Internal/ConsoleLifetime.cs | 15 ++-- .../Internal/Host.cs | 8 +- .../Internal/ProcessLifetime.cs | 28 ------- .../Fakes/FakeHostLifetime.cs | 15 +--- .../HostTests.cs | 74 ++----------------- 8 files changed, 49 insertions(+), 151 deletions(-) delete mode 100644 src/Microsoft.Extensions.Hosting/Internal/ProcessLifetime.cs diff --git a/samples/GenericHostSample/ServiceBaseLifetime.cs b/samples/GenericHostSample/ServiceBaseLifetime.cs index d5625a4671..df5c8368e9 100644 --- a/samples/GenericHostSample/ServiceBaseLifetime.cs +++ b/samples/GenericHostSample/ServiceBaseLifetime.cs @@ -23,23 +23,35 @@ namespace GenericHostSample public class ServiceBaseLifetime : ServiceBase, IHostLifetime { - private Action _startCallback; - private Action _stopCallback; - private object _startState; - private object _stopState; + private TaskCompletionSource _delayStart = new TaskCompletionSource(); - public void RegisterDelayStartCallback(Action callback, object state) + public ServiceBaseLifetime(IApplicationLifetime applicationLifetime) { - _startCallback = callback ?? throw new ArgumentNullException(nameof(callback)); - _startState = state; - - Run(this); + ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime)); } - public void RegisterStopCallback(Action callback, object state) + private IApplicationLifetime ApplicationLifetime { get; } + + public Task WaitForStartAsync(CancellationToken cancellationToken) { - _stopCallback = callback ?? throw new ArgumentNullException(nameof(callback)); - _stopState = state; + cancellationToken.Register(() => _delayStart.TrySetCanceled()); + ApplicationLifetime.ApplicationStopping.Register(Stop); + + new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing. + return _delayStart.Task; + } + + private void Run() + { + try + { + Run(this); // This blocks until the service is stopped. + _delayStart.TrySetException(new InvalidOperationException("Stopped without starting")); + } + catch (Exception ex) + { + _delayStart.TrySetException(ex); + } } public Task StopAsync(CancellationToken cancellationToken) @@ -48,15 +60,18 @@ namespace GenericHostSample return Task.CompletedTask; } + // Called by base.Run when the service is ready to start. protected override void OnStart(string[] args) { - _startCallback(_startState); + _delayStart.TrySetResult(null); base.OnStart(args); } + // Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync. + // That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion. protected override void OnStop() { - _stopCallback(_stopState); + ApplicationLifetime.StopApplication(); base.OnStop(); } } diff --git a/src/Microsoft.Extensions.Hosting.Abstractions/IHostLifetime.cs b/src/Microsoft.Extensions.Hosting.Abstractions/IHostLifetime.cs index ee89a3bd23..129fb44c8a 100644 --- a/src/Microsoft.Extensions.Hosting.Abstractions/IHostLifetime.cs +++ b/src/Microsoft.Extensions.Hosting.Abstractions/IHostLifetime.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; @@ -10,20 +9,10 @@ namespace Microsoft.Extensions.Hosting public interface IHostLifetime { /// - /// Called at the start of which will wait until the callback is invoked before + /// Called at the start of which will wait until it's compete before /// continuing. This can be used to delay startup until signaled by an external event. /// - /// A callback that will be invoked when the host should continue. - /// State to pass to the callback. - void RegisterDelayStartCallback(Action callback, object state); - - /// - /// Called at the start of to register the given callback for initiating the - /// application shutdown process. - /// - /// A callback to invoke when an external signal indicates the application should stop. - /// State to pass to the callback. - void RegisterStopCallback(Action callback, object state); + Task WaitForStartAsync(CancellationToken cancellationToken); /// /// Called from to indicate that the host as stopped and clean up resources. diff --git a/src/Microsoft.Extensions.Hosting/HostBuilder.cs b/src/Microsoft.Extensions.Hosting/HostBuilder.cs index 1d7208ba1e..bb6fdec478 100644 --- a/src/Microsoft.Extensions.Hosting/HostBuilder.cs +++ b/src/Microsoft.Extensions.Hosting/HostBuilder.cs @@ -178,7 +178,7 @@ namespace Microsoft.Extensions.Hosting services.AddSingleton(_hostBuilderContext); services.AddSingleton(_appConfiguration); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddOptions(); services.AddLogging(); diff --git a/src/Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs b/src/Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs index 05197d171f..a5160589b3 100644 --- a/src/Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs +++ b/src/Microsoft.Extensions.Hosting/Internal/ConsoleLifetime.cs @@ -26,7 +26,7 @@ namespace Microsoft.Extensions.Hosting.Internal private IApplicationLifetime ApplicationLifetime { get; } - public void RegisterDelayStartCallback(Action callback, object state) + public Task WaitForStartAsync(CancellationToken cancellationToken) { if (!Options.SuppressStatusMessages) { @@ -38,18 +38,15 @@ namespace Microsoft.Extensions.Hosting.Internal }); } - // Console applications start immediately. - callback(state); - } - - public void RegisterStopCallback(Action callback, object state) - { - AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => callback(state); + AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => ApplicationLifetime.StopApplication(); Console.CancelKeyPress += (sender, e) => { e.Cancel = true; - callback(state); + ApplicationLifetime.StopApplication(); }; + + // Console applications start immediately. + return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) diff --git a/src/Microsoft.Extensions.Hosting/Internal/Host.cs b/src/Microsoft.Extensions.Hosting/Internal/Host.cs index ce7d531dc7..451d0fb103 100644 --- a/src/Microsoft.Extensions.Hosting/Internal/Host.cs +++ b/src/Microsoft.Extensions.Hosting/Internal/Host.cs @@ -36,13 +36,9 @@ namespace Microsoft.Extensions.Hosting.Internal { _logger.Starting(); - var delayStart = new TaskCompletionSource(); - cancellationToken.Register(obj => ((TaskCompletionSource)obj).TrySetCanceled(), delayStart); - _hostLifetime.RegisterDelayStartCallback(obj => ((TaskCompletionSource)obj).TrySetResult(null), delayStart); - _hostLifetime.RegisterStopCallback(obj => (obj as IApplicationLifetime)?.StopApplication(), _applicationLifetime); - - await delayStart.Task; + await _hostLifetime.WaitForStartAsync(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); _hostedServices = Services.GetService>(); foreach (var hostedService in _hostedServices) diff --git a/src/Microsoft.Extensions.Hosting/Internal/ProcessLifetime.cs b/src/Microsoft.Extensions.Hosting/Internal/ProcessLifetime.cs deleted file mode 100644 index e8ba9b3942..0000000000 --- a/src/Microsoft.Extensions.Hosting/Internal/ProcessLifetime.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.Hosting.Internal -{ - public class ProcessLifetime : IHostLifetime - { - public void RegisterDelayStartCallback(Action callback, object state) - { - // Never delays start. - callback(state); - } - - public void RegisterStopCallback(Action callback, object state) - { - AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => callback(state); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostLifetime.cs b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostLifetime.cs index 2e998dc708..35ed57a9a2 100644 --- a/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostLifetime.cs +++ b/test/Microsoft.Extensions.Hosting.Tests/Fakes/FakeHostLifetime.cs @@ -10,23 +10,16 @@ namespace Microsoft.Extensions.Hosting.Tests.Fakes public class FakeHostLifetime : IHostLifetime { public int StartCount { get; internal set; } - public int StoppingCount { get; internal set; } public int StopCount { get; internal set; } - public Action, object> StartAction { get; set; } - public Action, object> StoppingAction { get; set; } + public Action StartAction { get; set; } public Action StopAction { get; set; } - public void RegisterDelayStartCallback(Action callback, object state) + public Task WaitForStartAsync(CancellationToken cancellationToken) { StartCount++; - StartAction?.Invoke(callback, state); - } - - public void RegisterStopCallback(Action callback, object state) - { - StoppingCount++; - StoppingAction?.Invoke(callback, state); + StartAction?.Invoke(cancellationToken); + return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) diff --git a/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs b/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs index 83166d6360..5f6b5db76e 100644 --- a/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs +++ b/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs @@ -233,11 +233,10 @@ namespace Microsoft.Extensions.Hosting }); services.AddSingleton(_ => new FakeHostLifetime() { - StartAction = (callback, state) => + StartAction = ct => { lifetimeStart.Set(); Assert.True(lifetimeContinue.WaitOne(TimeSpan.FromSeconds(5))); - callback(state); } }); }) @@ -259,7 +258,6 @@ namespace Microsoft.Extensions.Hosting lifetime = (FakeHostLifetime)host.Services.GetRequiredService(); Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); Assert.Equal(0, lifetime.StopCount); } @@ -268,7 +266,6 @@ namespace Microsoft.Extensions.Hosting Assert.Equal(1, service.DisposeCount); Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); Assert.Equal(0, lifetime.StopCount); } @@ -292,9 +289,10 @@ namespace Microsoft.Extensions.Hosting }); services.AddSingleton(_ => new FakeHostLifetime() { - StartAction = (callback, state) => + StartAction = ct => { lifetimeStart.Set(); + WaitHandle.WaitAny(new[] { lifetimeContinue, ct.WaitHandle }); } }); }) @@ -308,7 +306,7 @@ namespace Microsoft.Extensions.Hosting Assert.False(serviceStarting.WaitOne(0)); cts.Cancel(); - await Assert.ThrowsAsync(() => startTask); + await Assert.ThrowsAsync(() => startTask); Assert.False(serviceStarting.WaitOne(0)); lifetimeContinue.Set(); @@ -322,7 +320,6 @@ namespace Microsoft.Extensions.Hosting lifetime = (FakeHostLifetime)host.Services.GetRequiredService(); Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); Assert.Equal(0, lifetime.StopCount); } @@ -331,61 +328,6 @@ namespace Microsoft.Extensions.Hosting Assert.Equal(1, service.DisposeCount); Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); - Assert.Equal(0, lifetime.StopCount); - } - - [Fact] - public async Task HostLifetimeOnStoppingTriggersIApplicationLifetime() - { - var lifetimeRegistered = new ManualResetEvent(false); - Action stoppingAction = null; - object stoppingState = null; - FakeHostedService service; - FakeHostLifetime lifetime; - using (var host = CreateBuilder() - .ConfigureServices((services) => - { - services.AddSingleton(); - services.AddSingleton(_ => new FakeHostLifetime() - { - StartAction = (callback, state) => callback(state), - StoppingAction = (callback, state) => - { - stoppingAction = callback; - stoppingState = state; - lifetimeRegistered.Set(); - } - }); - }) - .Build()) - { - await host.StartAsync(); - Assert.True(lifetimeRegistered.WaitOne(0)); - - var appLifetime = host.Services.GetRequiredService(); - - stoppingAction(stoppingState); - - Assert.True(appLifetime.ApplicationStopping.WaitHandle.WaitOne(TimeSpan.FromSeconds(5))); - - service = (FakeHostedService)host.Services.GetRequiredService(); - Assert.Equal(1, service.StartCount); - Assert.Equal(0, service.StopCount); - Assert.Equal(0, service.DisposeCount); - - lifetime = (FakeHostLifetime)host.Services.GetRequiredService(); - Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); - Assert.Equal(0, lifetime.StopCount); - } - - Assert.Equal(1, service.StartCount); - Assert.Equal(0, service.StopCount); - Assert.Equal(1, service.DisposeCount); - - Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); Assert.Equal(0, lifetime.StopCount); } @@ -398,10 +340,7 @@ namespace Microsoft.Extensions.Hosting .ConfigureServices((services) => { services.AddSingleton(); - services.AddSingleton(_ => new FakeHostLifetime() - { - StartAction = (callback, state) => callback(state), - }); + services.AddSingleton(); }) .Build()) { @@ -414,7 +353,6 @@ namespace Microsoft.Extensions.Hosting lifetime = (FakeHostLifetime)host.Services.GetRequiredService(); Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); Assert.Equal(0, lifetime.StopCount); await host.StopAsync(); @@ -424,7 +362,6 @@ namespace Microsoft.Extensions.Hosting Assert.Equal(0, service.DisposeCount); Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); Assert.Equal(1, lifetime.StopCount); } @@ -433,7 +370,6 @@ namespace Microsoft.Extensions.Hosting Assert.Equal(1, service.DisposeCount); Assert.Equal(1, lifetime.StartCount); - Assert.Equal(1, lifetime.StoppingCount); Assert.Equal(1, lifetime.StopCount); }