-
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
Add CancellationTokenSource.TryReset() #48492
Comments
Tagging subscribers to this area: @dotnet/ncl Issue DetailsBackground and MotivationWhen a library or framework exposes a To use the Another scenario where people want to be able to "reset" a CTS is after calling Another benefit is that it's immediately obvious if resetting failed. If you try resetting a timeout with Proposed APInamespace System.Threading
{
public class CancellationTokenSource : IDisposable {
+ // Returns false if the CTS has already been canceled
+ public bool TryReset();
}
} Usage ExamplesSimplified private readonly CancellationTokenSource _abortCts = new CancellationTokenSource();
// Imagine this could be called in the background because a client disconnect or some timeout.
void AbortConnection()
{
_abortCts.Cancel();
}
async Task ProcessRequestsAsync()
{
while (_abortCts.TryReset())
{
var httpContext = await ParseRequest(_abortCts.Token);
httpContext.RequestAborted = _abortCts.Token;
await Middleware.Invoke(httpContext);
}
} HttpClient request var cts = new CancellationTokenSource();
var httpClient = new HttpClient();
while (true)
{
cts.CancelAfter(TimeSpan.FromSeconds(10));
var response = await httpClient.GetAsync("http://example.org/healthcheck", cts.Token);
response.EnsureSuccessStatusCode();
if (!cts.TryReset())
{
cts = new CancellationTokenSource();
}
} Today, without var cts = new CancellationTokenSource();
var httpClient = new HttpClient();
while (true)
{
cts.CancelAfter(TimeSpan.FromSeconds(10));
if (cts.Token.IsCancellationRequested)
{
cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
}
// ... So in the case of timeouts, the proposed reset APIs don't offer any entirely new functionality, but it exposes it in a much more obvious way. Alternative DesignsSince calling namespace System.Threading
{
public class CancellationTokenSource : IDisposable {
+ public bool TryClearRegistrations();
}
} RisksPeople might assume they can reset a CTS that has already been canceled. We should clearly document this isn't the case. Hopefully the Code that references a stale token and registers after the backing CTS has been reset can run into the same problems those who don't dispose their registrations today face when the CTS is reused. That might not sound bad, but this API will likely encourage more reuse of CTS objects. I do think that not disposing registrations is more common than using stale token though.
|
Why can't they? |
Resetting a canceled CTS would uncancel associated CancellationTokens. That's a surprising behavior we'd like to avoid. |
I'm assuming that you can only use Reset if you know (or can reasonably assume) that there are no cancellation tokens in the wild. Otherwise reusing the CTS for something else would cancel them at the wrong time, plus removing registrations would also be surprising. |
Yes. TryReset really wouldn't be doing much to "reset" the CTS, other than stop any associated timer and clear out registrations that really shouldn't have been there in the first place if the code was properly constructed, and from that perspective the name is a little strange, but I don't have a better one in mind. CancellationToken is just a struct-wrapper for a CancellationTokenSource, so any CT previously handed out will still be referencing this CTS, and any usage of that CT after TryReset would still work exactly as it previously did. So, the user of this method would simply be saying "I plan to reuse this CTS because the operation with which it was previously associated is now complete; as such, I'm going to clear out registrations because I'm not going to ever cancel them, anyway". If either the owner calling the TryReset was wrong, or if code still tried to use the "old" CT handed out prior to the TryReset call, that's a bug that code needs to fix. I think this is reasonable. It's a bit dangerous if used poorly, but much, much less dangerous than a CancellationToken transitioning from canceled to not-canceled. I think we'd also want to be a little fuzzy about what a false return value means, if for no other reason than future proofing. If true, feel free to reuse the token as it wasn't canceled. If false, it was either canceled or for some reason we didn't feel comfortable suggesting you can reuse it. The 99% case will be that it returns true, as cancellation is the minority case. |
Will this clear out the free node list as well? I was hoping that would be reusable. That could basically result in 0 allocations even for re-registers across uses. It does mean that the free list can grow longer without a way to clear it or detect that is has grown though. |
Implementation detail, but functionally it needn't, so I'd start with "no". When I prototyped it, I explicitly did the opposite, transferring any stale registrations to the free list. |
That's what I wanted to hear 😁 |
namespace System.Threading
{
public partial class CancellationTokenSource
{
public bool TryReset();
}
} |
Background and Motivation
When a library or framework exposes a
CancellationToken
that usually does not get canceled (e.g. HttpContext.RequestAborted) they often still need to dispose the backingCancellationTokenSource
(CTS) after an operation completes rather than reusing it in order to account for callers that might never dispose their registrations.To use the
RequestAborted
example, Kestrel disposes the backing CTS after every request where theRequestAborted
token is accessed. If Kestrel attempted to reuse the CTS backing theRequestAborted
token for future requests in order to reduce allocations, it would risk leaking any undisposed registrations and triggering the undisposed registrations when unrelated requests are aborted.Another scenario where people want to be able to "reset" a CTS is after calling
CancelAfter()
. This can already be achieved by callingCancelAfter(Timeout.Infinite)
, but that's not necessarily obvious unless you read the docs.TryReset()
is something that would immediately make sense in this scenario when looking at intellisense completions.Another benefit is that it's immediately obvious if resetting failed. If you try resetting a timeout with
CancelAfter()
, you have to check after the call to verify their was no cancellation before or during the call causingCancelAfter()
to no-op. This is demonstrated in the secondHttpClient
usage example.Proposed API
Usage Examples
Simplified
RequestAborted
example.HttpClient request
Today, without
TryReset()
, you could achieve similar functionality like so:So in the case of timeouts, the proposed reset APIs don't offer any entirely new functionality, but it exposes it in a much more obvious way.
Alternative Designs
Since calling
CancelAfter(Timeout.Infinite)
should already reset any timeouts, we could consider an API that just clears registrations. You could call bothCancelAfter(Timeout.Infinite)
andTryClearRegistrations()
to get the fullTryReset()
behavior mentioned above.namespace System.Threading { public class CancellationTokenSource : IDisposable { + public bool TryClearRegistrations(); } }
Risks
People might assume they can reset a CTS that has already been canceled. We should clearly document this isn't the case. Hopefully the
Try
in the name will get people thinking about the cases in which it won't work.Code that references a stale token and registers after the backing CTS has been reset can run into the same problems those who don't dispose their registrations today face when the CTS is reused. That might not sound bad, but this API will likely encourage more reuse of CTS objects. I do think that not disposing registrations is more common than using stale tokens though.
The text was updated successfully, but these errors were encountered: