Align host behavior with respect to exceptions (#7657)

- Don't catch errors in IHostedService.StartAsync
- Only catch errors when executing StopAsync but rethrow an aggregate
- Updated the tests
This commit is contained in:
David Fowler 2019-02-16 19:09:14 -08:00 committed by GitHub
parent 799b91a324
commit 0fd753bfc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 110 additions and 42 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -21,31 +21,17 @@ namespace Microsoft.AspNetCore.Hosting.Internal
_services = services; _services = services;
} }
public async Task StartAsync(CancellationToken token) public Task StartAsync(CancellationToken token)
{ {
try return ExecuteAsync(service => service.StartAsync(token));
{
await ExecuteAsync(service => service.StartAsync(token));
}
catch (Exception ex)
{
_logger.ApplicationError(LoggerEventIds.HostedServiceStartException, "An error occurred starting the application", ex);
}
} }
public async Task StopAsync(CancellationToken token) public Task StopAsync(CancellationToken token)
{ {
try return ExecuteAsync(service => service.StopAsync(token), throwOnFirstFailure: false);
{
await ExecuteAsync(service => service.StopAsync(token));
}
catch (Exception ex)
{
_logger.ApplicationError(LoggerEventIds.HostedServiceStopException, "An error occurred stopping the application", ex);
}
} }
private async Task ExecuteAsync(Func<IHostedService, Task> callback) private async Task ExecuteAsync(Func<IHostedService, Task> callback, bool throwOnFirstFailure = true)
{ {
List<Exception> exceptions = null; List<Exception> exceptions = null;
@ -57,6 +43,11 @@ namespace Microsoft.AspNetCore.Hosting.Internal
} }
catch (Exception ex) catch (Exception ex)
{ {
if (throwOnFirstFailure)
{
throw;
}
if (exceptions == null) if (exceptions == null)
{ {
exceptions = new List<Exception>(); exceptions = new List<Exception>();

View File

@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Hosting
await AssertResponseContains(server.RequestDelegate, "Exception from constructor"); await AssertResponseContains(server.RequestDelegate, "Exception from constructor");
} }
} }
[Theory] [Theory]
[MemberData(nameof(DefaultWebHostBuildersWithConfig))] [MemberData(nameof(DefaultWebHostBuildersWithConfig))]
public async Task StartupConfigureServicesThrows_Fallback(IWebHostBuilder builder) public async Task StartupConfigureServicesThrows_Fallback(IWebHostBuilder builder)
@ -967,7 +967,8 @@ namespace Microsoft.AspNetCore.Hosting
{ {
builder = builder builder = builder
.CaptureStartupErrors(false) .CaptureStartupErrors(false)
.ConfigureAppConfiguration((context, configurationBuilder) => { .ConfigureAppConfiguration((context, configurationBuilder) =>
{
configurationBuilder.AddInMemoryCollection( configurationBuilder.AddInMemoryCollection(
new[] new[]
{ {
@ -1204,6 +1205,51 @@ namespace Microsoft.AspNetCore.Hosting
} }
} }
[Theory]
[MemberData(nameof(DefaultWebHostBuilders))]
public async Task ThrowingFromHostedServiceFailsStartAsync(IWebHostBuilder builder)
{
builder.Configure(app => { })
.ConfigureServices(services =>
{
services.AddHostedService<ThrowingHostedService>();
})
.UseServer(new TestServer());
var host = builder.Build();
var startEx = await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
Assert.Equal("Hosted Service throws in StartAsync", startEx.Message);
var stopEx = await Assert.ThrowsAsync<AggregateException>(() => host.StopAsync());
Assert.Single(stopEx.InnerExceptions);
Assert.Equal("Hosted Service throws in StopAsync", stopEx.InnerExceptions[0].Message);
}
[Theory]
[MemberData(nameof(DefaultWebHostBuilders))]
public async Task ThrowingFromHostedServiceStopsOtherHostedServicesFromRunningStartAsync(IWebHostBuilder builder)
{
builder.Configure(app => { })
.ConfigureServices(services =>
{
services.AddHostedService<ThrowingHostedService>();
services.AddHostedService<NonThrowingHostedService>();
})
.UseServer(new TestServer());
var host = builder.Build();
var service = host.Services.GetServices<IHostedService>().OfType<NonThrowingHostedService>().First();
var startEx = await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
Assert.Equal("Hosted Service throws in StartAsync", startEx.Message);
var stopEx = await Assert.ThrowsAsync<AggregateException>(() => host.StopAsync());
Assert.Single(stopEx.InnerExceptions);
Assert.Equal("Hosted Service throws in StopAsync", stopEx.InnerExceptions[0].Message);
// This service is never constructed
Assert.False(service.StartCalled);
Assert.True(service.StopCalled);
}
private static void StaticConfigureMethod(IApplicationBuilder app) { } private static void StaticConfigureMethod(IApplicationBuilder app) { }
private IWebHostBuilder CreateWebHostBuilder() private IWebHostBuilder CreateWebHostBuilder()
@ -1257,6 +1303,37 @@ namespace Microsoft.AspNetCore.Hosting
Assert.Contains(expectedText, bodyText); Assert.Contains(expectedText, bodyText);
} }
private class ThrowingHostedService : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
throw new InvalidOperationException("Hosted Service throws in StartAsync");
}
public Task StopAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException("Hosted Service throws in StopAsync");
}
}
private class NonThrowingHostedService : IHostedService
{
public bool StartCalled { get; set; }
public bool StopCalled { get; set; }
public Task StartAsync(CancellationToken cancellationToken)
{
StartCalled = true;
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
StopCalled = true;
return Task.CompletedTask;
}
}
private class MyStartupFilter : IStartupFilter private class MyStartupFilter : IStartupFilter
{ {
public bool Executed { get; set; } public bool Executed { get; set; }

View File

@ -458,26 +458,26 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public async Task WebHostNotifiesAllIApplicationLifetimeEventsCallbacksEvenIfTheyThrow() public async Task WebHostDoesNotNotifyAllIApplicationLifetimeEventsCallbacksIfTheyThrow()
{ {
bool[] events1 = null; bool[] hostedSeviceCalls1 = null;
bool[] events2 = null; bool[] hostedServiceCalls2 = null;
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseFakeServer() .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
events1 = RegisterCallbacksThatThrow(services); hostedSeviceCalls1 = RegisterCallbacksThatThrow(services);
events2 = RegisterCallbacksThatThrow(services); hostedServiceCalls2 = RegisterCallbacksThatThrow(services);
}) })
.Build()) .Build())
{ {
await host.StartAsync(); await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
Assert.True(events1[0]); Assert.True(hostedSeviceCalls1[0]);
Assert.True(events2[0]); Assert.False(hostedServiceCalls2[0]);
host.Dispose(); host.Dispose();
Assert.True(events1[1]); Assert.True(hostedSeviceCalls1[1]);
Assert.True(events2[1]); Assert.True(hostedServiceCalls2[1]);
} }
} }
@ -667,17 +667,17 @@ namespace Microsoft.AspNetCore.Hosting
} }
[Fact] [Fact]
public async Task WebHostNotifiesAllIHostedServicesAndIApplicationLifetimeCallbacksEvenIfTheyThrow() public async Task WebHostDoesNotNotifyAllIHostedServicesAndIApplicationLifetimeCallbacksIfTheyThrow()
{ {
bool[] events1 = null; bool[] hostedServiceCalls1 = null;
bool[] events2 = null; bool[] hostedServiceCalls2 = null;
using (var host = CreateBuilder() using (var host = CreateBuilder()
.UseFakeServer() .UseFakeServer()
.ConfigureServices(services => .ConfigureServices(services =>
{ {
events1 = RegisterCallbacksThatThrow(services); hostedServiceCalls1 = RegisterCallbacksThatThrow(services);
events2 = RegisterCallbacksThatThrow(services); hostedServiceCalls2 = RegisterCallbacksThatThrow(services);
}) })
.Build()) .Build())
{ {
@ -690,14 +690,14 @@ namespace Microsoft.AspNetCore.Hosting
var started2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStarted); var started2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStarted);
var stopping2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStopping); var stopping2 = RegisterCallbacksThatThrow(applicationLifetime2.ApplicationStopping);
await host.StartAsync(); await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
Assert.True(events1[0]); Assert.True(hostedServiceCalls1[0]);
Assert.True(events2[0]); Assert.False(hostedServiceCalls2[0]);
Assert.True(started.All(s => s)); Assert.True(started.All(s => s));
Assert.True(started2.All(s => s)); Assert.True(started2.All(s => s));
host.Dispose(); host.Dispose();
Assert.True(events1[1]); Assert.True(hostedServiceCalls1[1]);
Assert.True(events2[1]); Assert.True(hostedServiceCalls2[1]);
Assert.True(stopping.All(s => s)); Assert.True(stopping.All(s => s));
Assert.True(stopping2.All(s => s)); Assert.True(stopping2.All(s => s));
} }