You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
BackgroundService appears to create tasks that will never complete #36380
Based on my own understanding of BackgroundService and the description of its use of cancellation in dotnet/extensions#1245, I don't expect the CancellationToken to StartAsync or StopAsync to be marked as cancelled under normal operation. (This is why BackgroundService exists: it provides a more useful CancellationToken to ExecuteAsync than the one provided to StartAsync, which is helpful for tasks that run indefinitely.) That means the task created with this expression is likely to never complete:
Task.Delay(Timeout.Infinite,cancellationToken)
To resolve this, a new CancellationTokenSource could be created for the current scope:
This line appears to create a task that will most likely never complete:
This basically is just a way to "await" the signalling of a cancellation token. The timeout is never intended to fire.
I don't expect the CancellationToken to StartAsync or StopAsync to be marked as cancelled under normal operation.
The token from IHost.StopAsync is being flowed in to allow the calling code to stop waiting for shutdown. The intent of this code is to block StopAsync until the service finishes shutting down or the caller decides to stop waiting.
That means the task created with this expression is likely to never complete:
It won't complete if the app finishes shutting down on time, but the impact of that is very low since it's just a single entry in the timer queue that never gets fired.
To resolve this, a new CancellationTokenSource could be created for the current scope:
I don't see how that would fix anything... You're still triggering cancellation based on the same token, just in a more complicated way, right? You're cancelling the stopCts when the cancellationToken is cancelled, which is just a round-about way of just using cancellationToken, as far as I can tell.
I don't see how that would fix anything... You're still triggering cancellation based on the same token, just in a more complicated way, right? You're cancelling the stopCts when the cancellationToken is cancelled, which is just a round-about way of just using cancellationToken, as far as I can tell.
There is a difference, and it revolves around this point:
It won't complete if the app finishes shutting down on time, but the impact of that is very low since it's just a single entry in the timer queue that never gets fired.
It is theoretically possible for the service to be shut down independent of the rest of the program, and for a service to be started and stopped numerous times before the application exits--for example, an application that self-restarts all its services if it encounters an unrecoverable issue. There has been some talk lately of developing applications that actually favor this over attempting to handle every possible exception condition--I'm not saying I think it's ideal, but it is a valid recovery pattern.
In this scenario, my suggested replacement will clean up the task. The existing code will leave an abandoned entry until the application exits. It might not be huge, but it's still a leak, and it only takes a few lines to resolve.
Do you have measurements you can share with us? I'm certainly not opposed to taking a fix that has data showing it reduces memory usage.
I don't, I'm afraid. I assume it's quite minimal. The only time I can see it making a difference is when an application has another bug that causes it to restart frequently, or if a program is designed to start and stop background services as they're needed. If the application remains running for months or years, maybe it would cause some problems.
I'm mostly interested in fixing it because I see BackgroundService used as an example or reference from time to time; it's a simple service implementation that's easy to understand. I'm concerned that people reading and potentially copying the pattern on line 65 won't understand that it's likely to leak a task entry and isn't really correct usage, even if it's unlikely to have a significant impact in this particular scenario.
It may be worth checking whether this same pattern is used elsewhere in official codebases where it could potentially have a more significant impact.
Since it's easy to fix that shouldn't break anything, I'd be happy to submit a PR.
FYI: We aren't accepting new PRs for 3.0 right now and there isn't a branch for 3.1 (the next release after 3.0) yet. I would expect one to be up and running sometime next week though. Just want to set the expectations. We're starting to lock down for 3.0's release later in September.
I think a PR would be OK (though would want input from @Tratcher and @davidfowl). I think that using a separate "stop the timeout" token would be preferred rather than relying on the fact that chaining the tokens like you did above means that when the method ends the timeout will cancel (because the CTS is disposed).
usingvarcancelTimeoutCts=newCancellationTokenSource();usingvarlinkedSource=CancellationTokenSource.CreateLinkedTokenSource(cancelTimeoutCts.Token,cancellationToken);awaitTask.WhenAny(_executingTask,Task.Delay(Timeout.Infinite,linkedSource.Token));cancelTimeoutCts.Cancel();// Unconditionally stop the Task.Delay, if it's still running.
That seems reasonable. I think you're right you don't need the other token. Creating a linked source doesn't restrict the ability of the CTS to be cancelled.
Looking at the source for Task and CancellationTokenSource, it seems awfully convoluted to be using Task.WhenAny and Task.Delay this way; it looks as though it's prone to race conditions, and it utilizes a timer where it really doesn't need to. There's anecdotal evidence scattered around sites like Stack Overflow suggesting that it is possible to crash an application with poorly managed timers and CTSes. Is there a better pattern that can be used? Perhaps it makes more sense to make a proposal for a solution in CoreCLR before continuing here.
Test 1 causes a very fast, linear increase in memory usage (on my Surface Book, about 20 MiB/sec).
Tests 2 and 3 maintain consistent memory usage.
If test 1 is passed a default cancellation token instead of exitCts.Token, it doesn't leak.
Thus, if StopAsync is passed a cancellation token that can be cancelled, it will leak. It won't be significant unless it happens a lot, but it could add up, especially if this same pattern is used elsewhere.
Relying on CancellationTokenSource.Dispose without calling Cancel appears to be sufficient. I don't understand why this is the case, so I can't determine whether it's ideal.
Edit: A good number of those seem to be forks containing BackgroundService.cs, but there are also a lot of non-forks that contain classes based heavily on BackgroundService, so it is being used as an example and should have correct form.
Test 1 causes a very fast, linear increase in memory usage (on my Surface Book, about 20 MiB/sec).
Let's also remember the context. This is during shutdown and you wouldn't expect a huge number of background services. I don't disagree that a leak is bad, but 20MiB/sec when in a tight loop like this may translate to a million or more calls to Task.Delay which isn't really a close analog to the real situation.
Running a similar sample myself indicated closer to 125 bytes per invocation, which is still worth fixing, but unless the user has thousands (or more) of BackgroundService instances it won't get up to 20MiB.
Test 3 seems the most effective way of those options.
Another option to evaluate would be to use a TaskCompletionSource, like so:
// Pseudo-code not run through a compiler ;)usingvartcs=newTaskCompletionSource<object>();usingvarregistration=cancellationToken.Register(()=>cts.TrySetCompleted(null));awaitTask.WhenAny(_exitTask,tcs.Task);
Basically, instead of using Task.Delay(Timeout.Infinite, ct) as a hack to await a CTS, just await a CTS "directly" by having it signal a TCS.
Have you considered running these through BenchmarkDotNet to get more statistically-accurate data?
Let's also remember the context. This is during shutdown and you wouldn't expect a huge number of background services. I don't disagree that a leak is bad, but 20MiB/sec when in a tight loop like this may translate to a million or more calls to Task.Delay which isn't really a close analog to the real situation.
It's certainly not an issue in the most common use case of BackgroundService itself. I see this mostly being an issue when people copy the pattern into code that doesn't just run on shutdown.
Have you considered running these through BenchmarkDotNet to get more statistically-accurate data?
I ran my tests while without internet connectivity, so I worked from scratch. I'm not sure that obtaining accurate numbers is important here, though; it's either leaking, or it's not. We're not really going for speed, rather trying to minimize race conditions that could result in a leak. In that regard, a simple loop for testing is probably a better choice.
Basically, instead of using Task.Delay(Timeout.Infinite, ct) as a hack to await a CTS, just await a CTS "directly" by having it signal a TCS.
@StephenCleary Your API designs are very 'clear'. 👍 Looking forward to using https://github.com/StephenCleary/AsyncEx to solve my HostedService BackgroundService recoverability. Currently, critical exceptions are vaporized, leaving the Host running, but useless! I don't want to build sophisticated recovery scenarios in my 'worker', it already has a reasonable implementation of 'starting up', why build another; let the host reinstantiate it! Simple, leverages DI without work-around re-init methods etc.
@moonrockfamily I blogged about that recently - part of a 3-part blog series. I use all three techniques to create a "critical BackgroundService" base type that avoids startup delay (Task.Run), catches ExecuteAsync exceptions, and logs and then exits the host if a critical background service fails.
Not sure if this issue is related but I just realized that the CancellationToken passed to BackgroundService.StartAsync is not the same as the one received in ExecuteAsync method. So when the CancellationToken is cancelled, ExecuteAsync is not getting notified through the CancellationToken and keeps running.
Activity
analogrelay commentedon Jul 24, 2019
This basically is just a way to "await" the signalling of a cancellation token. The timeout is never intended to fire.
The token from
IHost.StopAsyncis being flowed in to allow the calling code to stop waiting for shutdown. The intent of this code is to blockStopAsyncuntil the service finishes shutting down or the caller decides to stop waiting.It won't complete if the app finishes shutting down on time, but the impact of that is very low since it's just a single entry in the timer queue that never gets fired.
I don't see how that would fix anything... You're still triggering cancellation based on the same token, just in a more complicated way, right? You're cancelling the
stopCtswhen thecancellationTokenis cancelled, which is just a round-about way of just usingcancellationToken, as far as I can tell.Zenexer commentedon Jul 25, 2019
@anurse
There is a difference, and it revolves around this point:
It is theoretically possible for the service to be shut down independent of the rest of the program, and for a service to be started and stopped numerous times before the application exits--for example, an application that self-restarts all its services if it encounters an unrecoverable issue. There has been some talk lately of developing applications that actually favor this over attempting to handle every possible exception condition--I'm not saying I think it's ideal, but it is a valid recovery pattern.
In this scenario, my suggested replacement will clean up the task. The existing code will leave an abandoned entry until the application exits. It might not be huge, but it's still a leak, and it only takes a few lines to resolve.
analogrelay commentedon Jul 25, 2019
I see, because you are disposing this alternate CTS.
Do you have measurements you can share with us? I'm certainly not opposed to taking a fix that has data showing it reduces memory usage.
Zenexer commentedon Jul 26, 2019
I don't, I'm afraid. I assume it's quite minimal. The only time I can see it making a difference is when an application has another bug that causes it to restart frequently, or if a program is designed to start and stop background services as they're needed. If the application remains running for months or years, maybe it would cause some problems.
I'm mostly interested in fixing it because I see BackgroundService used as an example or reference from time to time; it's a simple service implementation that's easy to understand. I'm concerned that people reading and potentially copying the pattern on line 65 won't understand that it's likely to leak a task entry and isn't really correct usage, even if it's unlikely to have a significant impact in this particular scenario.
It may be worth checking whether this same pattern is used elsewhere in official codebases where it could potentially have a more significant impact.
Since it's easy to fix that shouldn't break anything, I'd be happy to submit a PR.
analogrelay commentedon Jul 26, 2019
FYI: We aren't accepting new PRs for 3.0 right now and there isn't a branch for 3.1 (the next release after 3.0) yet. I would expect one to be up and running sometime next week though. Just want to set the expectations. We're starting to lock down for 3.0's release later in September.
I think a PR would be OK (though would want input from @Tratcher and @davidfowl). I think that using a separate "stop the timeout" token would be preferred rather than relying on the fact that chaining the tokens like you did above means that when the method ends the timeout will cancel (because the CTS is disposed).
Zenexer commentedon Jul 26, 2019
I just realized I omitted the Cancel call from my example, which is probably why it wasn't clear what the change was supposed to do.
Should that be wrapped in try-finally? It's not unconditional because it won't run if _executingTask throws. Does disposing a CTS actually cancel it?
Zenexer commentedon Jul 26, 2019
Looks like this would work:
It doesn't seem to be necessary to pass two tokens to CreateLinkedTokenSource. There's an internal overload that accepts only one token. Although it's not exposed directly since it's marked as internal, it's called if only one token is passed to the public overload. It's basically going to do the same thing as my first example, but with UnsafeRegister instead of Register. I don't think it makes sense to create an extra CTS as in your example, but I may be missing something.
analogrelay commentedon Jul 26, 2019
Yep, should be in a
try-finally.I'm not actually 100% sure :). Clearly I assumed it did by how I read your initial example, but it may not.
That seems reasonable. I think you're right you don't need the other token. Creating a linked source doesn't restrict the ability of the CTS to be cancelled.
analogrelay commentedon Jul 26, 2019
So basically this then:
Zenexer commentedon Jul 27, 2019
With a try-finally, yes. As far as I can tell, disposing a CTS doesn't cancel it. Interestingly, it looks like there could still be a leak if cancellationToken is canceled via a timer.
Looking at the source for Task and CancellationTokenSource, it seems awfully convoluted to be using Task.WhenAny and Task.Delay this way; it looks as though it's prone to race conditions, and it utilizes a timer where it really doesn't need to. There's anecdotal evidence scattered around sites like Stack Overflow suggesting that it is possible to crash an application with poorly managed timers and CTSes. Is there a better pattern that can be used? Perhaps it makes more sense to make a proposal for a solution in CoreCLR before continuing here.
Zenexer commentedon Jul 27, 2019
I tested with the following code:
exitCts.Token, it doesn't leak.Thus, if StopAsync is passed a cancellation token that can be cancelled, it will leak. It won't be significant unless it happens a lot, but it could add up, especially if this same pattern is used elsewhere.
Relying on CancellationTokenSource.Dispose without calling Cancel appears to be sufficient. I don't understand why this is the case, so I can't determine whether it's ideal.
Zenexer commentedon Jul 27, 2019
A quick search reveals that this pattern is widespread, despite leaking: https://github.com/search?q=%22Task.WhenAny%22+%22Task.Delay(Timeout.Infinite%22&type=Code
Edit: A good number of those seem to be forks containing BackgroundService.cs, but there are also a lot of non-forks that contain classes based heavily on BackgroundService, so it is being used as an example and should have correct form.
analogrelay commentedon Jul 27, 2019
Let's also remember the context. This is during shutdown and you wouldn't expect a huge number of background services. I don't disagree that a leak is bad, but 20MiB/sec when in a tight loop like this may translate to a million or more calls to
Task.Delaywhich isn't really a close analog to the real situation.Running a similar sample myself indicated closer to 125 bytes per invocation, which is still worth fixing, but unless the user has thousands (or more) of
BackgroundServiceinstances it won't get up to 20MiB.Test 3 seems the most effective way of those options.
Another option to evaluate would be to use a
TaskCompletionSource, like so:Basically, instead of using
Task.Delay(Timeout.Infinite, ct)as a hack to await a CTS, just await a CTS "directly" by having it signal a TCS.Have you considered running these through BenchmarkDotNet to get more statistically-accurate data?
Zenexer commentedon Jul 29, 2019
It's certainly not an issue in the most common use case of BackgroundService itself. I see this mostly being an issue when people copy the pattern into code that doesn't just run on shutdown.
I ran my tests while without internet connectivity, so I worked from scratch. I'm not sure that obtaining accurate numbers is important here, though; it's either leaking, or it's not. We're not really going for speed, rather trying to minimize race conditions that could result in a leak. In that regard, a simple loop for testing is probably a better choice.
That seems to work:
analogrelay commentedon Jul 29, 2019
Fair point.
Sounds good. Probably the best option since it's clear as to what's happening.
8 remaining items
moonrockfamily commentedon May 28, 2020
@StephenCleary Your API designs are very 'clear'. 👍 Looking forward to using https://github.com/StephenCleary/AsyncEx to solve my HostedService BackgroundService recoverability. Currently, critical exceptions are vaporized, leaving the Host running, but useless! I don't want to build sophisticated recovery scenarios in my 'worker', it already has a reasonable implementation of 'starting up', why build another; let the host reinstantiate it! Simple, leverages DI without work-around re-init methods etc.
StephenCleary commentedon Jun 14, 2020
@moonrockfamily I blogged about that recently - part of a 3-part blog series. I use all three techniques to create a "critical
BackgroundService" base type that avoids startup delay (Task.Run), catchesExecuteAsyncexceptions, and logs and then exits the host if a critical background service fails.maryamariyan commentedon Oct 8, 2020
@Zenexer the source code has moved to dotnet/runtime since. Are you interested in creating a PR for this?
nminaya commentedon Oct 16, 2020
Not sure if this issue is related but I just realized that the CancellationToken passed to BackgroundService.StartAsync is not the same as the one received in ExecuteAsync method. So when the CancellationToken is cancelled, ExecuteAsync is not getting notified through the CancellationToken and keeps running.
Nick-Stanton commentedon Sep 2, 2022
@Zenexer thanks for the catch. A fix has been merged to resolve this behavior