Skip to content

Add CancellationTokenSource.TryReset() #48492

Closed
@halter73

Description

@halter73

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 backing CancellationTokenSource (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 the RequestAborted token is accessed. If Kestrel attempted to reuse the CTS backing the RequestAborted 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 calling CancelAfter(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 causing CancelAfter() to no-op. This is demonstrated in the second HttpClient usage example.

Proposed API

namespace System.Threading
{
    public class CancellationTokenSource : IDisposable
    {
+        // Returns false if the CTS has already been canceled
+        public bool TryReset();    
    }
}

Usage Examples

Simplified RequestAborted example.

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.InvokeAsync(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 TryReset(), you could achieve similar functionality like so:

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 Designs

Since calling CancelAfter(Timeout.Infinite) should already reset any timeouts, we could consider an API that just clears registrations. You could call both CancelAfter(Timeout.Infinite) and TryClearRegistrations() to get the full TryReset() 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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions