Implement IServer.StopAsync

This commit is contained in:
Chris R 2017-03-28 15:16:31 -07:00
parent bc003985c6
commit 4e8872b94d
6 changed files with 62 additions and 32 deletions

View File

@ -79,12 +79,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
} }
} }
/// <summary>
/// The amount of time to wait for active requests to drain while the server is shutting down.
/// New requests will receive a 503 response in this time period. The default is 5 seconds.
/// </summary>
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5);
internal void SetRequestQueueLimit(RequestQueue requestQueue) internal void SetRequestQueueLimit(RequestQueue requestQueue)
{ {
_requestQueue = requestQueue; _requestQueue = requestQueue;

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
private bool _stopping; private bool _stopping;
private int _outstandingRequests; private int _outstandingRequests;
private ManualResetEvent _shutdownSignal; private TaskCompletionSource<object> _shutdownSignal;
private readonly ServerAddressesFeature _serverAddresses; private readonly ServerAddressesFeature _serverAddresses;
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
_processRequest = new Action<object>(ProcessRequestAsync); _processRequest = new Action<object>(ProcessRequestAsync);
_maxAccepts = _options.MaxAccepts; _maxAccepts = _options.MaxAccepts;
EnableResponseCaching = _options.EnableResponseCaching; EnableResponseCaching = _options.EnableResponseCaching;
_shutdownSignal = new ManualResetEvent(false); _shutdownSignal = new TaskCompletionSource<object>();
} }
internal HttpSysListener Listener { get; } internal HttpSysListener Listener { get; }
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
public IFeatureCollection Features { get; } public IFeatureCollection Features { get; }
public void Start<TContext>(IHttpApplication<TContext> application) public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -124,6 +124,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Listener.Start(); Listener.Start();
ActivateRequestProcessingLimits(); ActivateRequestProcessingLimits();
return Task.CompletedTask;
} }
private void ActivateRequestProcessingLimits() private void ActivateRequestProcessingLimits()
@ -224,7 +226,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ {
if (Interlocked.Decrement(ref _outstandingRequests) == 0 && _stopping) if (Interlocked.Decrement(ref _outstandingRequests) == 0 && _stopping)
{ {
_shutdownSignal.Set(); LogHelper.LogInfo(_logger, "All requests drained.");
_shutdownSignal.TrySetResult(0);
} }
} }
} }
@ -242,24 +245,30 @@ namespace Microsoft.AspNetCore.Server.HttpSys
context.Dispose(); context.Dispose();
} }
public void Dispose() public Task StopAsync(CancellationToken cancellationToken)
{ {
_stopping = true; _stopping = true;
// Wait for active requests to drain // Wait for active requests to drain
if (_outstandingRequests > 0) if (_outstandingRequests > 0)
{ {
LogHelper.LogInfo(_logger, "Stopping, waiting for " + _outstandingRequests + " request(s) to drain."); LogHelper.LogInfo(_logger, "Stopping, waiting for " + _outstandingRequests + " request(s) to drain.");
var drained = _shutdownSignal.WaitOne(Listener.Options.ShutdownTimeout);
if (drained) var waitForStop = new TaskCompletionSource<object>();
{ cancellationToken.Register(() =>
LogHelper.LogInfo(_logger, "All requests drained successfully.");
}
else
{ {
LogHelper.LogInfo(_logger, "Timed out, terminating " + _outstandingRequests + " request(s)."); LogHelper.LogInfo(_logger, "Timed out, terminating " + _outstandingRequests + " request(s).");
} waitForStop.TrySetResult(0);
});
return Task.WhenAny(_shutdownSignal.Task, waitForStop.Task);
} }
// All requests are finished
return Task.CompletedTask;
}
public void Dispose()
{
_stopping = true;
Listener.Dispose(); Listener.Dispose();
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq; using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Testing.xunit; using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -25,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
serverAddressesFeature.PreferHostingUrls = true; serverAddressesFeature.PreferHostingUrls = true;
server.Listener.Options.UrlPrefixes.Add(serverAddress); server.Listener.Options.UrlPrefixes.Add(serverAddress);
server.Start(new DummyApplication()); server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
Assert.Equal(overrideAddress, serverAddressesFeature.Addresses.Single()); Assert.Equal(overrideAddress, serverAddressesFeature.Addresses.Single());
} }
@ -46,7 +47,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
serverAddressesFeature.Addresses.Add(overrideAddress); serverAddressesFeature.Addresses.Add(overrideAddress);
server.Listener.Options.UrlPrefixes.Add(serverAddress); server.Listener.Options.UrlPrefixes.Add(serverAddress);
server.Start(new DummyApplication()); server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
Assert.Equal(serverAddress, serverAddressesFeature.Addresses.Single()); Assert.Equal(serverAddress, serverAddressesFeature.Addresses.Single());
} }
@ -63,7 +64,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
serverAddressesFeature.PreferHostingUrls = true; serverAddressesFeature.PreferHostingUrls = true;
server.Listener.Options.UrlPrefixes.Add(serverAddress); server.Listener.Options.UrlPrefixes.Add(serverAddress);
server.Start(new DummyApplication()); server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
Assert.Equal(serverAddress, serverAddressesFeature.Addresses.Single()); Assert.Equal(serverAddress, serverAddressesFeature.Addresses.Single());
} }
@ -84,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
serverAddressesFeature.Addresses.Add(serverAddress); serverAddressesFeature.Addresses.Add(serverAddress);
server.Listener.Options.UrlPrefixes.Add(overrideAddress); server.Listener.Options.UrlPrefixes.Add(overrideAddress);
server.Start(new DummyApplication()); server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
Assert.Equal(overrideAddress, serverAddressesFeature.Addresses.Single()); Assert.Equal(overrideAddress, serverAddressesFeature.Addresses.Single());
} }
@ -100,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
var serverAddressesFeature = server.Features.Get<IServerAddressesFeature>(); var serverAddressesFeature = server.Features.Get<IServerAddressesFeature>();
serverAddressesFeature.Addresses.Add(serverAddress); serverAddressesFeature.Addresses.Add(serverAddress);
server.Start(new DummyApplication()); server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
} }
} }
@ -109,7 +110,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ {
using (var server = new MessagePump(Options.Create(new HttpSysOptions()), new LoggerFactory())) using (var server = new MessagePump(Options.Create(new HttpSysOptions()), new LoggerFactory()))
{ {
server.Start(new DummyApplication()); server.StartAsync(new DummyApplication(), CancellationToken.None).Wait();
Assert.Equal(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single()); Assert.Equal(Constants.DefaultServerAddress, server.Features.Get<IServerAddressesFeature>().Addresses.Single());
} }

View File

@ -7,6 +7,7 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -312,7 +313,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(rootUri.Scheme, rootUri.Host, rootUri.Port, path)); server.Listener.Options.UrlPrefixes.Add(UrlPrefix.Create(rootUri.Scheme, rootUri.Host, rootUri.Port, path));
} }
server.Start(new DummyApplication(app)); server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
return server; return server;
} }

View File

@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Task<string> responseTask; Task<string> responseTask;
ManualResetEvent received = new ManualResetEvent(false); ManualResetEvent received = new ManualResetEvent(false);
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => using (var server = Utilities.CreateHttpServer(out address, httpContext =>
{ {
received.Set(); received.Set();
httpContext.Response.ContentLength = 11; httpContext.Response.ContentLength = 11;
@ -82,11 +82,34 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ {
responseTask = SendRequestAsync(address); responseTask = SendRequestAsync(address);
Assert.True(received.WaitOne(10000)); Assert.True(received.WaitOne(10000));
await server.StopAsync(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
} }
string response = await responseTask; string response = await responseTask;
Assert.Equal("Hello World", response); Assert.Equal("Hello World", response);
} }
[ConditionalFact]
public async Task Server_DisposeWithoutStopDuringRequest_Aborts()
{
Task<string> responseTask;
var received = new ManualResetEvent(false);
var stopped = new ManualResetEvent(false);
string address;
using (var server = Utilities.CreateHttpServer(out address, httpContext =>
{
received.Set();
Assert.True(stopped.WaitOne(TimeSpan.FromSeconds(10)));
httpContext.Response.ContentLength = 11;
return httpContext.Response.WriteAsync("Hello World");
}))
{
responseTask = SendRequestAsync(address);
Assert.True(received.WaitOne(TimeSpan.FromSeconds(10)));
}
stopped.Set();
await Assert.ThrowsAsync<HttpRequestException>(async () => await responseTask);
}
[ConditionalFact] [ConditionalFact]
public async Task Server_ShutdownDuringLongRunningRequest_TimesOut() public async Task Server_ShutdownDuringLongRunningRequest_TimesOut()
{ {
@ -95,7 +118,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
bool? shutdown = null; bool? shutdown = null;
var waitForShutdown = new ManualResetEvent(false); var waitForShutdown = new ManualResetEvent(false);
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => using (var server = Utilities.CreateHttpServer(out address, httpContext =>
{ {
received.Set(); received.Set();
shutdown = waitForShutdown.WaitOne(TimeSpan.FromSeconds(15)); shutdown = waitForShutdown.WaitOne(TimeSpan.FromSeconds(15));
@ -105,8 +128,9 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ {
responseTask = SendRequestAsync(address); responseTask = SendRequestAsync(address);
Assert.True(received.WaitOne(TimeSpan.FromSeconds(10))); Assert.True(received.WaitOne(TimeSpan.FromSeconds(10)));
Assert.False(shutdown.HasValue);
await server.StopAsync(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
} }
Assert.False(shutdown.HasValue);
waitForShutdown.Set(); waitForShutdown.Set();
await Assert.ThrowsAsync<HttpRequestException>(async () => await responseTask); await Assert.ThrowsAsync<HttpRequestException>(async () => await responseTask);
} }
@ -271,7 +295,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
using (server) using (server)
{ {
server.Start(new DummyApplication()); await server.StartAsync(new DummyApplication(), CancellationToken.None);
string response = await SendRequestAsync(address); string response = await SendRequestAsync(address);
Assert.Equal(string.Empty, response); Assert.Equal(string.Empty, response);
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Threading;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
server.Listener.Options.Authentication.AllowAnonymous = allowAnonymous; server.Listener.Options.Authentication.AllowAnonymous = allowAnonymous;
try try
{ {
server.Start(new DummyApplication(app)); server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
return server; return server;
} }
catch (HttpSysException) catch (HttpSysException)
@ -76,7 +77,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ {
var server = new MessagePump(Options.Create(new HttpSysOptions()), new LoggerFactory()); var server = new MessagePump(Options.Create(new HttpSysOptions()), new LoggerFactory());
server.Features.Get<IServerAddressesFeature>().Addresses.Add(UrlPrefix.Create(scheme, host, port, path).ToString()); server.Features.Get<IServerAddressesFeature>().Addresses.Add(UrlPrefix.Create(scheme, host, port, path).ToString());
server.Start(new DummyApplication(app)); server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
return server; return server;
} }
} }