Skip to content

Commit

Permalink
Introduce TimeProvider (#1077)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Mar 22, 2023
1 parent 9c5c1ec commit 36ceee0
Show file tree
Hide file tree
Showing 12 changed files with 384 additions and 2 deletions.
8 changes: 8 additions & 0 deletions eng/Library.targets
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
<PackageReleaseNotes>See https://github.com/App-vNext/Polly/blob/master/CHANGELOG.md for details</PackageReleaseNotes>
</PropertyGroup>

<Target Name="AddInternalsVisibleToDynamicProxyGenAssembly2" BeforeTargets="BeforeCompile">
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Target>

<Target Name="AddInternalsVisibleToTest" BeforeTargets="BeforeCompile">
<ItemGroup>
<InternalsVisibleTo Include="%(InternalsVisibleToTest.Identity)$(PollyPublicKeySuffix)" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using FluentAssertions;
using Polly.Builder;
using Polly.Telemetry;
using Polly.Utils;
using Xunit;

namespace Polly.Core.Tests.Builder;
Expand All @@ -12,5 +14,8 @@ public void Ctor_EnsureDefaults()
var options = new ResilienceStrategyBuilderOptions();

options.BuilderName.Should().Be("");
options.Properties.Should().NotBeNull();
options.TimeProvider.Should().Be(TimeProvider.System);
options.TelemetryFactory.Should().Be(NullResilienceTelemetryFactory.Instance);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ public void BuildStrategy_EnsureCorrectContext()
{
Options = new ResilienceStrategyBuilderOptions
{
BuilderName = "builder-name"
BuilderName = "builder-name",
TimeProvider = new FakeTimeProvider().Object
}
};

Expand All @@ -265,6 +266,7 @@ public void BuildStrategy_EnsureCorrectContext()
context.BuilderProperties.Should().BeSameAs(builder.Options.Properties);
context.Telemetry.Should().NotBeNull();
context.Telemetry.Should().Be(NullResilienceTelemetry.Instance);
context.TimeProvider.Should().Be(builder.Options.TimeProvider);
verified1 = true;
return new TestResilienceStrategy();
Expand All @@ -280,6 +282,7 @@ public void BuildStrategy_EnsureCorrectContext()
context.BuilderProperties.Should().BeSameAs(builder.Options.Properties);
context.Telemetry.Should().NotBeNull();
context.Telemetry.Should().Be(NullResilienceTelemetry.Instance);
context.TimeProvider.Should().Be(builder.Options.TimeProvider);
verified2 = true;
return new TestResilienceStrategy();
Expand Down
29 changes: 29 additions & 0 deletions src/Polly.Core.Tests/Utils/FakeTimeProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Moq;
using Polly.Utils;

namespace Polly.Core.Tests.Utils;

internal class FakeTimeProvider : Mock<TimeProvider>
{
public FakeTimeProvider(long frequency)
: base(MockBehavior.Strict, frequency)
{
}

public FakeTimeProvider()
: this(Stopwatch.Frequency)
{
}

public FakeTimeProvider SetupDelay(TimeSpan delay, CancellationToken cancellationToken = default)
{
Setup(x => x.Delay(delay, cancellationToken)).Returns(Task.CompletedTask);
return this;
}

public FakeTimeProvider SetupDelayCancelled(TimeSpan delay, CancellationToken cancellationToken = default)
{
Setup(x => x.Delay(delay, cancellationToken)).ThrowsAsync(new OperationCanceledException());
return this;
}
}
93 changes: 93 additions & 0 deletions src/Polly.Core.Tests/Utils/SystemTimeProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using FluentAssertions;
using Polly.Utils;
using Xunit;

namespace Polly.Core.Tests.Utils;

public class SystemTimeProviderTests
{
[Fact]
public void TimestampFrequency_Ok()
{
TimeProvider.System.TimestampFrequency.Should().Be(Stopwatch.Frequency);
}

[Fact]
public async Task CancelAfter_Ok()
{
await TestUtils.AssertWithTimeoutAsync(async () =>
{
using var cts = new CancellationTokenSource();
TimeProvider.System.CancelAfter(cts, TimeSpan.FromMilliseconds(10));
cts.IsCancellationRequested.Should().BeFalse();
await Task.Delay(10);
cts.Token.IsCancellationRequested.Should().BeTrue();
});
}

[Fact]
public async Task Delay_Ok()
{
using var cts = new CancellationTokenSource();

await TestUtils.AssertWithTimeoutAsync(() =>
{
TimeProvider.System.Delay(TimeSpan.FromMilliseconds(10)).IsCompleted.Should().BeFalse();
});
}

[Fact]
public void Delay_NoDelay_Ok()
{
using var cts = new CancellationTokenSource();

TimeProvider.System.Delay(TimeSpan.Zero).IsCompleted.Should().BeTrue();
}

[Fact]
public async Task GetElapsedTime_Ok()
{
var delay = TimeSpan.FromMilliseconds(10);
var delayWithTolerance = TimeSpan.FromMilliseconds(30);

await TestUtils.AssertWithTimeoutAsync(async () =>
{
var stamp1 = TimeProvider.System.GetTimestamp();
await Task.Delay(10);
var stamp2 = TimeProvider.System.GetTimestamp();
var elapsed = TimeProvider.System.GetElapsedTime(stamp1, stamp2);
elapsed.Should().BeGreaterThanOrEqualTo(delay);
elapsed.Should().BeLessThan(delayWithTolerance);
});
}

[Fact]
public void GetElapsedTime_Mocked_Ok()
{
var provider = new FakeTimeProvider(40);
provider.SetupSequence(v => v.GetTimestamp()).Returns(120000).Returns(480000);

var stamp1 = provider.Object.GetTimestamp();
var stamp2 = provider.Object.GetTimestamp();

var delay = provider.Object.GetElapsedTime(stamp1, stamp2);

var tickFrequency = (double)TimeSpan.TicksPerSecond / 40;
var expected = new TimeSpan((long)((stamp2 - stamp1) * tickFrequency));

delay.Should().Be(expected);
}

[Fact]
public async Task UtcNow_Ok()
{
await TestUtils.AssertWithTimeoutAsync(() =>
{
var now = TimeProvider.System.UtcNow;
(DateTimeOffset.UtcNow - now).Should().BeLessThanOrEqualTo(TimeSpan.FromMilliseconds(10));
});
}

}
36 changes: 36 additions & 0 deletions src/Polly.Core.Tests/Utils/TestUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;

namespace Polly.Core.Tests.Utils;

#pragma warning disable CA1031 // Do not catch general exception types

public static class TestUtils
{
public static Task AssertWithTimeoutAsync(Func<Task> assertion) => AssertWithTimeoutAsync(assertion, TimeSpan.FromSeconds(60));

public static Task AssertWithTimeoutAsync(Action assertion) => AssertWithTimeoutAsync(
() =>
{
assertion();
return Task.CompletedTask;
},
TimeSpan.FromSeconds(60));

public static async Task AssertWithTimeoutAsync(Func<Task> assertion, TimeSpan timeout)
{
var watch = Stopwatch.StartNew();

while (true)
{
try
{
await assertion();
return;
}
catch (Exception) when (watch.Elapsed < timeout)
{
await Task.Delay(5);
}
}
}
}
89 changes: 89 additions & 0 deletions src/Polly.Core.Tests/Utils/TimeProviderExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using FluentAssertions;
using Polly.Utils;
using Xunit;

namespace Polly.Core.Tests.Utils;

public class TimeProviderExtensionsTests
{
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, true)]
[InlineData(false, true, false)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, true)]
[InlineData(true, true, false)]
[Theory]
public async Task DelayAsync_System_Ok(bool synchronous, bool mocked, bool hasCancellation)
{
using var tcs = new CancellationTokenSource();
var token = hasCancellation ? tcs.Token : default;
var delay = TimeSpan.FromMilliseconds(10);
var mock = new FakeTimeProvider();
var timeProvider = mocked ? mock.Object : TimeProvider.System;
var context = ResilienceContext.Get();
context.Initialize<VoidResult>(isSynchronous: synchronous);
context.CancellationToken = token;
mock.SetupDelay(delay, token);

await TestUtils.AssertWithTimeoutAsync(async () =>
{
var task = timeProvider.DelayAsync(delay, context);
task.IsCompleted.Should().Be(synchronous || mocked);
await task;
});

if (mocked)
{
mock.VerifyAll();
}
}

[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
[Theory]
public async Task DelayAsync_CancellationRequestedbefore_Throws(bool synchronous, bool mocked)
{
using var tcs = new CancellationTokenSource();
tcs.Cancel();
var token = tcs.Token;
var delay = TimeSpan.FromMilliseconds(10);
var mock = new FakeTimeProvider();
var timeProvider = mocked ? mock.Object : TimeProvider.System;
var context = ResilienceContext.Get();
context.Initialize<VoidResult>(isSynchronous: synchronous);
context.CancellationToken = token;
mock.SetupDelayCancelled(delay, token);

await Assert.ThrowsAsync<OperationCanceledException>(() => timeProvider.DelayAsync(delay, context));
}

[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
[Theory]
public async Task DelayAsync_CancellationAfter_Throws(bool synchronous, bool mocked)
{
var delay = TimeSpan.FromMilliseconds(20);

await TestUtils.AssertWithTimeoutAsync(async () =>
{
var mock = new FakeTimeProvider();
using var tcs = new CancellationTokenSource();
var token = tcs.Token;
var timeProvider = mocked ? mock.Object : TimeProvider.System;
var context = ResilienceContext.Get();
context.Initialize<VoidResult>(isSynchronous: synchronous);
context.CancellationToken = token;
mock.SetupDelayCancelled(delay, token);
tcs.CancelAfter(TimeSpan.FromMilliseconds(5));
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => timeProvider.DelayAsync(delay, context));
});
}
}
3 changes: 2 additions & 1 deletion src/Polly.Core/Builder/ResilienceStrategyBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ private ResilienceStrategy CreateResilienceStrategy(Entry entry)
BuilderProperties = Options.Properties,
StrategyName = entry.Properties.StrategyName,
StrategyType = entry.Properties.StrategyType,
Telemetry = Options.TelemetryFactory.Create(telemetryContext)
Telemetry = Options.TelemetryFactory.Create(telemetryContext),
TimeProvider = Options.TimeProvider
};

return entry.Factory(context);
Expand Down
5 changes: 5 additions & 0 deletions src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ public class ResilienceStrategyBuilderContext
/// Gets the resilience telemetry used to report important events.
/// </summary>
public ResilienceTelemetry Telemetry { get; internal set; } = NullResilienceTelemetry.Instance;

/// <summary>
/// Gets or sets the <see cref="TimeProvider"/> used by this strategy.
/// </summary>
internal TimeProvider TimeProvider { get; set; } = TimeProvider.System;
}
9 changes: 9 additions & 0 deletions src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,13 @@ public class ResilienceStrategyBuilderOptions
/// </summary>
[Required]
public ResilienceTelemetryFactory TelemetryFactory { get; set; } = NullResilienceTelemetryFactory.Instance;

/// <summary>
/// Gets or sets a <see cref="TimeProvider"/> that is used by strategies that work with time.
/// </summary>
/// <remarks>
/// This property is internal until we switch to official System.TimeProvider.
/// </remarks>
[Required]
internal TimeProvider TimeProvider { get; set; } = TimeProvider.System;
}
50 changes: 50 additions & 0 deletions src/Polly.Core/Utils/TimeProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Threading;

namespace Polly.Utils;

#pragma warning disable S3872 // Parameter names should not duplicate the names of their methods

/// <summary>
/// TEMPORARY ONLY, to be replaced with System.TimeProvider - https://github.com/dotnet/runtime/issues/36617 later.
/// </summary>
/// <remarks>We trimmed some of the API that's not relevant for us too.</remarks>
internal abstract class TimeProvider
{
private readonly double _tickFrequency;

public static TimeProvider System { get; } = new SystemTimeProvider();

protected TimeProvider(long timestampFrequency)
{
TimestampFrequency = timestampFrequency;
_tickFrequency = (double)TimeSpan.TicksPerSecond / TimestampFrequency;
}

public abstract DateTimeOffset UtcNow { get; }

public long TimestampFrequency { get; }

public abstract long GetTimestamp();

public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => new((long)((endingTimestamp - startingTimestamp) * _tickFrequency));

public abstract Task Delay(TimeSpan delay, CancellationToken cancellationToken = default);

public abstract void CancelAfter(CancellationTokenSource source, TimeSpan delay);

private sealed class SystemTimeProvider : TimeProvider
{
public SystemTimeProvider()
: base(Stopwatch.Frequency)
{
}

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

public override Task Delay(TimeSpan delay, CancellationToken cancellationToken = default) => Task.Delay(delay, cancellationToken);

public override void CancelAfter(CancellationTokenSource source, TimeSpan delay) => source.CancelAfter(delay);

public override DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
}
Loading

0 comments on commit 36ceee0

Please sign in to comment.