From 4f3fdaebeebbe4481ec00e4790da1758a6aa02ea Mon Sep 17 00:00:00 2001 From: "Chris Ross (ASP.NET)" Date: Fri, 13 Oct 2017 10:42:02 -0700 Subject: [PATCH] #1208 Default timeout for IHost.StopAsync. Create Host with DI. --- .../HostBuilder.cs | 3 +- .../HostOptions.cs | 18 +++++ .../Internal/Host.cs | 67 +++++++++++-------- .../HostTests.cs | 63 ++++++++++++++++- 4 files changed, 121 insertions(+), 30 deletions(-) create mode 100644 src/Microsoft.Extensions.Hosting/HostOptions.cs diff --git a/src/Microsoft.Extensions.Hosting/HostBuilder.cs b/src/Microsoft.Extensions.Hosting/HostBuilder.cs index bcaac8c9a8..1d7208ba1e 100644 --- a/src/Microsoft.Extensions.Hosting/HostBuilder.cs +++ b/src/Microsoft.Extensions.Hosting/HostBuilder.cs @@ -113,7 +113,7 @@ namespace Microsoft.Extensions.Hosting BuildAppConfiguration(); CreateServiceProvider(); - return new Host(_appServices); + return _appServices.GetRequiredService(); } private void BuildHostConfiguration() @@ -179,6 +179,7 @@ namespace Microsoft.Extensions.Hosting services.AddSingleton(_appConfiguration); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddOptions(); services.AddLogging(); diff --git a/src/Microsoft.Extensions.Hosting/HostOptions.cs b/src/Microsoft.Extensions.Hosting/HostOptions.cs new file mode 100644 index 0000000000..45fb9a187f --- /dev/null +++ b/src/Microsoft.Extensions.Hosting/HostOptions.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Options for + /// + public class HostOptions + { + /// + /// The default timeout for . + /// + public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Hosting/Internal/Host.cs b/src/Microsoft.Extensions.Hosting/Internal/Host.cs index e10834ade1..ce7d531dc7 100644 --- a/src/Microsoft.Extensions.Hosting/Internal/Host.cs +++ b/src/Microsoft.Extensions.Hosting/Internal/Host.cs @@ -8,22 +8,26 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Hosting.Internal { internal class Host : IHost { - private ILogger _logger; - private IHostLifetime _hostLifetime; - private ApplicationLifetime _applicationLifetime; + private readonly ILogger _logger; + private readonly IHostLifetime _hostLifetime; + private readonly ApplicationLifetime _applicationLifetime; + private readonly HostOptions _options; private IEnumerable _hostedServices; - internal Host(IServiceProvider services) + public Host(IServiceProvider services, IApplicationLifetime applicationLifetime, ILogger logger, + IHostLifetime hostLifetime, IOptions options) { Services = services ?? throw new ArgumentNullException(nameof(services)); - _applicationLifetime = Services.GetRequiredService() as ApplicationLifetime; - _logger = Services.GetRequiredService>(); - _hostLifetime = Services.GetRequiredService(); + _applicationLifetime = (applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime))) as ApplicationLifetime; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _hostLifetime = hostLifetime ?? throw new ArgumentNullException(nameof(hostLifetime)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); } public IServiceProvider Services { get; } @@ -56,36 +60,43 @@ namespace Microsoft.Extensions.Hosting.Internal public async Task StopAsync(CancellationToken cancellationToken = default) { _logger.Stopping(); - - // Trigger IApplicationLifetime.ApplicationStopping - _applicationLifetime?.StopApplication(); - IList exceptions = new List(); - if (_hostedServices != null) // Started? + using (var cts = new CancellationTokenSource(_options.ShutdownTimeout)) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken)) { - foreach (var hostedService in _hostedServices.Reverse()) + var token = linkedCts.Token; + // Trigger IApplicationLifetime.ApplicationStopping + _applicationLifetime?.StopApplication(); + + IList exceptions = new List(); + if (_hostedServices != null) // Started? { - try + foreach (var hostedService in _hostedServices.Reverse()) { - await hostedService.StopAsync(cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); + token.ThrowIfCancellationRequested(); + try + { + await hostedService.StopAsync(token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + } } } - } - await _hostLifetime.StopAsync(cancellationToken); + token.ThrowIfCancellationRequested(); + await _hostLifetime.StopAsync(token); - // Fire IApplicationLifetime.Stopped - _applicationLifetime?.NotifyStopped(); + // Fire IApplicationLifetime.Stopped + _applicationLifetime?.NotifyStopped(); - if (exceptions.Count > 0) - { - var ex = new AggregateException("One or more hosted services failed to stop.", exceptions); - _logger.StoppedWithException(ex); - throw ex; + if (exceptions.Count > 0) + { + var ex = new AggregateException("One or more hosted services failed to stop.", exceptions); + _logger.StoppedWithException(ex); + throw ex; + } } _logger.Stopped(); diff --git a/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs b/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs index d4604967d2..83166d6360 100644 --- a/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs +++ b/test/Microsoft.Extensions.Hosting.Tests/HostTests.cs @@ -500,7 +500,68 @@ namespace Microsoft.Extensions.Hosting var task = host.StopAsync(cts.Token); cts.Cancel(); - Assert.Equal(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(8)))); + Assert.Equal(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(5)))); + } + } + + [Fact] + public async Task HostStopAsyncUsesDefaultTimeoutIfGivenTokenDoesNotFire() + { + var service = new Mock(); + service.Setup(s => s.StopAsync(It.IsAny())) + .Returns(token => + { + return Task.Run(() => + { + token.WaitHandle.WaitOne(); + }); + }); + + using (var host = CreateBuilder() + .ConfigureServices((services) => + { + services.Configure(options => options.ShutdownTimeout = TimeSpan.FromSeconds(0.5)); + services.AddSingleton(service.Object); + }) + .Build()) + { + await host.StartAsync(); + + var cts = new CancellationTokenSource(); + + // Purposefully don't trigger cts + var task = host.StopAsync(cts.Token); + + Assert.Equal(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(10)))); + } + } + + [Fact] + public async Task WebHostStopAsyncUsesDefaultTimeoutIfNoTokenProvided() + { + var service = new Mock(); + service.Setup(s => s.StopAsync(It.IsAny())) + .Returns(token => + { + return Task.Run(() => + { + token.WaitHandle.WaitOne(); + }); + }); + + using (var host = CreateBuilder() + .ConfigureServices((services) => + { + services.Configure(options => options.ShutdownTimeout = TimeSpan.FromSeconds(0.5)); + services.AddSingleton(service.Object); + }) + .Build()) + { + await host.StartAsync(); + + var task = host.StopAsync(); + + Assert.Equal(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(10)))); } }