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.