任何区分取消和超时的方法

时间:2016-02-24 23:55:42

标签: c# http dotnet-httpclient cancellation cancellationtokensource

我有一些代码通过调用许多其他服务来验证某些数据。我并行启动所有调用,然后等到其中至少一个完成。如果任何请求失败,我不关心其他调用的结果。

我使用HttpClient进行调用,并且我已经通过HttpMessageHandler进行了大量的日志记录。基本上:

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;
}

当我取消请求时,我遇到麻烦的部分没有。当我取消请求时,我是故意这样做的,所以我不希望它被记录为超时,但是在取消和实际超时之间似乎没有任何区别。

无论如何要做到这一点?

编辑:我需要澄清一下这一点。并行调用的服务是使用超时传递CancellationTokens:

var ct = new CancellationTokenSource(TimeSpan.FromSeconds(2));

因此,当服务器响应时间超过两秒时,我得到OperationCanceledException,如果我手动取消令牌源(比如因为另一台服务器在1秒后返回错误),那么我仍然得到一个OperationCanceledException。理想情况下,我可以查看CancellationToken.IsCancellationRequested以确定它是否因超时而被取消,而不是明确要求取消,但看起来无论如何< / em>它被取消了。

2 个答案:

答案 0 :(得分:8)

如果要区分两种取消类型,则需要使用两种不同的取消令牌。别无他法。这不是太难,因为他们can be linked - 只是有点尴尬。

编写此IMO最简洁的方法是将超时代码移动到SendAsync方法而不是调用方法:

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(...);
    }
  }
}

如果您不想将超时代码移动到SendAsync,那么您还需要在该方法之外进行日志记录。

答案 1 :(得分:3)

如果例外情况没有告诉您两种情况之间的区别,那么您需要与TaskCancellationToken核对,看看是否确实有取消。< / p>

如果抛出未处理的Task(在IsCanceled内使用OperationCanceledException,我会倾向于询问CancellationToken.ThrowIfCancellationRequested哪个base.SendAsync属性返回true可能)。像这样......

HttpResponseMessage response = null;
Task sendTask = null;

try
{
  sendTask = base.SendAsync(request, cancellationToken);
  await sendTask;
}
catch (OperationCanceledException ex)
{
  if (!sendTask.IsCancelled)
  {
    LogTimeout(...);
    throw;
  }
}

修改

在回答问题的更新时,我想更新我的答案。您是正确的取消是否在CancellationTokenSource上特别要求,或者是否由超时引起将导致完全相同的结果。如果您反编译CancellationTokenSource,您会看到超时时它只设置一个Timer回调,当达到超时时将显式调用CancellationTokenSource.Cancel,因此两种方式最终都会调用相同的Cancel {1}}方法。

我想如果你想告诉差异你需要从CancellationTokenSource派生(它不是sealed类)然后添加你自己的自定义取消方法来设置一个标志让你知道你明确取消了操作,而不是让它超时。

这很不幸,因为您可以使用自定义取消方法和原始Cancel方法,并且必须确保使用自定义方法。您可以通过以下方式隐藏现有的Cancel操作来逃避自定义逻辑:

class CustomCancellationTokenSource : CancellationTokenSource
{
  public bool WasManuallyCancelled {get; private set;}

  public new void Cancel()
  {
    WasManuallyCancelled = true;
    base.Cancel();
  }
}

我认为隐藏基本方法会起作用,你可以试一试并找出答案。