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

Any way to differentiate Cancel and Timeout

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

我使用 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 以确定它是否因超时而被取消,而不是明确请求取消,但无论 怎么取消了。

如果异常没有告诉您这两种情况之间的区别,那么您将需要检查 TaskCancellationToken 以查看是否确实存在取消。

我倾向于询问 Task 如果未处理的 OperationCanceledException 被抛出(使用 CancellationToken.ThrowIfCancellationRequestedbase.SendAsync 最有可能)。像这样...

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方法。

我想如果你想区分你需要从 CancellationTokenSource 派生(它不是 sealed class)然后添加你自己的自定义取消方法这将设置一个标志,让您知道您明确取消了该操作,而不是让它超时。

这很不幸,因为您将同时使用自定义取消方法和原始 Cancel 方法,并且必须确保使用自定义方法。您也许可以通过隐藏现有的 Cancel 操作来摆脱您的自定义逻辑,例如:

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

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

我认为隐藏基本方法会起作用,你可以试试看。

如果要区分这两种取消类型,则需要使用两种不同的取消标记。没有别的办法。这并不难,因为他们 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, cts.Token);
    }
    catch (OperationCanceledException ex)
    {
      if (cancellationToken.IsCancellationRequested)
        return null;
      LogTimeout(...);
      throw;
    }
    catch (Exception ex)
    {
      LogFailure(...);
      throw;
    }
    finally
    {
      LogComplete(...);
    }
  }
}

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