您可以通过使用onRetry 函数来实现这一点。
为简单起见,让我定义 IsTransientError 和 GetSleepDurationByRetryAttempt 方法,如下所示:
public TimeSpan GetSleepDurationByRetryAttempt(int attempt) => TimeSpan.FromSeconds(attempt);
public bool IsTransientError(SqlException ex) => true;
顺便说一句,您可以通过避免(不必要的)匿名 lambda 来缩短您的策略定义:
var customPolicy = Policy
.Handle<SqlException>(IsTransientError)
.WaitAndRetryAsync(3, GetSleepDurationByRetryAttempt)
所以,回到 onRetry。 an overload 具有以下签名:Action<Exception, TimeSpan, Context>。这里的第二个参数是睡眠时长。
我们需要做的就是在这里提供一个函数,它会累积睡眠时长。
var totalSleepDuration = TimeSpan.Zero;
...
onRetry: (ex, duration, ctx) => { totalSleepDuration = totalSleepDuration.Add(duration); }
让我们把所有这些放在一起:
[Fact]
public async Task GivenACustomSleepDurationProvider_WhenIUseItInARetryPolicy_ThenTheAccumulatedDurationIsAsExpected()
{
//Arrange
var totalSleepDuration = TimeSpan.Zero;
var customPolicy = Policy
.Handle<SqlException>(IsTransientError)
.WaitAndRetryAsync(3, GetSleepDurationByRetryAttempt,
onRetry: (ex, duration, ctx) => { totalSleepDuration = totalSleepDuration.Add(duration); }
);
//Act
Func<Task> actionWithRetry = async() => await customPolicy.ExecuteAsync(() => throw new SqlException());
//Assert
_ = await Assert.ThrowsAsync<SqlException>(actionWithRetry);
Assert.Equal(6, totalSleepDuration.Seconds);
}
更新 #1:减少延迟并引入理论
根据您的要求,使用不同参数运行相同的测试用例可能是有意义的。这就是Theory 和InlineData 可以帮助您的地方:
[Theory]
[InlineData(3, 600)]
[InlineData(4, 1000)]
[InlineData(5, 1500)]
public async Task GivenACustomSleepDurationProvider_WhenIUseItInARetryPolicy_ThenTheAccumulatedDurationIsAsExpected(int retryCount, int expectedTotalSleepInMs)
{
//Arrange
var totalSleepDuration = TimeSpan.Zero;
var customPolicy = Policy
.Handle<SqlException>(IsTransientError)
.WaitAndRetryAsync(retryCount, GetSleepDurationByRetryAttempt,
onRetry: (ex, duration, ctx) => { totalSleepDuration = totalSleepDuration.Add(duration); }
);
//Act
Func<Task> actionWithRetry = async () => await customPolicy.ExecuteAsync(() => throw new SqlException());
//Assert
_ = await Assert.ThrowsAsync<SqlException>(actionWithRetry);
Assert.Equal(TimeSpan.FromMilliseconds(expectedTotalSleepInMs), totalSleepDuration);
}
public static TimeSpan GetSleepDurationByRetryAttempt(int attempt) => TimeSpan.FromMilliseconds(attempt * 100);
更新 #2:通过 Context 传递 TimeSpan
为了实现TimeSpana bit more type-safe的传输和检索,我们可以为此创建两个扩展方法:
public static class ContextExtensions
{
private const string Accumulator = "DurationAccumulator";
public static Context SetAccumulator(this Context context, TimeSpan durationAccumulator)
{
context[Accumulator] = durationAccumulator;
return context;
}
public static TimeSpan? GetAccumulator(this Context context)
{
if (!context.TryGetValue(Accumulator, out var ts))
return null;
if (ts is TimeSpan accumulator)
return accumulator;
return null;
}
}
我们还可以提取Policy创建逻辑:
private AsyncPolicy GetCustomPolicy(int retryCount)
=> Policy
.Handle<SqlException>(IsTransientError)
.WaitAndRetryAsync(retryCount, GetSleepDurationByRetryAttempt,
onRetry: (ex, duration, ctx) =>
{
var totalSleepDuration = ctx.GetAccumulator();
if (!totalSleepDuration.HasValue) return;
totalSleepDuration = totalSleepDuration.Value.Add(duration);
ctx.SetAccumulator(totalSleepDuration.Value);
});
现在让我们把所有这些放在一起(再一次):
[Theory]
[InlineData(3, 600)]
[InlineData(4, 1000)]
[InlineData(5, 1500)]
public async Task GivenACustomSleepDurationProvider_WhenIUseItInARetryPolicy_ThenTheAccumulatedDurationIsAsExpected(
int retryCount, int expectedTotalSleepInMs)
{
//Arrange
var totalSleepDuration = TimeSpan.Zero;
var customPolicy = GetCustomPolicy(retryCount);
var context = new Context().SetAccumulator(totalSleepDuration);
//Act
Func<Task> actionWithRetry = async () => await customPolicy.ExecuteAsync(ctx => throw new SqlException(), context);
//Assert
_ = await Assert.ThrowsAsync<SqlException>(actionWithRetry);
var accumulator = context.GetAccumulator();
Assert.NotNull(accumulator);
Assert.Equal(TimeSpan.FromMilliseconds(expectedTotalSleepInMs), accumulator.Value);
}