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

Posted

技术标签:

【中文标题】任何区分取消和超时的方法【英文标题】:Any way to differentiate Cancel and Timeout 【发布时间】:2016-02-24 23:55:41 【问题描述】:

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

我使用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 秒后返回错误),那么我仍然会得到一个@987654326 @。理想情况下,我可以查看CancellationToken.IsCancellationRequested 来确定它是否由于超时而被取消,而不是明确要求取消,但无论如何,您似乎都获得了相同的值> 它被取消了。

【问题讨论】:

已存储客户端的最后日期时间。然后检查它与当前时间。如果日期时间的差异大于超时时间,则认为客户端超时。否则,就是取消。沿着这条线的东西。 您可以只检查取消令牌是否已请求取消。即使发生竞争条件,并且请求在您想取消它时超时,您可能仍然想忽略它,因为您确实取消了它。 【参考方案1】:

如果要区分这两种取消类型,则需要使用两种不同的取消令牌。没有别的办法。这并不难,因为他们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,那么您也需要在该方法之外进行日志记录。

【讨论】:

感谢您的建议。如果我可以说使用给定的HttpClient 进行的所有调用都应该具有相同的超时时间,并且我可以在构造HttpMessageHandler 时将其作为参数,那么我可能能够摆脱这种做法,否则我想我必须坚持使用 Chiune 提供的 CustomCancellationTokenSource 解决方案。【参考方案2】:

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

如果抛出未处理的OperationCanceledException(最有可能在base.SendAsync 内使用CancellationToken.ThrowIfCancellationRequested),我倾向于询问Task,它的IsCanceled 属性将返回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方法。

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

这很不幸,因为您将同时使用自定义取消方法和原始Cancel 方法,并且必须确保使用自定义取消方法。您可能能够摆脱您的自定义逻辑,只需隐藏现有的 Cancel 操作,如下所示:

class CustomCancellationTokenSource : CancellationTokenSource

  public bool WasManuallyCancelled get; private set;

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

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

【讨论】:

我在消息中添加了一个说明,说明我正在处理两种情况:1. 我致电 CancellationTokenSource.Cancel() 请求取消任务,或者 2. 提供给我的 @987654340 的超时@ 过期并为我取消请求。这样我就不会等待每个请求返回 30 秒,但不幸的是,这意味着我在两种情况下都得到了OperationCanceledException,而且似乎IsCancellationRequested 在两种情况下都是正确的。 更新了答案,让我知道你的想法。 CustomCancellationTokenSource 基本上就是我现在所拥有的。我将它作为一种新方法公开:CancelWithoutError() 设置标志。不幸的是,在我的HttpMessageHandler 中,如果不使用反射将其从CancellationToken 中拉出,我将无法访问CancellationTokenSource :(。相反,我设置了一个指向“CurrentSafeCancellationTokenSource”的静态引用,并且在我的处理程序中,我检查是否使用了安全令牌源,如果是,我检查是否手动取消了请求。这有点掩盖了细节,但它本质上是它的工作原理。 我不知道你能以简单的方式做得更好。如果你想变得复杂,你可以分析抛出异常的堆栈跟踪,Cancel 调用是一些内部调用的结果,如果它是超时的,那么你可以用它来区分两者。 不幸的是,当调用者(在本例中为基本 HttpClient)检查它们是否已被取消时,异常被抛出,所以你甚至看不到 Cancel 调用作为堆栈。

以上是关于任何区分取消和超时的方法的主要内容,如果未能解决你的问题,请参考以下文章

CompletableFuture 异步超时 和取消

如何区分 curl 最大时间和连接超时?

操作成功完成后是否应取消Twisted超时?

有没有办法区分连接超时和套接字超时?

《CLR via C#》之线程处理——协作式取消和超时

订单超时自动取消golang