Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public partial class HostOptions
{
public HostOptions() { }
public Microsoft.Extensions.Hosting.BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { get { throw null; } set { } }
public bool ServicesStartConcurrently { get { throw null; } set { } }
public bool ServicesStopConcurrently { get { throw null; } set { } }
public System.TimeSpan ShutdownTimeout { get { throw null; } set { } }
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ public class HostOptions
/// </summary>
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// Determines if the <see cref="IHost"/> will start registered instances of <see cref="IHostedService"/> concurrently or sequentially. Defaults to false.
/// </summary>
public bool ServicesStartConcurrently { get; set; }

/// <summary>
/// Determines if the <see cref="IHost"/> will stop registered instances of <see cref="IHostedService"/> concurrently or sequentially. Defaults to false.
/// </summary>
public bool ServicesStopConcurrently { get; set; }

/// <summary>
/// The behavior the <see cref="IHost"/> will follow when any of
/// its <see cref="BackgroundService"/> instances throw an unhandled exception.
Expand All @@ -35,6 +45,20 @@ internal void Initialize(IConfiguration configuration)
{
ShutdownTimeout = TimeSpan.FromSeconds(seconds);
}

var servicesStartConcurrently = configuration["servicesStartConcurrently"];
if (!string.IsNullOrEmpty(servicesStartConcurrently)
&& bool.TryParse(servicesStartConcurrently, out bool startBehavior))
{
ServicesStartConcurrently = startBehavior;
}

var servicesStopConcurrently = configuration["servicesStopConcurrently"];
if (!string.IsNullOrEmpty(servicesStopConcurrently)
&& bool.TryParse(servicesStopConcurrently, out bool stopBehavior))
{
ServicesStopConcurrently = stopBehavior;
}
}
}
}
103 changes: 92 additions & 11 deletions src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -65,14 +66,65 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
combinedCancellationToken.ThrowIfCancellationRequested();
_hostedServices = Services.GetRequiredService<IEnumerable<IHostedService>>();

foreach (IHostedService hostedService in _hostedServices)
List<Exception> exceptions = new List<Exception>();

if (_options.ServicesStartConcurrently)
{
// Fire IHostedService.Start
await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
Task tasks = Task.WhenAll(_hostedServices.Select(async service =>
{
await service.StartAsync(combinedCancellationToken).ConfigureAwait(false);

if (service is BackgroundService backgroundService)
{
_ = TryExecuteBackgroundServiceAsync(backgroundService);
}
}));

if (hostedService is BackgroundService backgroundService)
try
{
_ = TryExecuteBackgroundServiceAsync(backgroundService);
await tasks.ConfigureAwait(false);
}
catch (Exception ex)
{
exceptions.AddRange(tasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
}
}
else
{
foreach (IHostedService hostedService in _hostedServices)
{
try
{
// Fire IHostedService.Start
await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);

if (hostedService is BackgroundService backgroundService)
{
_ = TryExecuteBackgroundServiceAsync(backgroundService);
}
}
catch (Exception ex)
{
exceptions.Add(ex);
break;
}
}
}

if (exceptions.Count > 0)
{
if (exceptions.Count == 1)
{
// Rethrow if it's a single error
Exception singleException = exceptions[0];
_logger.HostedServiceStartupFaulted(singleException);
ExceptionDispatchInfo.Capture(singleException).Throw();
}
else
{
var ex = new AggregateException("One or more hosted services failed to start.", exceptions);
_logger.HostedServiceStartupFaulted(ex);
throw ex;
}
}

Expand Down Expand Up @@ -128,15 +180,34 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
var exceptions = new List<Exception>();
if (_hostedServices != null) // Started?
{
foreach (IHostedService hostedService in _hostedServices.Reverse())
// Ensure hosted services are stopped in LIFO order
IEnumerable<IHostedService> hostedServices = _hostedServices.Reverse();

if (_options.ServicesStopConcurrently)
{
Task tasks = Task.WhenAll(hostedServices.Select(async service => await service.StopAsync(token).ConfigureAwait(false)));

try
{
await hostedService.StopAsync(token).ConfigureAwait(false);
await tasks.ConfigureAwait(false);
}
catch (Exception ex)
{
exceptions.Add(ex);
exceptions.AddRange(tasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
}
}
else
{
foreach (IHostedService hostedService in hostedServices)
{
try
{
await hostedService.StopAsync(token).ConfigureAwait(false);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
}
}
Expand All @@ -155,9 +226,19 @@ public async Task StopAsync(CancellationToken cancellationToken = default)

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 == 1)
{
// Rethrow if it's a single error
Exception singleException = exceptions[0];
_logger.StoppedWithException(singleException);
ExceptionDispatchInfo.Capture(singleException).Throw();
}
else
{
var ex = new AggregateException("One or more hosted services failed to stop.", exceptions);
_logger.StoppedWithException(ex);
throw ex;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,16 @@ public static void BackgroundServiceStoppingHost(this ILogger logger, Exception?
message: SR.BackgroundServiceExceptionStoppedHost);
}
}

public static void HostedServiceStartupFaulted(this ILogger logger, Exception? ex)
{
if (logger.IsEnabled(LogLevel.Error))
{
logger.LogError(
eventId: LoggerEventIds.HostedServiceStartupFaulted,
exception: ex,
message: "Hosting failed to start");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ internal static class LoggerEventIds
public static readonly EventId ApplicationStoppedException = new EventId(8, nameof(ApplicationStoppedException));
public static readonly EventId BackgroundServiceFaulted = new EventId(9, nameof(BackgroundServiceFaulted));
public static readonly EventId BackgroundServiceStoppingHost = new EventId(10, nameof(BackgroundServiceStoppingHost));
public static readonly EventId HostedServiceStartupFaulted = new EventId(11, nameof(HostedServiceStartupFaulted));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Hosting.Unit.Tests;

internal class DelegateHostedService : IHostedService, IDisposable
{
private readonly Action _started;
private readonly Action _stopping;
private readonly Action _disposing;

public DelegateHostedService(Action started, Action stopping, Action disposing)
{
_started = started;
_stopping = stopping;
_disposing = disposing;
}

public Task StartAsync(CancellationToken token)
{
StartDate = DateTimeOffset.Now;
_started();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken token)
{
StopDate = DateTimeOffset.Now;
_stopping();
return Task.CompletedTask;
}

public void Dispose() => _disposing();

public DateTimeOffset StartDate { get; private set; }
public DateTimeOffset StopDate { get; private set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting.Unit.Tests;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Xunit;
Expand All @@ -36,6 +36,78 @@ public async Task StopAsyncWithCancellation()
await host.StopAsync(cts.Token);
}

public static IEnumerable<object[]> StartAsync_StopAsync_Concurrency_TestCases
{
get
{
foreach (bool stopConcurrently in new[] { true, false })
{
foreach (bool startConcurrently in new[] { true, false })
{
foreach (int hostedServiceCount in new[] { 1, 4, 10 })
{
yield return new object[] { stopConcurrently, startConcurrently, hostedServiceCount };
}
}
}
}
}

[Theory]
[MemberData(nameof(StartAsync_StopAsync_Concurrency_TestCases))]
public async Task StartAsync_StopAsync_Concurrency(bool stopConcurrently, bool startConcurrently, int hostedServiceCount)
{
var hostedServices = new DelegateHostedService[hostedServiceCount];
bool[][] events = new bool[hostedServiceCount][];

for (int i = 0; i < hostedServiceCount; i++)
{
var index = i;
events[index] = new bool[2];
var service = new DelegateHostedService(() => { events[index][0] = true; }, () => { events[index][1] = true; } , () => { });

hostedServices[index] = service;
}

using var host = Host.CreateDefaultBuilder().ConfigureHostConfiguration(configBuilder =>
{
configBuilder.AddInMemoryCollection(new KeyValuePair<string, string>[]
{
new KeyValuePair<string, string>("servicesStartConcurrently", startConcurrently.ToString()),
new KeyValuePair<string, string>("servicesStopConcurrently", stopConcurrently.ToString())
});
}).ConfigureServices(serviceCollection =>
{
foreach (var hostedService in hostedServices)
{
serviceCollection.Add(ServiceDescriptor.Singleton<IHostedService>(hostedService));
}
}).Build();

await host.StartAsync(CancellationToken.None);

// Verifies that StartAsync had been called and that StopAsync had not been launched yet
for (int i = 0; i < hostedServiceCount; i++)
{
Assert.True(events[i][0]);
Assert.False(events[i][1]);
}

// Ensures that IHostedService instances are started in FIFO order
AssertExtensions.CollectionEqual(hostedServices, hostedServices.OrderBy(h => h.StartDate), EqualityComparer<DelegateHostedService>.Default);

await host.StopAsync(CancellationToken.None);

// Verifies that StopAsync had been called
for (int i = 0; i < hostedServiceCount; i++)
{
Assert.True(events[i][1]);
}

// Ensures that IHostedService instances are stopped in LIFO order
AssertExtensions.CollectionEqual(hostedServices.Reverse(), hostedServices.OrderBy(h => h.StopDate), EqualityComparer<DelegateHostedService>.Default);
}

[Fact]
public void CreateDefaultBuilder_IncludesContentRootByDefault()
{
Expand Down
Loading