I have some code that is validating some data by making calls to a number of other services. I start all of the calls in parallel and then wait until at least one of them finishes. If any of the requests fail, I don't care about the result of the other calls.
I make the calls with HttpClient
and I have passed an HttpMessageHandler
in that does a bunch of logging. Essentially:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
try
{
response = await base.SendAsync(request, cancellationToken);
}
catch (OperationCanceledException ex)
{
LogTimeout(...);
throw;
}
catch (Exception ex)
{
LogFailure(...);
throw;
}
finally
{
LogComplete(...);
}
return response;
}
No the part that I'm having trouble with is when I cancel the requests. When I cancel a request, I'm doing it on purpose, so I don't want it to get logged as a timeout, but there doesn't appear to be any difference between a cancellation and a real timeout.
Is there anyway to accomplish this?
Edit: I need to clarify this, a little bit. The service making the calls in parallel is passing in CancellationTokens with a timeout:
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(2));
So when the server takes more than two seconds to respond, I get an OperationCanceledException
, and if I manually cancel the token source (say because another server returned an error after 1 second), then I still get an OperationCanceledException
. Ideally, I would be able to look at CancellationToken.IsCancellationRequested
to determine if it was cancelled due to a timeout, as opposed to explicitly requested to be cancelled, but it appears that you get the same value regardless of how it was canceled.
If you want to distinguish the two cancellation types, then you need to use two different cancellation tokens. There's no other way. This is not too hard since they can be linked - just a bit awkward.
The cleanest way to write this IMO is to move the timeout code into the SendAsync
method instead of the calling method:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
cts.CancelAfter(TimeSpan.FromSeconds(2));
try
{
return await base.SendAsync(request, cancellationToken);
}
catch (OperationCanceledException ex)
{
if (cancellationToken.IsCancellationRequested)
return null;
LogTimeout(...);
throw;
}
catch (Exception ex)
{
LogFailure(...);
throw;
}
finally
{
LogComplete(...);
}
}
}
If you don't want to move the timeout code into SendAsync
, then you'll need to do the logging outside of that method, too.
If the exceptions aren't telling you the difference between the two cases then you will need to check with either the Task
or the CancellationToken
to see if there was actually a cancellation.
I would lean toward asking the Task
which will have its IsCanceled
property return true if an unhandled OperationCanceledException
was thrown (using CancellationToken.ThrowIfCancellationRequested
inside base.SendAsync
most likely). Something like this...
HttpResponseMessage response = null;
Task sendTask = null;
try
{
sendTask = base.SendAsync(request, cancellationToken);
await sendTask;
}
catch (OperationCanceledException ex)
{
if (!sendTask.IsCancelled)
{
LogTimeout(...);
throw;
}
}
EDIT
In response to the update to the question, I wanted to update my answer. You are right cancellation whether it is specifically requested on the CancellationTokenSource
or if it is caused by a timeout will lead to exactly the same outcome. If you decompile CancellationTokenSource
you will see that for the timeout it just sets a Timer
callback that will explicitly call CancellationTokenSource.Cancel
when the timeout is reached, so both ways will end up calling the same Cancel
method.
I think if you want to tell the difference you will need to derive from CancellationTokenSource
(it isn't a sealed
class) and then add your own custom cancel method that will set a flag to let you know that you explicitly cancelled the operation rather than letting it time out.
This is unfortunate since you will have both your custom cancel method and the original Cancel
method available and will have to be sure to use the custom one. You may be able to get away with your custom logic just hiding the existing Cancel
operation with something like this:
class CustomCancellationTokenSource : CancellationTokenSource
{
public bool WasManuallyCancelled {get; private set;}
public new void Cancel()
{
WasManuallyCancelled = true;
base.Cancel();
}
}
I would think that hiding the base method will work, you can give it a shot and find out.
来源:https://stackoverflow.com/questions/35615638/any-way-to-differentiate-cancel-and-timeout