Make IServer Start and Stop async

This commit is contained in:
Chris R 2017-04-11 10:18:29 -07:00
parent 11ab602b2f
commit e043fa871e
3 changed files with 37 additions and 19 deletions

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
@ -29,6 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
private readonly ITransportFactory _transportFactory; private readonly ITransportFactory _transportFactory;
private bool _isRunning; private bool _isRunning;
private int _stopped;
private Heartbeat _heartbeat; private Heartbeat _heartbeat;
public KestrelServer( public KestrelServer(
@ -67,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
private InternalKestrelServerOptions InternalOptions { get; } private InternalKestrelServerOptions InternalOptions { get; }
public void Start<TContext>(IHttpApplication<TContext> application) public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{ {
try try
{ {
@ -129,7 +131,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
_logger.LogDebug($"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default."); _logger.LogDebug($"No listening endpoints were configured. Binding to {Constants.DefaultServerAddress} by default.");
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. // "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
StartLocalhost(ServerAddress.FromUrl(Constants.DefaultServerAddress), serviceContext, application); await StartLocalhostAsync(ServerAddress.FromUrl(Constants.DefaultServerAddress), serviceContext, application, cancellationToken);
// If StartLocalhost doesn't throw, there is at least one listener. // If StartLocalhost doesn't throw, there is at least one listener.
// The port cannot change for "localhost". // The port cannot change for "localhost".
@ -174,7 +176,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase)) if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase))
{ {
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. // "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
StartLocalhost(parsedAddress, serviceContext, application); await StartLocalhostAsync(parsedAddress, serviceContext, application, cancellationToken);
// If StartLocalhost doesn't throw, there is at least one listener. // If StartLocalhost doesn't throw, there is at least one listener.
// The port cannot change for "localhost". // The port cannot change for "localhost".
@ -200,9 +202,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
try try
{ {
transport.BindAsync().Wait(); await transport.BindAsync();
} }
catch (AggregateException ex) when (ex.InnerException is AddressInUseException) catch (AddressInUseException ex)
{ {
throw new IOException($"Failed to bind to address {endPoint}: address already in use.", ex); throw new IOException($"Failed to bind to address {endPoint}: address already in use.", ex);
} }
@ -219,8 +221,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
} }
} }
public void Dispose() // Graceful shutdown if possible
public async Task StopAsync(CancellationToken cancellationToken)
{ {
if (Interlocked.Exchange(ref _stopped, 1) == 1)
{
return;
}
if (_transports != null) if (_transports != null)
{ {
var tasks = new Task[_transports.Count]; var tasks = new Task[_transports.Count];
@ -228,19 +236,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
{ {
tasks[i] = _transports[i].UnbindAsync(); tasks[i] = _transports[i].UnbindAsync();
} }
Task.WaitAll(tasks); await Task.WhenAll(tasks);
// TODO: Do transport-agnostic connection management/shutdown. // TODO: Do transport-agnostic connection management/shutdown.
for (int i = 0; i < _transports.Count; i++) for (int i = 0; i < _transports.Count; i++)
{ {
tasks[i] = _transports[i].StopAsync(); tasks[i] = _transports[i].StopAsync();
} }
Task.WaitAll(tasks); await Task.WhenAll(tasks);
} }
_heartbeat?.Dispose(); _heartbeat?.Dispose();
} }
// Ungraceful shutdown
public void Dispose()
{
var cancelledTokenSource = new CancellationTokenSource();
cancelledTokenSource.Cancel();
StopAsync(cancelledTokenSource.Token).GetAwaiter().GetResult();
}
private void ValidateOptions() private void ValidateOptions()
{ {
if (Options.Limits.MaxRequestBufferSize.HasValue && if (Options.Limits.MaxRequestBufferSize.HasValue &&
@ -258,7 +274,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
} }
} }
private void StartLocalhost<TContext>(ServerAddress parsedAddress, ServiceContext serviceContext, IHttpApplication<TContext> application) private async Task StartLocalhostAsync<TContext>(ServerAddress parsedAddress, ServiceContext serviceContext, IHttpApplication<TContext> application, CancellationToken cancellationToken)
{ {
if (parsedAddress.Port == 0) if (parsedAddress.Port == 0)
{ {
@ -277,16 +293,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
var connectionHandler = new ConnectionHandler<TContext>(ipv4ListenOptions, serviceContext, application); var connectionHandler = new ConnectionHandler<TContext>(ipv4ListenOptions, serviceContext, application);
var transport = _transportFactory.Create(ipv4ListenOptions, connectionHandler); var transport = _transportFactory.Create(ipv4ListenOptions, connectionHandler);
_transports.Add(transport); _transports.Add(transport);
transport.BindAsync().Wait(); await transport.BindAsync();
} }
catch (AggregateException ex) when (ex.InnerException is AddressInUseException) catch (AddressInUseException ex)
{ {
throw new IOException($"Failed to bind to address {parsedAddress} on the IPv4 loopback interface: port already in use.", ex); throw new IOException($"Failed to bind to address {parsedAddress} on the IPv4 loopback interface: port already in use.", ex);
} }
catch (AggregateException ex) catch (Exception ex)
{ {
_logger.LogWarning(0, $"Unable to bind to {parsedAddress} on the IPv4 loopback interface: ({ex.Message})"); _logger.LogWarning(0, $"Unable to bind to {parsedAddress} on the IPv4 loopback interface: ({ex.Message})");
exceptions.Add(ex.InnerException); exceptions.Add(ex);
} }
try try
@ -299,16 +315,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
var connectionHandler = new ConnectionHandler<TContext>(ipv6ListenOptions, serviceContext, application); var connectionHandler = new ConnectionHandler<TContext>(ipv6ListenOptions, serviceContext, application);
var transport = _transportFactory.Create(ipv6ListenOptions, connectionHandler); var transport = _transportFactory.Create(ipv6ListenOptions, connectionHandler);
_transports.Add(transport); _transports.Add(transport);
transport.BindAsync().Wait(); await transport.BindAsync();
} }
catch (AggregateException ex) when (ex.InnerException is AddressInUseException) catch (AddressInUseException ex)
{ {
throw new IOException($"Failed to bind to address {parsedAddress} on the IPv6 loopback interface: port already in use.", ex); throw new IOException($"Failed to bind to address {parsedAddress} on the IPv6 loopback interface: port already in use.", ex);
} }
catch (AggregateException ex) catch (Exception ex)
{ {
_logger.LogWarning(0, $"Unable to bind to {parsedAddress} on the IPv6 loopback interface: ({ex.Message})"); _logger.LogWarning(0, $"Unable to bind to {parsedAddress} on the IPv6 loopback interface: ({ex.Message})");
exceptions.Add(ex.InnerException); exceptions.Add(ex);
} }
if (exceptions.Count == 2) if (exceptions.Count == 2)

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
@ -161,7 +162,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private static void StartDummyApplication(IServer server) private static void StartDummyApplication(IServer server)
{ {
server.Start(new DummyApplication(context => TaskCache.CompletedTask)); server.StartAsync(new DummyApplication(context => TaskCache.CompletedTask), CancellationToken.None).GetAwaiter().GetResult();
} }
private class MockTransportFactory : ITransportFactory private class MockTransportFactory : ITransportFactory

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Net; using System.Net;
using System.Threading;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv;
@ -27,7 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.TestHelpers
try try
{ {
_server.Start(new DummyApplication(app)); _server.StartAsync(new DummyApplication(app), CancellationToken.None).GetAwaiter().GetResult();
} }
catch catch
{ {