任何区分取消和超时的方法
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
以确定它是否因超时而被取消,而不是明确请求取消,但无论 怎么取消了。
如果异常没有告诉您这两种情况之间的区别,那么您将需要检查 Task
或 CancellationToken
以查看是否确实存在取消。
我倾向于询问 Task
如果未处理的 OperationCanceledException
被抛出(使用 CancellationToken.ThrowIfCancellationRequested
在 base.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
,那么您也需要在该方法之外进行日志记录。
我有一些代码通过调用许多其他服务来验证某些数据。我并行启动所有调用,然后等到至少其中一个调用结束。如果任何请求失败,我不关心其他调用的结果。
我使用 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
以确定它是否因超时而被取消,而不是明确请求取消,但无论 怎么取消了。
如果异常没有告诉您这两种情况之间的区别,那么您将需要检查 Task
或 CancellationToken
以查看是否确实存在取消。
我倾向于询问 Task
如果未处理的 OperationCanceledException
被抛出(使用 CancellationToken.ThrowIfCancellationRequested
在 base.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
,那么您也需要在该方法之外进行日志记录。