How to cancel a TaskCompletionSource using a timeout

You may pass a CancellationToken to your method, which can internally implement the cancellation logic:

public Task<StatePropertyEx> RequestStateForEntity(
    EntityKey entity, string propName, CancellationToken token)
{
    var tcs = new TaskCompletionSource<StateInfo>();
    try
    {
        // Cache checking
        _evtClient.SubmitStateRequest(entity, propName, token);

        return tcs.Task;
    }
    catch (Exception ex)
    {
        tcs.SetException(ex);
        return tcs.Task;
    }
}

And inside SubmitStateRequest:

token.ThrowIfCancellationRequest();

Note ThrowIfCancellationRequest will throw a OperationCanceledException which you will need to handle. If you are making a blocking call, you can internally set the CancellationTokenSource with a TimeSpan:

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

First off, what you really want to enable is cancellation. The fact that the cancellation comes from a timeout is just a footnote.

.NET has some great built-in support for cancellation, and the Task-based Asynchronous Pattern prescribes how to use it.

Essentially, what you want to do is take a CancellationToken:

Task<StatePropertyEx> RequestStateForEntity(EntityKey entity, string propName,
    CancellationToken cancellationToken);

Next, you want to respond when that token is signaled. Ideally, you would want to just pass the CancellationToken down to the _evtClient so that the request is truly cancelled:

_evtClient.SubmitStateRequest(entity, propName, cancellationToken);

This is the normal way of implementing cancellation, and it works great if SubmitStateRequest already understands cancellation. Often the event arguments have a flag indicating cancellation (e.g., AsyncCompletedEventArgs.Cancelled). If at all possible, use this approach (i.e., change _evtClient as necessary to support cancellation).

But sometimes this just isn't possible. In this case, you can choose to pretend to support cancellation. What you're actually doing is just ignoring the request if it completes after it was cancelled. This is not the most ideal situation but sometimes you have no choice.

Personally, I don't really like this kind of approach since it makes the API "lie": the method signature claims to support cancellation but it really is just faking it. So first, I recommend documenting this. Put in a code comment apology explaining that _evtClient doesn't support cancellation, and the "cancellation" is actually just pretend cancellation.

Then, you'll need to hook into the CancellationToken yourself, after the state request item is in the list but before the actual request is sent:

var item = new StateRequestItem(entity, propName, tcs);
_stateRequestItemList.TryAdd(cacheKey, item);
item.CancellationRegistration = cancellationToken.Register(() =>
{
  StateRequestItem cancelledItem;
  if (!_stateRequestItemList.TryRemove(cacheKey, out cancelledItem))
    return;
  cancelledItem.TaskCompletionSource.TrySetCanceled();
});
_evtClient.SubmitStateRequest(entity, propName);

Finally, you'll need to update your event handler completion code (not shown) to ignore the situation where the state request item has already been removed, and to dispose the CancellationRegistration if the state request item is found.


Once your method supports cancellation, then it's easy to cancel via a timer:

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
CancellationToken token = cts.Token;

or from any other kind of situation. Say, if the user cancels whatever (s)he's doing. Or if another part of the system decides it doesn't need that data anymore. Once your code supports cancellation, it can handle cancellation for any reason.