22

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.

Ben Randall
  • 1,205
  • 10
  • 27
  • Having the last date time of the client stored. Then check it with the current time. If the difference in date time is greater than the timeout time, then the client is considered timeout. Otherwise, it is a cancel. Something along that line. – Ian Feb 25 '16 at 00:02
  • You can just check the cancellation token for whether a cancel has been requested or not. Even if a race condition occurs, and the request times out just as you want to cancel it, you probably still want to ignore it since you did cancel it. – SimpleVar Feb 25 '16 at 00:08

2 Answers2

17

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, cts.Token);
    }
    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.

Anderson Rancan
  • 366
  • 5
  • 13
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for the suggestion. I may be able to get away with doing something like this if I can say that all the calls being made with the given `HttpClient` should have the same timeout, and I can just make it a parameter when constructing the `HttpMessageHandler`, otherwise I think I'll have to stick with the CustomCancellationTokenSource solution provided by Chiune. – Ben Randall Feb 25 '16 at 18:53
8

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.

AndreyAkinshin
  • 18,603
  • 29
  • 96
  • 155
Chiune Sugihara
  • 1,179
  • 1
  • 7
  • 14
  • I added a clarification to my message stating that I'm dealing with two scenarios: 1. I call `CancellationTokenSource.Cancel()` to request that the task get's cancelled, or 2. The timeout provided to my `CancellationTokenSource` expires and cancels the request for me. This is so that I don't wait for 30 seconds for each request to return, but unfortunately means that I get `OperationCanceledException` in both cases and it appears that `IsCancellationRequested` is true in both cases. – Ben Randall Feb 25 '16 at 17:45
  • Updated the answer, let me know what you think. – Chiune Sugihara Feb 25 '16 at 18:02
  • The CustomCancellationTokenSource is essentially what I have now. I exposed it as a new method: `CancelWithoutError()` which sets the flag. Unfortunately, in my `HttpMessageHandler`, I don't have access to the `CancellationTokenSource` without using reflection to pull it out of the `CancellationToken` :(. Instead, I set a static reference to point to the "CurrentSafeCancellationTokenSource", and in my handler, I check if there is a safe token source is use and if so, I check if a request was manually cancelled. That glosses over the details a bit, but it's essentially how it works. – Ben Randall Feb 25 '16 at 18:34
  • I don't know that you can do any better than that in a simple manner. If you want to get complicated you could analyze the stack trace of the thrown exception, the `Cancel` call is the result of some internal calls if it is a timeout so you could use that to differentiate the two. – Chiune Sugihara Feb 25 '16 at 18:38
  • Unfortunately the exception gets thrown when the caller (the base `HttpClient` in this case) checks if they have been cancelled, so you don't even get to see the `Cancel` call as part of the stack. – Ben Randall Feb 25 '16 at 18:42
  • Checking that stack would be more complicated than that. I think what you are looking at is the call that will throw if cancellation is requested. What you would likely need to do is register a cancellation callback using one of the `CancellationToken.Register` overloads. That will get fired whenever cancellation is requested and from there the stacks for the two paths should be different. You can pass in an argument so you could set outside state to let you know which outcome it was. Again this is getting more into really complicated territory which I wouldn't recommend... – Chiune Sugihara Feb 25 '16 at 19:16
  • Yup, I looked into `CancellationToken.Register` too but it had the unfortunate problem of not necessarily being called before the exception handler was called. :( – Ben Randall Feb 26 '16 at 19:08
  • For me it should be the accepted answer because it is the only one that actually works – bN_ Sep 01 '20 at 09:30