-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
[API Proposal]: Add API to enable BackgroundService startup to avoid blocking host startup #36063
Comments
Fixed by adding |
Yeah the method shouldn't be blocking. Wrapping in a |
Reopening this so we can look at it. I'm glad that you solved your problem but we should try to solve this so others don't hit it. |
It looks like if your run a bunch of CPU-bound code in In health checks I ended up inserting a |
Async method executes synchronously till first |
But it would be great if you can solve this case |
It's a trade off. There may be reasonable initialization code the service needs to run before letting the app continue. It's within the service's control how long it blocks. |
I have also encountered this issue and solved it in the same way as @vova-lantsov-dev did. However, my main concern is that this behavior is not straightforward, would be nice if this was more predictable or documented somewhere |
I think it's reasonable to dispatch public class MyService: BackgroundService
{
public override async Task StartAsync()
{
await InitializeAsync();
await base.StartAsync();
}
public override async Task ExecuteAsync()
{
// Do background stuff...
}
} It's called BackgroundService. It seems odd that you can block the foreground in the default case. If we're not happy with overriding This would be a breaking change however, since existing services may depend on the initial synchronous work blocking the startup process. |
Sorry I never did this change but enough people have hit it now I think we should just do it. https://github.com/aspnet/Extensions/tree/davidfowl/background-service |
I just ran into this and was about to leave feedback on the docs. Glad I checked here first. The Worker template works because it awaits a Task.Delay. Change that to a |
@davidfowl I just looked at the BackgroundService changes in your branch - would it make more sense for the Host to Task.Run each hosted service instead of depending on the hosted service to do it? |
I'm not a fan as it's wasteful, but if that many people run into it it might be worth doing. I'd honestly rather teach people that these things aren't running on dedicated threads so blocking them isn't the way to go. |
Triage summary: The work here is to consider dispatching |
It was very helpful to learn from the updated docs that "No further services are started until ExecuteAsync becomes asynchronous, such as by calling await." docs Is there a possibility of this being addressed in .NET Core 3.1? |
This is how I resolved it in my case [as detailed in 17674 referenced above]: I started from scratch, choosing to implement my own version of it using This works quite well in my setup and actually sped up the app start time [compared to even using the Task.Yield() workaround as mentioned above]. |
For what it's worth: The argument that you can just override This: class MyService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
var state = ...;
await Task.Yield();
while (true)
{
Work(state);
await Task.Delay(..., ct);
}
}
} Turns into this: class MyService : BackgroundService
{
T? _state;
public override Task StartAsync(CancellationToken ct)
{
_state = ...;
return base.StartAsync(ct);
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (true)
{
Work(_state!);
await Task.Delay(..., ct);
}
}
} This seems like a significant regression in code clarity to me. There's also an argument to be made that, if the current behavior of The name |
@davidfowl is your commit here still a true WIP or are you redacting your idea to go this route? I found the documentation here that references this issue a little confusing as it initially made me think that even using I believe any confusion would be mitigated by doing what you initially proposed in that commit and having the abstraction dispatch our executing |
I couldn't figure out the best area label to add to this issue. Please help me learn by adding exactly one area label. |
I noticed that hangs can now occur in later versions of ASP Core if you do a Task.Delay. Previous versions of ASP Core were not causing this issue but I don't have exact version information. Calling |
@HelloKitty |
@vova-lantsov-dev It appears so, but I do not think this was the case in a previous version of ASP Core. Maybe 2.1. This ran fine without hanging a year ago, anyway I have adopted |
This causes an issue when unit testing. If I mock a service to return a [TestClass]
public class TestClass
{
[TestMethod, TestCategory("UnitTest")]
public async Task Background_Service_Should_Return_FromStartAsync()
{
var sut = new FakeService();
await sut.StartAsync(Token);
//code never reaches here because ExecuteAsync never awaits a task
Assert.IsTrue(true);
}
}
public class FakeService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.CompletedTask;
}
}
} |
I've run into this too. |
I think this should make it in NET 8.0 as indicated here |
The |
Sad that even in v8 this won't be fixed. For anyone who lands here, see Stephen Cleary's blog series which provides workarounds for these issues. |
.NET 8 has been released, and unfortunately, Microsoft has turned a blind eye to this issue. The situation is amusing - to start a synchronous service, you need to await something, for example, Task.Yield, resulting in the generation of a state machine and a loss of at least 0.05% performance at this stage. |
Yeah.. MS is way more focusing on implementing YET next type of UI library, just to deprecate and kill it 1 year later, rather then solve these issues.. |
I’m very confused why Task.Run isn’t a viable workaround? |
You can do that until the presence of a state machine becomes a hindrance to you, and the TaskScheduler works perfectly. |
When does that happen? |
Most developers don't think about |
This sounds like an unnecessary "rite of passage" pitfall for anyone deciding to stroll into BackgroundService country. Myself twice now (2020 + 2024) 😆 |
Nothing here was working for me...🔴 As it turns out you cannot test this with breakpoints! Maybe that seems logical to some people, but it did not dawn on me until I spent an hour, tried every single solution and workaround in this issue, and finally said, to myself, "Hmmm, maybe I'll try logging instead of using a breakpoint". Give me a thumbs up if this was helpful. And give me a pie in the face otherwise! 🤣 |
this sounds more like a edjucation/knownledge or documentation issue then. Just slapping async/await onto everything is not "working with tasks". |
Everything should've been built in a foolproof way. |
Hit this issue again. It takes a long time to diagnose the issue and arrive here for various workarounds. If one doesn't have guru-level async knowledge, it's an insidious time waster. Please please please consider this for v9. 🙏 |
Just hit this issue again. I reviewed some PR's for some of our senior devs and assumed it had been fixed since they are very experienced. Turns out they had never used .NET background services and too just fell into the newb trap. Something called |
I think that's a reasonable breaking change to take. Is a property or constructor parameter on |
This is such a funny issue. On the one hand you'd think starting your "Background Service" would run in the background. On the other hand async/await runs sync until it hits something async. So you either get a weird idea of an 'eventually background service' or you make this a special case where await doesn't actually do what await does everywhere else in .net. Is this just fallout from the weird 'runs sync until async' thing. That can bite you in other circumstances too. It's weird, or maybe 'unexpected' or 'unintuitive' rather. For my background services I'm just Task.Yielding at the start of them reflexively now so I don't even have to think about when it switches to actually async. |
Yep! I took a stab at it, and marked it ready for review. Feel free to edit the proposal or suggest better naming (or patterns). |
namespace Microsoft.Extensions.Hosting;
public class BackgroundService : IHostedService
{
public virtual bool StartAsynchronously { get; } = true;
} |
I like and agree with just making this always on. Can you link the new issue number here when available? |
Background and motivation
IHostedService.StartAsync executes inline (and sequentially) when IHost.Start/StartAsync is called. With a BackgroundService, this causes lots of confusion since running code that isn't async in ExecuteAsync or StartAsync also runs inline. The workaround today is to call Task.Run on your own code or to use Task.Yield(), but this is not obvious.
API Proposal
public class BackgroundService : IHostedService { + public virtual bool StartAsynchronously { get; } = true; }
NOTE: StartAsynchronously would be true by default.
API Usage
Alternative Designs
An optional constructor argument.
Original Issue
BackgroundService blocked the execution of whole host
Describe the bug
When I run the specific
foreach
cycle inBackgroundService
-derived class, it blocks the whole host from starting. Even second hosted service doesn't start. When I commentforeach
cycle, everything works as expected.To Reproduce
TargetFramework: netcoreapp2.1
Version: 2.1.12
Use following hosted service: NotificationRunner.cs
And singleton: NotificationService.cs
Expected behavior
BackgroundService.ExecuteAsync should work in background without blocking even if it has blocking code. As you can see in NotificationService class, cancellation of enumerator is based on IApplicationLifetime.ApplicationStopping, but anyway it shouldn't affect the host startup because BackgroundService is expected to run in background :)
Screenshots
When
foreach
cycle existsThen execution is blocked on this cycle
But when
foreach
cycle is commentedThen execution continues as expected
(But why twice?)
Additional context
The text was updated successfully, but these errors were encountered: