-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Add the ability to start and stop IHostedService instances concurrently #75894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
f17c035
3798699
8d804b4
99986e7
e0257b8
ff29ba5
92be997
335c576
fd8a0c7
e634a6c
946a3d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
…ged mid-execution; add support for stop behavior and exception behavior to be set in config file
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -120,15 +120,17 @@ public async Task StopAsync(CancellationToken cancellationToken = default) | |||||
| // Trigger IHostApplicationLifetime.ApplicationStopping | ||||||
| _applicationLifetime.StopApplication(); | ||||||
|
|
||||||
| IList<Exception> exceptions = new List<Exception>(); | ||||||
| if (_hostedServices != null) // Started? | ||||||
| List<Exception> exceptions = new List<Exception>(); | ||||||
| if (_hostedServices?.Any() == true) // Started? | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the change to call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose the inner block here might succeed even if the list is empty. I'm not 100% sure what happens if you pass an empty list of tasks into I just felt like it wasn't necessary to even have to worry about that. |
||||||
| { | ||||||
| Queue<Task> tasks = new Queue<Task>(); | ||||||
| foreach (IHostedService hostedService in _hostedServices.Reverse()) | ||||||
| // Ensure hosted services are stopped in FILO order | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| IEnumerable<IHostedService> hostedServices = _hostedServices.Reverse(); | ||||||
|
|
||||||
| switch (_options.BackgroundServiceStopBehavior) | ||||||
| { | ||||||
| switch (_options.BackgroundServiceStopBehavior) | ||||||
| { | ||||||
| case BackgroundServiceStopBehavior.Sequential: | ||||||
| case BackgroundServiceStopBehavior.Sequential: | ||||||
| foreach (IHostedService hostedService in hostedServices) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| await hostedService.StopAsync(token).ConfigureAwait(false); | ||||||
|
|
@@ -137,24 +139,21 @@ public async Task StopAsync(CancellationToken cancellationToken = default) | |||||
| { | ||||||
| exceptions.Add(ex); | ||||||
| } | ||||||
| break; | ||||||
| case BackgroundServiceStopBehavior.Asynchronous: | ||||||
| default: | ||||||
| tasks.Enqueue(hostedService.StopAsync(token)); | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| foreach (Task task in tasks) | ||||||
| { | ||||||
| try | ||||||
| { | ||||||
| await task.ConfigureAwait(false); | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| exceptions.Add(ex); | ||||||
| } | ||||||
| } | ||||||
| break; | ||||||
| case BackgroundServiceStopBehavior.Asynchronous: | ||||||
| default: | ||||||
| Task tasks = Task.WhenAll(hostedServices.Select(async service => await service.StopAsync(token).ConfigureAwait(false))); | ||||||
|
|
||||||
| try | ||||||
| { | ||||||
| await tasks.ConfigureAwait(false); | ||||||
| } | ||||||
| catch (Exception ex) | ||||||
| { | ||||||
| exceptions.AddRange(tasks.Exception?.InnerExceptions?.ToArray() ?? new[] { ex }); | ||||||
| } | ||||||
| break; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,8 +14,10 @@ | |||||||||||||||||||
| using Microsoft.Extensions.Configuration; | ||||||||||||||||||||
| using Microsoft.Extensions.Configuration.UserSecrets; | ||||||||||||||||||||
| using Microsoft.Extensions.DependencyInjection; | ||||||||||||||||||||
| using Microsoft.Extensions.DependencyInjection.Extensions; | ||||||||||||||||||||
| using Microsoft.Extensions.Logging; | ||||||||||||||||||||
| using Microsoft.Extensions.Options; | ||||||||||||||||||||
| using Moq; | ||||||||||||||||||||
| using Xunit; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| namespace Microsoft.Extensions.Hosting.Tests | ||||||||||||||||||||
|
|
@@ -34,6 +36,85 @@ public async Task StopAsyncWithCancellation() | |||||||||||||||||||
| await host.StopAsync(cts.Token); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| public static IEnumerable<object[]> StopAsync_ValidBackgroundServiceStopBehavior_TestCases | ||||||||||||||||||||
| { | ||||||||||||||||||||
| get | ||||||||||||||||||||
| { | ||||||||||||||||||||
| foreach (BackgroundServiceStopBehavior stopBehavior in Enum.GetValues(typeof(BackgroundServiceStopBehavior))) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| foreach (int hostedServiceCount in new[] { 0, 1, 10 }) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| yield return new object[] { stopBehavior, hostedServiceCount }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| [Theory] | ||||||||||||||||||||
| [MemberData(nameof(StopAsync_ValidBackgroundServiceStopBehavior_TestCases))] | ||||||||||||||||||||
| public async Task StopAsync_ValidBackgroundServiceStopBehavior(BackgroundServiceStopBehavior stopBehavior, int hostedServiceCount) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| var callOrder = hostedServiceCount; | ||||||||||||||||||||
| var hostedServices = new Mock<BackgroundService>[hostedServiceCount]; | ||||||||||||||||||||
| var executionCount = 0; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| for (int i = 0; i < hostedServiceCount; i++) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| var index = i; | ||||||||||||||||||||
| var service = new Mock<BackgroundService>(); | ||||||||||||||||||||
| service.Setup(y => y.StopAsync(It.IsAny<CancellationToken>())) | ||||||||||||||||||||
| .Callback(() => | ||||||||||||||||||||
| { | ||||||||||||||||||||
| // Ensures that IHostedService instances are stopped in FILO order | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
LIFO is the more common term |
||||||||||||||||||||
| Assert.Equal(index, --callOrder); | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| .Returns(async () => | ||||||||||||||||||||
| { | ||||||||||||||||||||
| if (stopBehavior.Equals(BackgroundServiceStopBehavior.Asynchronous) && hostedServiceCount > 1) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| // Basically this will force all IHostedService instances to have StopAsync called before any one completes | ||||||||||||||||||||
| executionCount++; | ||||||||||||||||||||
| int waitCount = 0; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| while (executionCount < hostedServiceCount && waitCount++ < 20) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| await Task.Delay(10).ConfigureAwait(false); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // This will ensure that we truly are stopping all services asynchronously | ||||||||||||||||||||
| Assert.Equal(hostedServiceCount, executionCount); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| .Verifiable(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| hostedServices[index] = service; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| var builder = new HostBuilder(); | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Doesn't appear to be used. |
||||||||||||||||||||
| using var host = Host.CreateDefaultBuilder().ConfigureHostConfiguration(configBuilder => | ||||||||||||||||||||
| { | ||||||||||||||||||||
| configBuilder.AddInMemoryCollection(new KeyValuePair<string, string>[] | ||||||||||||||||||||
| { | ||||||||||||||||||||
| new KeyValuePair<string, string>("backgroundServiceStopBehavior", stopBehavior.ToString()) | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| }).ConfigureServices(configurer => | ||||||||||||||||||||
| { | ||||||||||||||||||||
| for (int i = 0; i < hostedServiceCount; i++) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| var index = i; | ||||||||||||||||||||
| configurer.Add(ServiceDescriptor.Singleton<IHostedService>(hostedServices[index].Object)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+132
to
+136
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
| }).Build(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| await host.StartAsync(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| await host.StopAsync(CancellationToken.None); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| foreach (var service in hostedServices) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| service.Verify(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| [Fact] | ||||||||||||||||||||
| public void CreateDefaultBuilder_IncludesContentRootByDefault() | ||||||||||||||||||||
| { | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change seems unrelated/unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why you would want to use
NullOrEmptyoverNullOrWhiteSpacehere. TheTryParsemethods further down the chain will handle the blank-space scenarios, but I don't see a need to even get there when we can stop it so easily.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The odds that the config value is going to be just whitespace are VERY low. So doing extra checking just slows down the happy path.
Look at the implementation of
IsNullOrWhiteSpace:runtime/src/libraries/System.Private.CoreLib/src/System/String.cs
Lines 491 to 501 in f518c05
It goes through the string checking the chars one by one. There is no reason to do this.