使用 Polly 反对 Task.TimeoutAfter

Using Polly in oppose to Task.TimeoutAfter

如何使用 Polly 来对抗 TimeoutAfter

我想在下面的代码中使用 Polly 而不是 TimeoutAfter:

public async Task StartAsync()
{
    await _webSocket.ConnectAsync(_uri, CancellationToken.None).TimeoutAfter(OpenTimeoutMs).ConfigureAwait(false);

    ...

    await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None).TimeoutAfter(CloseTimeoutMs);
}

public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
{
    try
    {
        await task.WaitAsync(timeout).ConfigureAwait(false);
    }
    catch (TimeoutException ex)
    {
        throw new TimeoutException($"Task timed out after {timeout}");
    }
}

我的想法(不确定我是否正确)

为了让 Polly 处理事情,我需要将 CancellationToken 传递给 StartAsync 并用它替换 CancellationToken.None

我是这么想的,不知道对不对。这就是我问这个问题的原因。

var timeoutPolicy = Policy
    .Handle<TimeoutException>()
    .TimeoutAsync(TimeSpan.FromMilliseconds(timeoutMs), TimeoutStrategy.Optimistic,
        (context, timeSpan, task, ex) =>
        {
            Console.WriteLine($"Task timed out after {timeSpan.TotalSeconds} seconds");
            return Task.CompletedTask;
        });

await retryPolicy.ExecuteAsync(async (ct) =>
{
    await client.StartAsync(ct);
}, CancellationToken.None);

// changed the method to accept CancellationToken
public async Task StartAsync(CancellationToken ct = default)
{
    await _webSocket.ConnectAsync(_uri, ct).ConfigureAwait(false);

    ...

    await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", ct).ConfigureAwait(false);
}

您的 TimeoutAfter 扩展方法被应用了两次,分别用于 ConnectAsyncCloseAsync 方法。这意味着每个方法调用都有一个“本地”超时。

在您的 Polly 版本中,您定义了一个“全局”/“总体”超时,其中包括从 ConnectAsyncCloseAsync 的所有内容。

因此,这两个版本相同。

顺便说一句,你的代码有一些问题:

  • TimeoutAsync 不能有 Handle 子句
  • retryPolicy 未在此范围内定义

您可以将 TimeoutAfter 代码替换为此代码以利用 Polly:

public static async Task TimeoutAfter(Func<CancellationToken, Task> task, CancellationToken token, TimeSpan timeout)
{
    try
    {
        await CreateTimeoutConstraint(timeout)
            .ExecuteAsync(async (ct) => await task(ct), token)
            .ConfigureAwait(false);
    }
    catch (TimeoutRejectedException ex)
    {
        throw new OperationCanceledException($"Task timed out after {timeout}", ex);
    }
}

private static IAsyncPolicy CreateTimeoutConstraint(TimeSpan threshold)
    => Policy.TimeoutAsync(threshold, TimeoutStrategy.Optimistic,
    (context, timeSpan, task, ex) =>
    {
        Console.WriteLine($"Task timed out after {timeSpan.TotalSeconds} seconds");
        return Task.CompletedTask;
    });
  • 为了尊重用户取消令牌和超时取消令牌,您必须更改签名
    • 该方法应该接收一个函数,该函数预期 CancellationToken 和 returns 一个 Task
    • 它还应该收到用户取消令牌
  • ExecuteAsync 中调用您“组合”TimeoutPolicy 和用户取消令牌
    • 如果 token 先请求取消,那么 ExecuteAsync 将抛出一个 TaskCanceledException
    • 如果 TimeoutPolicy 首先请求取消,那么 ExecuteAsync 将抛出一个 TimeoutRejectedException,它被转换为一个 OperationCanceledException

这个方法的用法是这样的:

await TimeoutAfter((token) => _webSocket.ConnectAsync(_uri, token), ct, OpenTimeoutMs);

...

await TimeoutAfter((token) => _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", token), ct, CloseTimeoutMs);