I have configured a retry based pipeline with Polly.
// Using Polly for retry logic
private readonly ResiliencePipeline _retryPipeline = new ResiliencePipelineBuilder { TimeProvider = timeProvider }
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<ConditionalCheckFailedException>(),
Delay = TimeSpan.FromMilliseconds(_backoffFactor),
MaxRetryAttempts = _maxAttempts - 1,
// Linear backoff increases the delay each time by the backoff factor
BackoffType = DelayBackoffType.Linear,
OnRetry = onRetryArguments =>
{
logger.LogWarning(
"Failed to acquire lock. Retrying. {@LogContext}",
new { onRetryArguments });
return ValueTask.CompletedTask;
}
})
.Build();
Which I execute using
// Attempt to store the lock with backoff retry
LockResult result = await _retryPipeline.ExecuteAsync(
async _ => await AttemptLockStorageAsync(lockId, expiryMilliseconds, attempts++),
cancellationTokenSource.Token);
When unit testing, I find that I have to add a Task.Delay(1) in order for Polly to perform the retries
// Act
Func<Task<DistributedLock>> func = async () =>
{
Task<DistributedLock> result = _distributedLockService.AcquireLockAsync(lockId);
for (int i = 1; i <= 4; i++)
{
_timeProvider.Advance(TimeSpan.FromMilliseconds(1000 * i + 1));
await Task.Delay(1);
}
return await result;
};
// Assert
// We expect that we should be able to attempt 5 full times, rather than getting a TaskCancelledException.
(await func.Should().ThrowAsync<TimeoutException>()).WithMessage(
$"Could not acquire lock {lockId}. Attempted 5 times.");
Why is the Task.Delay necessary?
Edit
TimeProvider provided to the SUT via the primary constructor.
public class DistributedLockService(
IDistributedLockRepository distributedLockRepository,
ILogger<DistributedLockService> logger,
TimeProvider timeProvider)
: IDisposable, IDistributedLockService
FakeTimer is provided in the unit test constructor
private readonly FakeTimeProvider _timeProvider = new();
public DistributedLockServiceTests()
{
_timeProvider.SetUtcNow(DateTimeOffset.Parse("2024-01-23", CultureInfo.InvariantCulture));
_distributedLockService = new DistributedLockService(
_distributedLockRepository.Object,
_logger.Object,
_timeProvider);
}
Filed a bug report based on minimal reproduction https://github.com/App-vNext/Polly/issues/1932
First let me show you the working code
The trick here is the following: call the
Advancein awhileloop not in aforloop.To test failure scenario the
Actpart needs to be adjusted like this