Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/Polly.Core/ToBeRemoved/TimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ public DateTimeOffset GetLocalNow()

public virtual long GetTimestamp() => Stopwatch.GetTimestamp();

// This one is not on TimeProvider, temporarly we need to use it
public virtual Task Delay(TimeSpan delay, CancellationToken cancellationToken = default) => Task.Delay(delay, cancellationToken);

public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
{
long timestampFrequency = TimestampFrequency;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,11 @@ public async Task TryWaitForCompletedExecutionAsync_HedgedExecution_Ok()
}

var hedgingDelay = TimeSpan.FromSeconds(5);
var count = _timeProvider.DelayEntries.Count;
var count = _timeProvider.TimerEntries.Count;
var task = context.TryWaitForCompletedExecutionAsync(hedgingDelay).AsTask();
task.Wait(20).Should().BeFalse();
_timeProvider.DelayEntries.Should().HaveCount(count + 1);
_timeProvider.DelayEntries.Last().Delay.Should().Be(hedgingDelay);
_timeProvider.TimerEntries.Should().HaveCount(count + 1);
_timeProvider.TimerEntries.Last().Delay.Should().Be(hedgingDelay);
_timeProvider.Advance(TimeSpan.FromDays(1));
await task;
await context.Tasks[0].ExecutionTaskSafe!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ public void ExecutePrimaryAndSecondary_EnsureAttemptReported()
var attempts = _events.Select(v => v.Arguments).OfType<ExecutionAttemptArguments>().ToArray();

attempts[0].Handled.Should().BeTrue();
attempts[0].ExecutionTime.Should().Be(_timeProvider.GetElapsedTime(0, 1000));
attempts[0].ExecutionTime.Should().BeGreaterThan(TimeSpan.Zero);
attempts[0].Attempt.Should().Be(0);

attempts[1].Handled.Should().BeTrue();
attempts[1].ExecutionTime.Should().Be(_timeProvider.GetElapsedTime(0, 1000));
attempts[1].ExecutionTime.Should().BeGreaterThan(TimeSpan.Zero);
attempts[1].Attempt.Should().Be(1);
}

Expand Down Expand Up @@ -164,7 +164,7 @@ public async Task ExecuteAsync_ShouldReturnAnyPossibleResult()
var result = await strategy.ExecuteAsync(_primaryTasks.SlowTask);

result.Should().NotBeNull();
_timeProvider.DelayEntries.Should().HaveCount(5);
_timeProvider.TimerEntries.Should().HaveCount(5);
result.Should().Be("Oranges");
}

Expand Down Expand Up @@ -865,7 +865,7 @@ public async Task ExecuteAsync_EnsureHedgingDelayGeneratorRespected()

_timeProvider.Advance(TimeSpan.FromHours(5));
(await task).Should().Be(Success);
_timeProvider.DelayEntries.Should().Contain(e => e.Delay == delay);
_timeProvider.TimerEntries.Should().Contain(e => e.Delay == delay);
}

[Fact]
Expand Down
33 changes: 22 additions & 11 deletions test/Polly.Core.Tests/Hedging/HedgingTimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,44 @@ public void Advance(TimeSpan diff)
{
_utcNow = _utcNow.Add(diff);

foreach (var entry in DelayEntries.Where(e => e.TimeStamp <= _utcNow))
foreach (var entry in TimerEntries.Where(e => e.TimeStamp <= _utcNow))
{
entry.Complete();
}
}

public Func<int> TimeStampProvider { get; set; } = () => 0;

public List<DelayEntry> DelayEntries { get; } = new List<DelayEntry>();
public List<TimerEntry> TimerEntries { get; } = new List<TimerEntry>();

public override DateTimeOffset GetUtcNow() => _utcNow;

public override long GetTimestamp() => TimeStampProvider();

public override Task Delay(TimeSpan delayValue, CancellationToken cancellationToken = default)
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
var entry = new DelayEntry(delayValue, new TaskCompletionSource<bool>(), _utcNow.Add(delayValue));
cancellationToken.Register(() => entry.Source.TrySetCanceled(cancellationToken));
DelayEntries.Add(entry);
var entry = new TimerEntry(dueTime, new TaskCompletionSource<bool>(), _utcNow.Add(dueTime), () => callback(state));
TimerEntries.Add(entry);

Advance(AutoAdvance);

return entry.Source.Task;
return entry;
}

public record DelayEntry(TimeSpan Delay, TaskCompletionSource<bool> Source, DateTimeOffset TimeStamp)
public record TimerEntry(TimeSpan Delay, TaskCompletionSource<bool> Source, DateTimeOffset TimeStamp, Action Callback) : ITimer
{
public void Complete() => Source.TrySetResult(true);
public bool Change(TimeSpan dueTime, TimeSpan period) => throw new NotSupportedException();

public void Complete()
{
Callback();
Source.TrySetResult(true);
}

public void Dispose() => Source.TrySetResult(true);

public ValueTask DisposeAsync()
{
Source.TrySetResult(true);
return default;
}
}
}
35 changes: 29 additions & 6 deletions test/Polly.Core.Tests/Helpers/MockTimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,44 @@ public MockTimeProvider()
{
}

public MockTimeProvider SetupAnyDelay(CancellationToken cancellationToken = default)
public MockTimeProvider SetupAnyCreateTimer()
{
Setup(x => x.Delay(It.IsAny<TimeSpan>(), cancellationToken)).Returns(Task.CompletedTask);
Setup(t => t.CreateTimer(It.IsAny<TimerCallback>(), It.IsAny<object?>(), It.IsAny<TimeSpan>(), It.IsAny<TimeSpan>()))
.Callback((TimerCallback callback, object? state, TimeSpan _, TimeSpan _) => callback(state))
.Returns(Of<ITimer>());

return this;
}

public MockTimeProvider SetupDelay(TimeSpan delay, CancellationToken cancellationToken = default)
public Mock<ITimer> SetupCreateTimer(TimeSpan delay)
{
var timer = new Mock<ITimer>(MockBehavior.Loose);

Setup(t => t.CreateTimer(It.IsAny<TimerCallback>(), It.IsAny<object?>(), delay, It.IsAny<TimeSpan>()))
.Callback((TimerCallback callback, object? state, TimeSpan _, TimeSpan _) => callback(state))
.Returns(timer.Object);

return timer;
}

public MockTimeProvider VerifyCreateTimer(Times times)
{
Setup(x => x.Delay(delay, cancellationToken)).Returns(Task.CompletedTask);
Verify(t => t.CreateTimer(It.IsAny<TimerCallback>(), It.IsAny<object?>(), It.IsAny<TimeSpan>(), It.IsAny<TimeSpan>()), times);
return this;
}

public MockTimeProvider SetupDelayCancelled(TimeSpan delay, CancellationToken cancellationToken = default)
public MockTimeProvider VerifyCreateTimer(TimeSpan delay, Times times)
{
Setup(x => x.Delay(delay, cancellationToken)).ThrowsAsync(new OperationCanceledException());
Verify(t => t.CreateTimer(It.IsAny<TimerCallback>(), It.IsAny<object?>(), delay, It.IsAny<TimeSpan>()), times);
return this;
}

public MockTimeProvider SetupCreateTimerException(TimeSpan delay, Exception exception)
{
Setup(t => t.CreateTimer(It.IsAny<TimerCallback>(), It.IsAny<object?>(), delay, It.IsAny<TimeSpan>()))
.Callback((TimerCallback _, object? _, TimeSpan _, TimeSpan _) => throw exception)
.Returns(Of<ITimer>());

return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void HandleMultipleResults_898()
};

// create the strategy
var strategy = new ResilienceStrategyBuilder { TimeProvider = TimeProvider }.AddRetry(options).Build();
var strategy = new ResilienceStrategyBuilder().AddRetry(options).Build();

// check that int-based results is retried
bool isRetry = false;
Expand Down
72 changes: 40 additions & 32 deletions test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Time.Testing;
using Moq;
using Polly.Retry;
using Polly.Telemetry;
Expand All @@ -7,7 +8,7 @@ namespace Polly.Core.Tests.Retry;
public class RetryResilienceStrategyTests
{
private readonly RetryStrategyOptions _options = new();
private readonly MockTimeProvider _timeProvider = new();
private readonly FakeTimeProvider _timeProvider = new();
private readonly Mock<DiagnosticSource> _diagnosticSource = new();
private ResilienceStrategyTelemetry _telemetry;

Expand All @@ -16,8 +17,6 @@ public RetryResilienceStrategyTests()
_telemetry = TestUtilities.CreateResilienceTelemetry(_diagnosticSource.Object);
_options.ShouldHandle = _ => new ValueTask<bool>(false);

_timeProvider.Setup(v => v.TimestampFrequency).Returns(Stopwatch.Frequency);
_timeProvider.SetupSequence(v => v.GetTimestamp()).Returns(0).Returns(100);
}

[Fact]
Expand Down Expand Up @@ -74,7 +73,6 @@ public void ExecuteAsync_MultipleRetries_EnsureDiscardedResultsDisposed()
// arrange
_options.RetryCount = 5;
SetupNoDelay();
_timeProvider.SetupAnyDelay();
_options.ShouldHandle = _ => PredicateResult.True;
var results = new List<DisposableResult>();
var sut = CreateSut();
Expand Down Expand Up @@ -158,25 +156,22 @@ public void Retry_Infinite_Respected()
}

[Fact]
public void RetryDelayGenerator_Respected()
public async Task RetryDelayGenerator_Respected()
{
int calls = 0;
_options.OnRetry = _ => { calls++; return default; };
_options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0);
_options.RetryCount = 3;
_options.BackoffType = RetryBackoffType.Constant;
_options.RetryDelayGenerator = _ => new ValueTask<TimeSpan>(TimeSpan.FromMilliseconds(123));
_timeProvider.SetupDelay(TimeSpan.FromMilliseconds(123));

var sut = CreateSut();

sut.Execute(() => 0);

_timeProvider.Verify(v => v.Delay(TimeSpan.FromMilliseconds(123), default), Times.Exactly(3));
await ExecuteAndAdvance(sut);
}

[Fact]
public void OnRetry_EnsureCorrectArguments()
public async void OnRetry_EnsureCorrectArguments()
{
var attempts = new List<int>();
var delays = new List<TimeSpan>();
Expand All @@ -190,14 +185,15 @@ public void OnRetry_EnsureCorrectArguments()
return default;
};

_options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0);
_options.ShouldHandle = args => PredicateResult.True;
_options.RetryCount = 3;
_options.BackoffType = RetryBackoffType.Linear;
_timeProvider.SetupAnyDelay();

var sut = CreateSut();

sut.Execute(() => 0);
var executing = ExecuteAndAdvance(sut);

await executing;

attempts.Should().HaveCount(3);
attempts[0].Should().Be(0);
Expand All @@ -210,54 +206,56 @@ public void OnRetry_EnsureCorrectArguments()
}

[Fact]
public void OnRetry_EnsureExecutionTime()
public async Task OnRetry_EnsureExecutionTime()
{
_options.OnRetry = args =>
{
args.Arguments.ExecutionTime.Should().Be(_timeProvider.Object.GetElapsedTime(100, 1000));
args.Arguments.ExecutionTime.Should().Be(TimeSpan.FromMinutes(1));

return default;
};

_options.ShouldHandle = _ => PredicateResult.True;
_options.RetryCount = 1;
_options.BackoffType = RetryBackoffType.Linear;
_timeProvider.SetupAnyDelay();
_timeProvider
.SetupSequence(v => v.GetTimestamp())
.Returns(100)
.Returns(1000)
.Returns(100)
.Returns(1000);
_options.BackoffType = RetryBackoffType.Constant;
_options.BaseDelay = TimeSpan.Zero;

var sut = CreateSut();

sut.Execute(() => 0);
await sut.ExecuteAsync(_ =>
{
_timeProvider.Advance(TimeSpan.FromMinutes(1));
return new ValueTask<int>(0);
}).AsTask();
}

[Fact]
public void Execute_EnsureAttemptReported()
{
var called = false;
_timeProvider.SetupSequence(v => v.GetTimestamp()).Returns(100).Returns(1000);
_telemetry = TestUtilities.CreateResilienceTelemetry(args =>
{
var attempt = args.Arguments.Should().BeOfType<ExecutionAttemptArguments>().Subject;

attempt.Handled.Should().BeFalse();
attempt.Attempt.Should().Be(0);
attempt.ExecutionTime.Should().Be(_timeProvider.Object.GetElapsedTime(100, 1000));
attempt.ExecutionTime.Should().Be(TimeSpan.FromSeconds(1));
called = true;
});

var sut = CreateSut();

sut.Execute(() => 0);
sut.Execute(() =>
{
_timeProvider.Advance(TimeSpan.FromSeconds(1));
return 0;
});

called.Should().BeTrue();
}

[Fact]
public void OnRetry_EnsureTelemetry()
public async Task OnRetry_EnsureTelemetry()
{
var attempts = new List<int>();
var delays = new List<TimeSpan>();
Expand All @@ -267,11 +265,10 @@ public void OnRetry_EnsureTelemetry()
_options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0);
_options.RetryCount = 3;
_options.BackoffType = RetryBackoffType.Linear;
_timeProvider.SetupAnyDelay();

var sut = CreateSut();

sut.Execute(() => 0);
await ExecuteAndAdvance(sut);

_diagnosticSource.VerifyAll();
}
Expand All @@ -295,7 +292,6 @@ public void RetryDelayGenerator_EnsureCorrectArguments()
_options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0);
_options.RetryCount = 3;
_options.BackoffType = RetryBackoffType.Linear;
_timeProvider.SetupAnyDelay();

var sut = CreateSut();

Expand All @@ -313,10 +309,22 @@ public void RetryDelayGenerator_EnsureCorrectArguments()

private void SetupNoDelay() => _options.RetryDelayGenerator = _ => new ValueTask<TimeSpan>(TimeSpan.Zero);

private async ValueTask<int> ExecuteAndAdvance(RetryResilienceStrategy<object> sut)
{
var executing = sut.ExecuteAsync(_ => new ValueTask<int>(0)).AsTask();

while (!executing.IsCompleted)
{
_timeProvider.Advance(TimeSpan.FromMinutes(1));
}

return await executing;
}

private RetryResilienceStrategy<object> CreateSut(TimeProvider? timeProvider = null) =>
new(_options,
false,
timeProvider ?? _timeProvider.Object,
timeProvider ?? _timeProvider,
_telemetry,
() => 1.0);
}
6 changes: 3 additions & 3 deletions test/Polly.Core.Tests/Utils/TimeProviderExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public async Task DelayAsync_System_Ok(bool synchronous, bool mocked, bool hasCa
var context = ResilienceContext.Get();
context.Initialize<VoidResult>(isSynchronous: synchronous);
context.CancellationToken = token;
mock.SetupDelay(delay, token);
mock.SetupCreateTimer(delay);

await TestUtilities.AssertWithTimeoutAsync(async () =>
{
Expand Down Expand Up @@ -88,7 +88,7 @@ public async Task DelayAsync_CancellationRequestedBefore_Throws(bool synchronous
var context = ResilienceContext.Get();
context.Initialize<VoidResult>(isSynchronous: synchronous);
context.CancellationToken = token;
mock.SetupDelayCancelled(delay, token);
mock.SetupCreateTimer(delay);

await Assert.ThrowsAsync<OperationCanceledException>(() => timeProvider.DelayAsync(delay, context));
}
Expand All @@ -111,7 +111,7 @@ await TestUtilities.AssertWithTimeoutAsync(async () =>
var context = ResilienceContext.Get();
context.Initialize<VoidResult>(isSynchronous: synchronous);
context.CancellationToken = token;
mock.SetupDelayCancelled(delay, token);
mock.SetupCreateTimerException(delay, new OperationCanceledException());

tcs.CancelAfter(TimeSpan.FromMilliseconds(5));

Expand Down