From a2559b1ab7bf9c1e12c71183ce2dfa937bc9c7a6 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Tue, 14 Nov 2023 16:29:43 +0000 Subject: [PATCH] Add support for .NET 8 (#1144) - Add `net8.0` targets. - Use .NET `TimeProvider` implementation. - Use `FakeTimeProvider` for tests. - Use new .NET 8 APIs where relevant. Co-Authored-By: martintmk <103487740+martintmk@users.noreply.github.com> --- Directory.Packages.props | 12 +- .../Polly.Benchmarks/Polly.Benchmarks.csproj | 2 +- .../Polly.Core.Benchmarks.csproj | 2 +- eng/Library.targets | 1 + .../Controller/CircuitStateController.cs | 4 + .../Controller/ScheduledTaskExecutor.cs | 4 + .../Controller/HedgingExecutionContext.cs | 8 + src/Polly.Core/Polly.Core.csproj | 3 +- src/Polly.Core/PublicAPI.Unshipped.txt | 3 + .../RegistryPipelineComponentBuilder.cs | 2 +- .../ResiliencePipelineBuilderBase.cs | 16 +- src/Polly.Core/StrategyBuilderContext.cs | 2 +- src/Polly.Core/ToBeRemoved/TimeProvider.cs | 456 ------------------ .../CancellationTokenSourcePool.Disposable.cs | 2 + .../CancellationTokenSourcePool.Pooled.cs | 13 +- .../Utils/CancellationTokenSourcePool.cs | 9 +- .../Pipeline/ExecutionTrackingComponent.cs | 4 + .../Pipeline/PipelineComponentFactory.cs | 13 + .../Utils/TimeProviderExtensions.cs | 11 +- .../PollyServiceCollectionExtensions.cs | 6 + src/Polly.Extensions/Polly.Extensions.csproj | 3 +- .../Polly.RateLimiting.csproj | 2 +- src/Polly.Testing/Polly.Testing.csproj | 2 +- .../GenericResiliencePipelineBuilderTests.cs | 2 +- .../Hedging/HedgingResilienceStrategyTests.cs | 4 + .../Hedging/HedgingTimeProvider.cs | 10 +- .../Hedging/PrimaryStringTasks.cs | 9 + .../Helpers/FakeTimeProvider.cs | 375 -------------- test/Polly.Core.Tests/Polly.Core.Tests.csproj | 4 +- .../ResiliencePipelineBuilderTests.cs | 14 +- .../Timeout/TimeoutResilienceStrategyTests.cs | 27 +- .../Pipeline/PipelineComponentFactoryTests.cs | 53 +- .../PollyServiceCollectionExtensionTests.cs | 37 +- .../Polly.Extensions.Tests.csproj | 2 +- .../Polly.RateLimiting.Tests.csproj | 2 +- test/Polly.Specs/Polly.Specs.csproj | 2 +- test/Polly.TestUtils/Polly.TestUtils.csproj | 2 +- .../Polly.Testing.Tests.csproj | 2 +- 38 files changed, 234 insertions(+), 891 deletions(-) delete mode 100644 src/Polly.Core/ToBeRemoved/TimeProvider.cs delete mode 100644 test/Polly.Core.Tests/Helpers/FakeTimeProvider.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index e8b9af959fb..fb622139459 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,6 @@ - 7.0.0 + 8.0.0 true 8.1.0 @@ -10,6 +10,7 @@ + @@ -20,6 +21,7 @@ + @@ -41,12 +43,4 @@ - - - - - - - - diff --git a/bench/Polly.Benchmarks/Polly.Benchmarks.csproj b/bench/Polly.Benchmarks/Polly.Benchmarks.csproj index 53a3e563d6a..0fc02332e3f 100644 --- a/bench/Polly.Benchmarks/Polly.Benchmarks.csproj +++ b/bench/Polly.Benchmarks/Polly.Benchmarks.csproj @@ -2,7 +2,7 @@ false Exe - net6.0;net7.0 + net6.0;net7.0;net8.0 enable Benchmark $(NoWarn);CA1822;SA1414;IDE0060 diff --git a/bench/Polly.Core.Benchmarks/Polly.Core.Benchmarks.csproj b/bench/Polly.Core.Benchmarks/Polly.Core.Benchmarks.csproj index d7742e9db96..fd0ec851ece 100644 --- a/bench/Polly.Core.Benchmarks/Polly.Core.Benchmarks.csproj +++ b/bench/Polly.Core.Benchmarks/Polly.Core.Benchmarks.csproj @@ -1,6 +1,6 @@  - net7.0 + net8.0;net7.0 Polly true Benchmark diff --git a/eng/Library.targets b/eng/Library.targets index 64004df8f31..b09e5c2a93a 100644 --- a/eng/Library.targets +++ b/eng/Library.targets @@ -14,6 +14,7 @@ true + 8.1.0 diff --git a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs index 8e71fb79c7a..73fc1010629 100644 --- a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs +++ b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs @@ -254,10 +254,14 @@ private static bool IsDateTimeOverflow(DateTimeOffset utcNow, TimeSpan breakDura private void EnsureNotDisposed() { +#if NET8_0_OR_GREATER + ObjectDisposedException.ThrowIf(_disposed, this); +#else if (_disposed) { throw new ObjectDisposedException(nameof(CircuitStateController)); } +#endif } private void CloseCircuit_NeedsLock(Outcome outcome, bool manual, ResilienceContext context, out Task? scheduledTask) diff --git a/src/Polly.Core/CircuitBreaker/Controller/ScheduledTaskExecutor.cs b/src/Polly.Core/CircuitBreaker/Controller/ScheduledTaskExecutor.cs index b3b7cc530ef..7bc3f6f6d5f 100644 --- a/src/Polly.Core/CircuitBreaker/Controller/ScheduledTaskExecutor.cs +++ b/src/Polly.Core/CircuitBreaker/Controller/ScheduledTaskExecutor.cs @@ -17,10 +17,14 @@ internal sealed class ScheduledTaskExecutor : IDisposable public void ScheduleTask(Func taskFactory, ResilienceContext context, out Task task) { +#if NET8_0_OR_GREATER + ObjectDisposedException.ThrowIf(_disposed, this); +#else if (_disposed) { throw new ObjectDisposedException(nameof(ScheduledTaskExecutor)); } +#endif var source = new TaskCompletionSource(); task = source.Task; diff --git a/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs b/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs index 58772f45b95..f1c580fe41d 100644 --- a/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs +++ b/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs @@ -122,7 +122,11 @@ public async ValueTask DisposeAsync() using var delayTaskCancellation = CancellationTokenSource.CreateLinkedTokenSource(PrimaryContext!.CancellationToken); +#if NET8_0_OR_GREATER + var delayTask = Task.Delay(hedgingDelay, _timeProvider, delayTaskCancellation.Token); +#else var delayTask = _timeProvider.Delay(hedgingDelay, delayTaskCancellation.Token); +#endif Task whenAnyHedgedTask = WaitForTaskCompetitionAsync(); var completedTask = await Task.WhenAny(whenAnyHedgedTask, delayTask).ConfigureAwait(ContinueOnCapturedContext); @@ -133,7 +137,11 @@ public async ValueTask DisposeAsync() // cancel the ongoing delay task // Stryker disable once boolean : no means to test this +#if NET8_0_OR_GREATER + await delayTaskCancellation.CancelAsync().ConfigureAwait(ContinueOnCapturedContext); +#else delayTaskCancellation.Cancel(throwOnFirstException: false); +#endif await whenAnyHedgedTask.ConfigureAwait(ContinueOnCapturedContext); diff --git a/src/Polly.Core/Polly.Core.csproj b/src/Polly.Core/Polly.Core.csproj index 6476f89a6eb..bb55baca269 100644 --- a/src/Polly.Core/Polly.Core.csproj +++ b/src/Polly.Core/Polly.Core.csproj @@ -1,7 +1,7 @@  - net6.0;netstandard2.0;net472;net462 + net8.0;net6.0;netstandard2.0;net472;net462 Polly.Core Polly enable @@ -29,6 +29,7 @@ + diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index a84ad565317..4045c01b593 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -7,3 +7,6 @@ Polly.CircuitBreaker.BreakDurationGeneratorArguments.FailureCount.get -> int Polly.CircuitBreaker.BreakDurationGeneratorArguments.FailureRate.get -> double Polly.CircuitBreaker.CircuitBreakerStrategyOptions.BreakDurationGenerator.get -> System.Func>? Polly.CircuitBreaker.CircuitBreakerStrategyOptions.BreakDurationGenerator.set -> void +Polly.ResiliencePipelineBuilderBase.TimeProvider.get -> System.TimeProvider? +Polly.ResiliencePipelineBuilderBase.TimeProvider.set -> void +Polly.StrategyBuilderContext.TimeProvider.get -> System.TimeProvider! diff --git a/src/Polly.Core/Registry/RegistryPipelineComponentBuilder.cs b/src/Polly.Core/Registry/RegistryPipelineComponentBuilder.cs index 6a3c0a00757..10b7f937f8a 100644 --- a/src/Polly.Core/Registry/RegistryPipelineComponentBuilder.cs +++ b/src/Polly.Core/Registry/RegistryPipelineComponentBuilder.cs @@ -59,7 +59,7 @@ private Builder CreateBuilder() builder.InstanceName = _instanceName; _configure(builder, context); - var timeProvider = builder.TimeProvider; + var timeProvider = builder.TimeProviderInternal; var telemetry = new ResilienceStrategyTelemetry( new ResilienceTelemetrySource(builder.Name, builder.InstanceName, null), builder.TelemetryListener); diff --git a/src/Polly.Core/ResiliencePipelineBuilderBase.cs b/src/Polly.Core/ResiliencePipelineBuilderBase.cs index d7b49610d24..5317d7c735c 100644 --- a/src/Polly.Core/ResiliencePipelineBuilderBase.cs +++ b/src/Polly.Core/ResiliencePipelineBuilderBase.cs @@ -68,16 +68,12 @@ private protected ResiliencePipelineBuilderBase(ResiliencePipelineBuilderBase ot public ResilienceContextPool? ContextPool { get; set; } /// - /// Gets or sets a that is used by strategies that work with time. + /// Gets or sets a that is used by strategies that work with time. /// - /// - /// This property is internal until we switch to official System.TimeProvider. - /// /// - /// The default value is . + /// The default value is and unless set, will be used. /// - [Required] - internal TimeProvider TimeProvider { get; set; } = TimeProvider.System; + public TimeProvider? TimeProvider { get; set; } /// /// Gets or sets the that is used by Polly to report resilience events. @@ -91,6 +87,8 @@ private protected ResiliencePipelineBuilderBase(ResiliencePipelineBuilderBase ot [EditorBrowsable(EditorBrowsableState.Never)] public TelemetryListener? TelemetryListener { get; set; } + internal TimeProvider TimeProviderInternal => TimeProvider ?? TimeProvider.System; + /// /// Gets the validator that is used for the validation. /// @@ -132,13 +130,13 @@ internal PipelineComponent BuildPipelineComponent() var source = new ResilienceTelemetrySource(Name, InstanceName, null); - return PipelineComponentFactory.CreateComposite(components, new ResilienceStrategyTelemetry(source, TelemetryListener), TimeProvider); + return PipelineComponentFactory.CreateComposite(components, new ResilienceStrategyTelemetry(source, TelemetryListener), TimeProviderInternal); } private PipelineComponent CreateComponent(Entry entry) { var source = new ResilienceTelemetrySource(Name, InstanceName, entry.Options.Name); - var context = new StrategyBuilderContext(new ResilienceStrategyTelemetry(source, TelemetryListener), TimeProvider); + var context = new StrategyBuilderContext(new ResilienceStrategyTelemetry(source, TelemetryListener), TimeProviderInternal); var strategy = entry.Factory(context); strategy.Options = entry.Options; diff --git a/src/Polly.Core/StrategyBuilderContext.cs b/src/Polly.Core/StrategyBuilderContext.cs index db1b3686088..5da3b183240 100644 --- a/src/Polly.Core/StrategyBuilderContext.cs +++ b/src/Polly.Core/StrategyBuilderContext.cs @@ -21,5 +21,5 @@ internal StrategyBuilderContext(ResilienceStrategyTelemetry telemetry, TimeProvi /// /// Gets the used by this strategy. /// - internal TimeProvider TimeProvider { get; } + public TimeProvider TimeProvider { get; } } diff --git a/src/Polly.Core/ToBeRemoved/TimeProvider.cs b/src/Polly.Core/ToBeRemoved/TimeProvider.cs deleted file mode 100644 index b6e644df1dc..00000000000 --- a/src/Polly.Core/ToBeRemoved/TimeProvider.cs +++ /dev/null @@ -1,456 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; - -#pragma warning disable - -namespace System.Threading -{ - internal interface ITimer : IDisposable, IAsyncDisposable - { - bool Change(TimeSpan dueTime, TimeSpan period); - } -} - -namespace System -{ - // Temporary, will be removed - // Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/TimeProvider.cs and trimmed some fat which is not relevant for internal stuff - - [ExcludeFromCodeCoverage] - internal abstract class TimeProvider - { - public static TimeProvider System { get; } = new SystemTimeProvider(); - - protected TimeProvider() - { - } - - public virtual DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow; - - private static readonly long MinDateTicks = DateTime.MinValue.Ticks; - private static readonly long MaxDateTicks = DateTime.MaxValue.Ticks; - - public DateTimeOffset GetLocalNow() - { - DateTimeOffset utcDateTime = GetUtcNow(); - TimeZoneInfo zoneInfo = LocalTimeZone; - if (zoneInfo is null) - { - throw new InvalidOperationException(); - } - - TimeSpan offset = zoneInfo.GetUtcOffset(utcDateTime); - - long localTicks = utcDateTime.Ticks + offset.Ticks; - if ((ulong)localTicks > (ulong)MaxDateTicks) - { - localTicks = localTicks < MinDateTicks ? MinDateTicks : MaxDateTicks; - } - - return new DateTimeOffset(localTicks, offset); - } - - public virtual TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local; - - public virtual long TimestampFrequency => Stopwatch.Frequency; - - public virtual long GetTimestamp() => Stopwatch.GetTimestamp(); - - public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) - { - long timestampFrequency = TimestampFrequency; - if (timestampFrequency <= 0) - { - throw new InvalidOperationException(); - } - - return new TimeSpan((long)((endingTimestamp - startingTimestamp) * ((double)TimeSpan.TicksPerSecond / timestampFrequency))); - } - - public TimeSpan GetElapsedTime(long startingTimestamp) => GetElapsedTime(startingTimestamp, GetTimestamp()); - - public virtual ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) - { - if (callback is null) - { - throw new ArgumentNullException(nameof(callback)); - } - - return new SystemTimeProviderTimer(dueTime, period, callback, state); - } - - [ExcludeFromCodeCoverage] - private sealed class SystemTimeProviderTimer : ITimer - { - private readonly Timer _timer; - - public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state) - { - (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period); - - // We need to ensure the timer roots itself. Timer created with a duration and period argument - // only roots the state object, so to root the timer we need the state object to reference the - // timer recursively. - var timerState = new TimerState(callback, state); - timerState.Timer = _timer = new Timer(static s => - { - TimerState ts = (TimerState)s!; - ts.Callback(ts.State); - }, timerState, duration, periodTime); - } - - private sealed class TimerState - { - public TimerState(TimerCallback callback, object? state) - { - Callback = callback; - State = state; - } - - public TimerCallback Callback { get; } - - public object? State { get; } - - public Timer? Timer { get; set; } - } - - public bool Change(TimeSpan dueTime, TimeSpan period) - { - (uint duration, uint periodTime) = CheckAndGetValues(dueTime, period); - try - { - return _timer.Change(duration, periodTime); - } - catch (ObjectDisposedException) - { - return false; - } - } - - public void Dispose() => _timer.Dispose(); - - public ValueTask DisposeAsync() - { - _timer.Dispose(); - return default; - } - - private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime) - { - long dueTm = (long)dueTime.TotalMilliseconds; - long periodTm = (long)periodTime.TotalMilliseconds; - - const uint MaxSupportedTimeout = 0xfffffffe; - - if (dueTm < -1) - { - throw new ArgumentOutOfRangeException(nameof(dueTime)); - } - - if (dueTm > MaxSupportedTimeout) - { - throw new ArgumentOutOfRangeException(nameof(dueTime)); - } - - if (periodTm < -1) - { - throw new ArgumentOutOfRangeException(nameof(periodTm)); - } - - if (periodTm > MaxSupportedTimeout) - { - throw new ArgumentOutOfRangeException(nameof(periodTm)); - } - - return ((uint)dueTm, (uint)periodTm); - } - } - - [ExcludeFromCodeCoverage] - private sealed class SystemTimeProvider : TimeProvider - { - internal SystemTimeProvider() - { - } - } - } -} - -namespace System.Threading.Tasks -{ - /// - /// Provide extensions methods for operations with . - /// - /// - /// The Microsoft.Bcl.TimeProvider library interfaces are intended solely for use in building against pre-.NET 8 surface area. - /// If your code is being built against .NET 8 or higher, then this library should not be utilized. - /// - [ExcludeFromCodeCoverage] - internal static class TimeProviderTaskExtensions - { - private sealed class DelayState : TaskCompletionSource - { - public DelayState(CancellationToken cancellationToken) - : base(TaskCreationOptions.RunContinuationsAsynchronously) => CancellationToken = cancellationToken; - - public ITimer? Timer { get; set; } - public CancellationToken CancellationToken { get; } - public CancellationTokenRegistration Registration { get; set; } - } - - private sealed class WaitAsyncState : TaskCompletionSource - { - public WaitAsyncState(CancellationToken cancellationToken) - : base(TaskCreationOptions.RunContinuationsAsynchronously) => CancellationToken = cancellationToken; - - public readonly CancellationTokenSource ContinuationCancellation = new(); - public CancellationToken CancellationToken { get; } - public CancellationTokenRegistration Registration; - public ITimer? Timer; - } - - /// Creates a task that completes after a specified time interval. - /// The with which to interpret . - /// The to wait before completing the returned task, or to wait indefinitely. - /// A cancellation token to observe while waiting for the task to complete. - /// A task that represents the time delay. - /// The argument is null. - /// represents a negative time interval other than . - public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default) - { - if (timeProvider == TimeProvider.System) - { - return Task.Delay(delay, cancellationToken); - } - - if (timeProvider is null) - { - throw new ArgumentNullException(nameof(timeProvider)); - } - - if (delay != Timeout.InfiniteTimeSpan && delay < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(delay)); - } - - if (delay == TimeSpan.Zero) - { - return Task.CompletedTask; - } - - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - - DelayState state = new(cancellationToken); - - state.Timer = timeProvider.CreateTimer(static delayState => - { - DelayState s = (DelayState)delayState!; - s.TrySetResult(true); - s.Registration.Dispose(); - s.Timer?.Dispose(); - }, state, delay, Timeout.InfiniteTimeSpan); - - state.Registration = cancellationToken.Register(static delayState => - { - DelayState s = (DelayState)delayState!; - s.TrySetCanceled(s.CancellationToken); - s.Registration.Dispose(); - s.Timer?.Dispose(); - }, state); - - // There are race conditions where the timer fires after we have attached the cancellation callback but before the - // registration is stored in state.Registration, or where cancellation is requested prior to the registration being - // stored into state.Registration, or where the timer could fire after it's been created but before it's been stored - // in state.Timer. In such cases, the cancellation registration and/or the Timer might be stored into state after the - // callbacks and thus left undisposed. So, we do a subsequent check here. If the task isn't completed by this point, - // then the callbacks won't have called TrySetResult (the callbacks invoke TrySetResult before disposing of the fields), - // in which case it will see both the timer and registration set and be able to Dispose them. If the task is completed - // by this point, then this is guaranteed to see s.Timer as non-null because it was deterministically set above. - if (state.Task.IsCompleted) - { - state.Registration.Dispose(); - state.Timer.Dispose(); - } - - return state.Task; - } - - /// - /// Gets a that will complete when this completes, - /// when the specified timeout expires, or when the specified has cancellation requested. - /// - /// The task for which to wait on until completion. - /// The timeout after which the should be faulted with a if it hasn't otherwise completed. - /// The with which to interpret . - /// The to monitor for a cancellation request. - /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. - /// The argument is null. - /// The argument is null. - /// represents a negative time interval other than . - public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) - { - if (task is null) - { - throw new ArgumentNullException(nameof(task)); - } - - if (timeout != Timeout.InfiniteTimeSpan && timeout < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(timeout)); - } - - if (timeProvider is null) - { - throw new ArgumentNullException(nameof(timeProvider)); - } - - if (task.IsCompleted) - { - return task; - } - - if (timeout == Timeout.InfiniteTimeSpan && !cancellationToken.CanBeCanceled) - { - return task; - } - - if (timeout == TimeSpan.Zero) - { - Task.FromException(new TimeoutException()); - } - - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - - WaitAsyncState state = new(cancellationToken); - - state.Timer = timeProvider.CreateTimer(static s => - { - var state = (WaitAsyncState)s!; - - state.TrySetException(new TimeoutException()); - - state.Registration.Dispose(); - state.Timer?.Dispose(); - state.ContinuationCancellation.Cancel(); - }, state, timeout, Timeout.InfiniteTimeSpan); - - _ = task.ContinueWith(static (t, s) => - { - var state = (WaitAsyncState)s!; - - if (t.IsFaulted) - { - state.TrySetException(t.Exception!.InnerExceptions); - } - else if (t.IsCanceled) - { - state.TrySetCanceled(); - } - else - { - state.TrySetResult(true); - } - - state.Registration.Dispose(); - state.Timer?.Dispose(); - }, state, state.ContinuationCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - - state.Registration = cancellationToken.Register(static s => - { - var state = (WaitAsyncState)s!; - - state.TrySetCanceled(state.CancellationToken); - - state.Timer?.Dispose(); - state.ContinuationCancellation.Cancel(); - }, state); - - // See explanation in Delay for this final check - if (state.Task.IsCompleted) - { - state.Registration.Dispose(); - state.Timer.Dispose(); - } - - return state.Task; - } - - /// - /// Gets a that will complete when this completes, - /// when the specified timeout expires, or when the specified has cancellation requested. - /// - /// The task for which to wait on until completion. - /// The timeout after which the should be faulted with a if it hasn't otherwise completed. - /// The with which to interpret . - /// The to monitor for a cancellation request. - /// The representing the asynchronous wait. It may or may not be the same instance as the current instance. - /// The argument is null. - /// The argument is null. - /// represents a negative time interval other than . - public static async Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default) - { - await ((Task)task).WaitAsync(timeout, timeProvider, cancellationToken).ConfigureAwait(false); - return task.Result; - } - - /// Initializes a new instance of the class that will be canceled after the specified . - /// The with which to interpret the . - /// The time interval to wait before canceling this . - /// The is negative and not equal to - /// or greater than maximum allowed timer duration. - /// that will be canceled after the specified . - /// - /// - /// The countdown for the delay starts during the call to the constructor. When the delay expires, - /// the constructed is canceled if it has - /// not been canceled already. - /// - /// - /// If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking on the resultant object. - /// This action will not terminate the initial timer indicated by . However, this restriction does not apply on .NET 8.0 and later versions. - /// - /// - public static CancellationTokenSource CreateCancellationTokenSource(this TimeProvider timeProvider, TimeSpan delay) - { - if (timeProvider is null) - { - throw new ArgumentNullException(nameof(timeProvider)); - } - - if (delay != Timeout.InfiniteTimeSpan && delay < TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(delay)); - } - - if (timeProvider == TimeProvider.System) - { - return new CancellationTokenSource(delay); - } - - var cts = new CancellationTokenSource(); - - ITimer timer = timeProvider.CreateTimer(static s => - { - try - { - ((CancellationTokenSource)s!).Cancel(); - } - catch (ObjectDisposedException) - { - // ok - } - }, cts, delay, Timeout.InfiniteTimeSpan); - - cts.Token.Register(static t => ((ITimer)t!).Dispose(), timer); - return cts; - } - } -} diff --git a/src/Polly.Core/Utils/CancellationTokenSourcePool.Disposable.cs b/src/Polly.Core/Utils/CancellationTokenSourcePool.Disposable.cs index 0510fa5cb5b..e9754965f62 100644 --- a/src/Polly.Core/Utils/CancellationTokenSourcePool.Disposable.cs +++ b/src/Polly.Core/Utils/CancellationTokenSourcePool.Disposable.cs @@ -2,6 +2,7 @@ namespace Polly.Utils; internal abstract partial class CancellationTokenSourcePool { +#if !NET8_0_OR_GREATER private sealed class DisposableCancellationTokenSourcePool : CancellationTokenSourcePool { private readonly TimeProvider _timeProvider; @@ -20,4 +21,5 @@ protected override CancellationTokenSource GetCore(TimeSpan delay) public override void Return(CancellationTokenSource source) => source.Dispose(); } +#endif } diff --git a/src/Polly.Core/Utils/CancellationTokenSourcePool.Pooled.cs b/src/Polly.Core/Utils/CancellationTokenSourcePool.Pooled.cs index 93b7acdfc11..4791a8fcc9f 100644 --- a/src/Polly.Core/Utils/CancellationTokenSourcePool.Pooled.cs +++ b/src/Polly.Core/Utils/CancellationTokenSourcePool.Pooled.cs @@ -5,11 +5,20 @@ internal abstract partial class CancellationTokenSourcePool #if NET6_0_OR_GREATER private sealed class PooledCancellationTokenSourcePool : CancellationTokenSourcePool { - public static readonly PooledCancellationTokenSourcePool SystemInstance = new(); + public static readonly PooledCancellationTokenSourcePool SystemInstance = new(TimeProvider.System); private readonly ObjectPool _pool; - public PooledCancellationTokenSourcePool() => _pool = new(static () => new CancellationTokenSource(), static cts => true); + public PooledCancellationTokenSourcePool(TimeProvider timeProvider) => _pool = new( + () => + { +#if NET8_0_OR_GREATER + return new CancellationTokenSource(System.Threading.Timeout.InfiniteTimeSpan, timeProvider); +#else + return new CancellationTokenSource(); +#endif + }, + static cts => true); protected override CancellationTokenSource GetCore(TimeSpan delay) { diff --git a/src/Polly.Core/Utils/CancellationTokenSourcePool.cs b/src/Polly.Core/Utils/CancellationTokenSourcePool.cs index cabb1f291d7..b11767f0b07 100644 --- a/src/Polly.Core/Utils/CancellationTokenSourcePool.cs +++ b/src/Polly.Core/Utils/CancellationTokenSourcePool.cs @@ -6,7 +6,14 @@ internal abstract partial class CancellationTokenSourcePool { public static CancellationTokenSourcePool Create(TimeProvider timeProvider) { -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER + if (timeProvider == TimeProvider.System) + { + return PooledCancellationTokenSourcePool.SystemInstance; + } + + return new PooledCancellationTokenSourcePool(timeProvider); +#elif NET6_0_OR_GREATER if (timeProvider == TimeProvider.System) { return PooledCancellationTokenSourcePool.SystemInstance; diff --git a/src/Polly.Core/Utils/Pipeline/ExecutionTrackingComponent.cs b/src/Polly.Core/Utils/Pipeline/ExecutionTrackingComponent.cs index 5a03e7f94fc..0aee198ec21 100644 --- a/src/Polly.Core/Utils/Pipeline/ExecutionTrackingComponent.cs +++ b/src/Polly.Core/Utils/Pipeline/ExecutionTrackingComponent.cs @@ -45,7 +45,11 @@ public override async ValueTask DisposeAsync() // so we will do "dummy" retries until there are no more executions. while (HasPendingExecutions) { +#if NET8_0_OR_GREATER + await Task.Delay(SleepDelay, _timeProvider).ConfigureAwait(false); +#else await _timeProvider.Delay(SleepDelay).ConfigureAwait(false); +#endif // stryker disable once equality : no means to test this if (_timeProvider.GetElapsedTime(start) > Timeout) diff --git a/src/Polly.Core/Utils/Pipeline/PipelineComponentFactory.cs b/src/Polly.Core/Utils/Pipeline/PipelineComponentFactory.cs index a671dc7e0f1..093858ae7b2 100644 --- a/src/Polly.Core/Utils/Pipeline/PipelineComponentFactory.cs +++ b/src/Polly.Core/Utils/Pipeline/PipelineComponentFactory.cs @@ -14,12 +14,25 @@ internal static class PipelineComponentFactory public static PipelineComponent WithDisposableCallbacks(PipelineComponent component, IEnumerable callbacks) { +#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection +#if NET6_0_OR_GREATER + if (callbacks.TryGetNonEnumeratedCount(out var count)) + { + if (count == 0) + { + return component; + } + } + else if (!callbacks.Any()) +#else if (!callbacks.Any()) +#endif { return component; } return new ComponentWithDisposeCallbacks(component, callbacks.ToList()); +#pragma warning restore CA1851 // Possible multiple enumerations of 'IEnumerable' collection } public static PipelineComponent WithExecutionTracking(PipelineComponent component, TimeProvider timeProvider) => new ExecutionTrackingComponent(component, timeProvider); diff --git a/src/Polly.Core/Utils/TimeProviderExtensions.cs b/src/Polly.Core/Utils/TimeProviderExtensions.cs index 7e54f24583b..8c7ecf66314 100644 --- a/src/Polly.Core/Utils/TimeProviderExtensions.cs +++ b/src/Polly.Core/Utils/TimeProviderExtensions.cs @@ -35,15 +35,20 @@ public static Task DelayAsync(this TimeProvider timeProvider, TimeSpan delay, Re // the use of Thread.Sleep() here because it is not cancellable and to // simplify the code. Sync-over-async is not a concern here because it // only applies in the case of a resilience event and not on the hot path. - - // re the Sync-over-async I guess that would be a concern when using the LatencyChaosStrategy - // since that's running on the hot path, thoughts? +#if NET8_0_OR_GREATER + Task.Delay(delay, timeProvider, context.CancellationToken).GetAwaiter().GetResult(); +#else timeProvider.Delay(delay, context.CancellationToken).GetAwaiter().GetResult(); +#endif #pragma warning restore CA1849 return Task.CompletedTask; } +#if NET8_0_OR_GREATER + return Task.Delay(delay, timeProvider, context.CancellationToken); +#else return timeProvider.Delay(delay, context.CancellationToken); +#endif } } diff --git a/src/Polly.Extensions/DependencyInjection/PollyServiceCollectionExtensions.cs b/src/Polly.Extensions/DependencyInjection/PollyServiceCollectionExtensions.cs index b6fdf2aa2ef..d345ec7ce41 100644 --- a/src/Polly.Extensions/DependencyInjection/PollyServiceCollectionExtensions.cs +++ b/src/Polly.Extensions/DependencyInjection/PollyServiceCollectionExtensions.cs @@ -245,6 +245,12 @@ private static void AddResiliencePipelineBuilder(this IServiceCollection service { var builder = new ResiliencePipelineBuilder(); builder.ConfigureTelemetry(serviceProvider.GetRequiredService>().Value); + + if (serviceProvider.GetService() is { } timeProvider) + { + builder.TimeProvider = timeProvider; + } + return builder; }); } diff --git a/src/Polly.Extensions/Polly.Extensions.csproj b/src/Polly.Extensions/Polly.Extensions.csproj index 429353154a5..172e3333756 100644 --- a/src/Polly.Extensions/Polly.Extensions.csproj +++ b/src/Polly.Extensions/Polly.Extensions.csproj @@ -1,6 +1,6 @@  - net6.0;netstandard2.0;net472;net462 + net8.0;net6.0;netstandard2.0;net472;net462 Polly.Extensions Polly enable @@ -20,7 +20,6 @@ - diff --git a/src/Polly.RateLimiting/Polly.RateLimiting.csproj b/src/Polly.RateLimiting/Polly.RateLimiting.csproj index 97be9f209d4..46e8f574290 100644 --- a/src/Polly.RateLimiting/Polly.RateLimiting.csproj +++ b/src/Polly.RateLimiting/Polly.RateLimiting.csproj @@ -1,6 +1,6 @@  - net6.0;netstandard2.0;net472;net462 + net8.0;net6.0;netstandard2.0;net472;net462 Polly.RateLimiting Polly.RateLimiting enable diff --git a/src/Polly.Testing/Polly.Testing.csproj b/src/Polly.Testing/Polly.Testing.csproj index d85ec8fba23..481df5fce1c 100644 --- a/src/Polly.Testing/Polly.Testing.csproj +++ b/src/Polly.Testing/Polly.Testing.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0;netstandard2.0 Polly.Testing Polly.Testing enable diff --git a/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs b/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs index 64d19efdff4..bfe69b5f8cc 100644 --- a/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs +++ b/test/Polly.Core.Tests/GenericResiliencePipelineBuilderTests.cs @@ -12,7 +12,7 @@ public class GenericResiliencePipelineBuilderTests public void Ctor_EnsureDefaults() { _builder.Name.Should().BeNull(); - _builder.TimeProvider.Should().Be(TimeProvider.System); + _builder.TimeProvider.Should().BeNull(); } [Fact] diff --git a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index e053cff5b79..7e6365178eb 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -155,7 +155,11 @@ public async Task ExecuteAsync_ShouldReturnAnyPossibleResult() var result = await strategy.ExecuteAsync(_primaryTasks.SlowTask); result.Should().NotBeNull(); +#if NET8_0_OR_GREATER + _timeProvider.TimerEntries.Should().HaveCount(8); +#else _timeProvider.TimerEntries.Should().HaveCount(5); +#endif result.Should().Be("Oranges"); } diff --git a/test/Polly.Core.Tests/Hedging/HedgingTimeProvider.cs b/test/Polly.Core.Tests/Hedging/HedgingTimeProvider.cs index babb0998755..2b8ee973cb4 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingTimeProvider.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingTimeProvider.cs @@ -1,3 +1,5 @@ +using System; + namespace Polly.Core.Tests.Hedging; internal class HedgingTimeProvider : TimeProvider @@ -26,7 +28,13 @@ public void Advance(TimeSpan diff) public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) { - var entry = new TimerEntry(dueTime, new TaskCompletionSource(), _utcNow.Add(dueTime), () => callback(state)); + var timeStamp = dueTime switch + { + _ when dueTime == global::System.Threading.Timeout.InfiniteTimeSpan => DateTimeOffset.MaxValue, + _ => _utcNow.Add(dueTime) + }; + + var entry = new TimerEntry(dueTime, new TaskCompletionSource(), timeStamp, () => callback(state)); TimerEntries.Enqueue(entry); Advance(AutoAdvance); diff --git a/test/Polly.Core.Tests/Hedging/PrimaryStringTasks.cs b/test/Polly.Core.Tests/Hedging/PrimaryStringTasks.cs index 7e3f095f734..926968b2a65 100644 --- a/test/Polly.Core.Tests/Hedging/PrimaryStringTasks.cs +++ b/test/Polly.Core.Tests/Hedging/PrimaryStringTasks.cs @@ -19,13 +19,22 @@ public static ValueTask InstantTask() public async ValueTask FastTask(CancellationToken token) { +#if NET8_0_OR_GREATER + await Task.Delay(TimeSpan.FromMilliseconds(10), _timeProvider, token); +#else await _timeProvider.Delay(TimeSpan.FromMilliseconds(10), token); +#endif return FastTaskResult; } public async ValueTask SlowTask(CancellationToken token) { +#if NET8_0_OR_GREATER + await Task.Delay(TimeSpan.FromDays(1), _timeProvider, token); +#else await _timeProvider.Delay(TimeSpan.FromDays(1), token); +#endif + return SlowTaskResult; } } diff --git a/test/Polly.Core.Tests/Helpers/FakeTimeProvider.cs b/test/Polly.Core.Tests/Helpers/FakeTimeProvider.cs deleted file mode 100644 index 6946ac944a6..00000000000 --- a/test/Polly.Core.Tests/Helpers/FakeTimeProvider.cs +++ /dev/null @@ -1,375 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#pragma warning disable - -// Replace with Microsoft.Extensions.TimeProvider.Testing when TimeProvider is used (see https://github.com/App-vNext/Polly/pull/1144) -// Based on https://github.com/dotnet/extensions/blob/14917b87e8fc81f10d44ceea52d9b24e50e26550/src/Libraries/Microsoft.Extensions.TimeProvider.Testing/FakeTimeProvider.cs - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Threading; -using Microsoft.Extensions.Time.Testing; - -namespace Microsoft.Extensions.Time.Testing; - -/// -/// A synthetic time provider used to enable deterministic behavior in tests. -/// -internal class FakeTimeProvider : TimeProvider -{ - internal readonly HashSet Waiters = new(); - private DateTimeOffset _now = new(2000, 1, 1, 0, 0, 0, 0, TimeSpan.Zero); - private TimeZoneInfo _localTimeZone = TimeZoneInfo.Utc; - private int _wakeWaitersGate; - private TimeSpan _autoAdvanceAmount; - - /// - /// Initializes a new instance of the class. - /// - /// - /// This creates a provider whose time is initially set to midnight January 1st 2000. - /// The provider is set to not automatically advance time each time it is read. - /// - public FakeTimeProvider() - { - Start = _now; - } - - /// - /// Initializes a new instance of the class. - /// - /// The initial time and date reported by the provider. - /// - /// The provider is set to not automatically advance time each time it is read. - /// - public FakeTimeProvider(DateTimeOffset startDateTime) - { - _now = startDateTime; - Start = _now; - } - - /// - /// Gets the starting date and time for this provider. - /// - public DateTimeOffset Start { get; } - - /// - /// Gets or sets the amount of time by which time advances whenever the clock is read. - /// - /// - /// This defaults to . - /// - public TimeSpan AutoAdvanceAmount - { - get => _autoAdvanceAmount; - set - { - _autoAdvanceAmount = value; - } - } - - /// - public override DateTimeOffset GetUtcNow() - { - DateTimeOffset result; - - lock (Waiters) - { - result = _now; - _now += _autoAdvanceAmount; - } - - WakeWaiters(); - return result; - } - - /// - /// Sets the date and time in the UTC time zone. - /// - /// The date and time in the UTC time zone. - public void SetUtcNow(DateTimeOffset value) - { - lock (Waiters) - { - if (value < _now) - { - throw new ArgumentOutOfRangeException(nameof(value), $"Cannot go back in time. Current time is {_now}."); - } - - _now = value; - } - - WakeWaiters(); - } - - /// - /// Advances time by a specific amount. - /// - /// The amount of time to advance the clock by. - /// - /// Advancing time affects the timers created from this provider, and all other operations that are directly or - /// indirectly using this provider as a time source. Whereas when using , time - /// marches forward automatically in hardware, for the fake time provider the application is responsible for - /// doing this explicitly by calling this method. - /// - public void Advance(TimeSpan delta) - { - lock (Waiters) - { - _now += delta; - } - - WakeWaiters(); - } - - /// - public override long GetTimestamp() - { - // Notionally we're multiplying by frequency and dividing by ticks per second, - // which are the same value for us. Don't actually do the math as the full - // precision of ticks (a long) cannot be represented in a double during division. - // For test stability we want a reproducible result. - // - // The same issue could occur converting back, in GetElapsedTime(). Unfortunately - // that isn't virtual so we can't do the same trick. However, if tests advance - // the clock in multiples of 1ms or so this loss of precision will not be visible. - Debug.Assert(TimestampFrequency == TimeSpan.TicksPerSecond, "Assuming frequency equals ticks per second"); - return _now.Ticks; - } - - /// - public override TimeZoneInfo LocalTimeZone => _localTimeZone; - - /// - /// Sets the local time zone. - /// - /// The local time zone. - public void SetLocalTimeZone(TimeZoneInfo localTimeZone) => _localTimeZone = localTimeZone; - - /// - /// Gets the amount by which the value from increments per second. - /// - /// - /// This is fixed to the value of . - /// - public override long TimestampFrequency => TimeSpan.TicksPerSecond; - - /// - /// Returns a string representation this provider's idea of current time. - /// - /// A string representing the provider's current time. - public override string ToString() => GetUtcNow().ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture); - - /// - public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) - { - var timer = new Timer(this, callback, state); - _ = timer.Change(dueTime, period); - return timer; - } - - internal void RemoveWaiter(Waiter waiter) - { - lock (Waiters) - { - _ = Waiters.Remove(waiter); - } - } - - internal void AddWaiter(Waiter waiter, long dueTime) - { - lock (Waiters) - { - waiter.ScheduledOn = _now.Ticks; - waiter.WakeupTime = _now.Ticks + dueTime; - _ = Waiters.Add(waiter); - } - - WakeWaiters(); - } - - private void WakeWaiters() - { - if (Interlocked.CompareExchange(ref _wakeWaitersGate, 1, 0) == 1) - { - // some other thread is already in here, so let it take care of things - return; - } - - while (true) - { - Waiter? candidate = null; - lock (Waiters) - { - // find an expired waiter - foreach (var waiter in Waiters) - { - if (waiter.WakeupTime > _now.Ticks) - { - // not expired yet - } - else if (candidate is null) - { - // our first candidate - candidate = waiter; - } - else if (waiter.WakeupTime < candidate.WakeupTime) - { - // found a waiter with an earlier wake time, it's our new candidate - candidate = waiter; - } - else if (waiter.WakeupTime > candidate.WakeupTime) - { - // the waiter has a later wake time, so keep the current candidate - } - else if (waiter.ScheduledOn < candidate.ScheduledOn) - { - // the new waiter has the same wake time aa the candidate, pick whichever was scheduled earliest to maintain order - candidate = waiter; - } - } - } - - if (candidate == null) - { - // didn't find a candidate to wake, we're done - _wakeWaitersGate = 0; - return; - } - - // invoke the callback - candidate.InvokeCallback(); - - // see if we need to reschedule the waiter - if (candidate.Period > 0) - { - // update the waiter's state - candidate.ScheduledOn = _now.Ticks; - candidate.WakeupTime += candidate.Period; - } - else - { - // this waiter is never running again, so remove from the set. - RemoveWaiter(candidate); - } - } - } -} - -// We keep all timer state here in order to prevent Timer instances from being self-referential, -// which would block them being collected when someone forgets to call Dispose on the timer. With -// this arrangement, the Timer object will always be collectible, which will end up calling Dispose -// on this object due to the timer's finalizer. -internal sealed class Waiter -{ - private readonly TimerCallback _callback; - private readonly object? _state; - - public long ScheduledOn { get; set; } = -1; - public long WakeupTime { get; set; } = -1; - public long Period { get; } - - public Waiter(TimerCallback callback, object? state, long period) - { - _callback = callback; - _state = state; - Period = period; - } - - public void InvokeCallback() - { - _callback(_state); - } -} - -// This implements the timer abstractions and is a thin wrapper around a waiter object. -// The main role of this type is to create the waiter, add it to the waiter list, and ensure it gets -// removed from the waiter list when the dispose is disposed or collected. -internal sealed class Timer : ITimer -{ - private const uint MaxSupportedTimeout = 0xfffffffe; - - private Waiter? _waiter; - private FakeTimeProvider? _timeProvider; - private TimerCallback? _callback; - private object? _state; - - public Timer(FakeTimeProvider timeProvider, TimerCallback callback, object? state) - { - _timeProvider = timeProvider; - _callback = callback; - _state = state; - } - - public bool Change(TimeSpan dueTime, TimeSpan period) - { - var dueTimeMs = (long)dueTime.TotalMilliseconds; - var periodMs = (long)period.TotalMilliseconds; - - if (_timeProvider == null) - { - // timer has been disposed - return false; - } - - if (_waiter != null) - { - // remove any previous waiter - _timeProvider.RemoveWaiter(_waiter); - _waiter = null; - } - - if (dueTimeMs < 0) - { - // this waiter will never wake up, so just bail - return true; - } - - if (periodMs < 0 || periodMs == Timeout.Infinite) - { - // normalize - period = TimeSpan.Zero; - } - - _waiter = new Waiter(_callback!, _state, period.Ticks); - _timeProvider.AddWaiter(_waiter, dueTime.Ticks); - return true; - } - - // In case the timer is not disposed, this will remove the Waiter instance from the provider. - ~Timer() => Dispose(false); - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public ValueTask DisposeAsync() - { - Dispose(true); - GC.SuppressFinalize(this); -#if NET5_0_OR_GREATER - return ValueTask.CompletedTask; -#else - return default; -#endif - } - - private void Dispose(bool _) - { - if (_waiter != null) - { - _timeProvider!.RemoveWaiter(_waiter); - _waiter = null; - } - - _timeProvider = null; - _callback = null; - _state = null; - } -} diff --git a/test/Polly.Core.Tests/Polly.Core.Tests.csproj b/test/Polly.Core.Tests/Polly.Core.Tests.csproj index 51e78af5d4b..3a56ef265ad 100644 --- a/test/Polly.Core.Tests/Polly.Core.Tests.csproj +++ b/test/Polly.Core.Tests/Polly.Core.Tests.csproj @@ -1,15 +1,17 @@  - net7.0;net6.0 + net8.0;net7.0;net6.0 $(TargetFrameworks);net481 Test enable 100 $(NoWarn);SA1600;SA1204;SA1602;S6608 [Polly.Core]* + true + diff --git a/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs b/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs index db3ed78c984..43e1b48acd5 100644 --- a/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs +++ b/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs @@ -16,7 +16,19 @@ public void Ctor_EnsureDefaults() var builder = new ResiliencePipelineBuilder(); builder.Name.Should().BeNull(); - builder.TimeProvider.Should().Be(TimeProvider.System); + builder.TimeProvider.Should().BeNull(); + } + + [Fact] + public void TimeProviderInternal_Ok() + { + var builder = new ResiliencePipelineBuilder(); + builder.TimeProviderInternal.Should().Be(TimeProvider.System); + + var timeProvider = Substitute.For(); + builder.TimeProvider = timeProvider; + + builder.TimeProvider.Should().Be(timeProvider); } [Fact] diff --git a/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs b/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs index c625d0958dc..089d1b09ff8 100644 --- a/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using Microsoft.Extensions.Time.Testing; using NSubstitute; using Polly.Telemetry; @@ -120,18 +121,24 @@ public async Task Execute_Timeout_EnsureStackTrace() SetTimeout(TimeSpan.FromSeconds(2)); var sut = CreateSut(); - var outcome = await sut.ExecuteOutcomeAsync(async (c, _) => - { - var delay = _timeProvider.Delay(TimeSpan.FromSeconds(4), c.CancellationToken); - _timeProvider.Advance(TimeSpan.FromSeconds(2)); - await delay; + var outcome = await sut.ExecuteOutcomeAsync( + async (c, _) => + { + var delay = _timeProvider.Delay(TimeSpan.FromSeconds(4), c.CancellationToken); + _timeProvider.Advance(TimeSpan.FromSeconds(2)); + await delay; + + return Outcome.FromResult("dummy"); + }, + ResilienceContextPool.Shared.Get(), + "state"); - return Outcome.FromResult("dummy"); - }, - ResilienceContextPool.Shared.Get(), - "state"); outcome.Exception.Should().BeOfType(); - outcome.Exception!.StackTrace.Should().Contain("Execute_Timeout_EnsureStackTrace"); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + outcome.Exception!.StackTrace.Should().NotBeEmpty(); + } } [Fact] diff --git a/test/Polly.Core.Tests/Utils/Pipeline/PipelineComponentFactoryTests.cs b/test/Polly.Core.Tests/Utils/Pipeline/PipelineComponentFactoryTests.cs index 5beed7f1567..1a23c41ec23 100644 --- a/test/Polly.Core.Tests/Utils/Pipeline/PipelineComponentFactoryTests.cs +++ b/test/Polly.Core.Tests/Utils/Pipeline/PipelineComponentFactoryTests.cs @@ -5,22 +5,63 @@ namespace Polly.Core.Tests.Utils.Pipeline; public class PipelineComponentFactoryTests { - [Fact] - public void WithDisposableCallbacks_NoCallbacks_ReturnsOriginalComponent() +#pragma warning disable IDE0028 + public static TheoryData> EmptyCallbacks = new() + { + Array.Empty(), + Enumerable.Empty(), + new List(), + new EmptyActionEnumerable(), // Explicitly does not provide TryGetNonEnumeratedCount() + }; + + public static TheoryData> NonEmptyCallbacks = new() + { + new[] { () => { } }, + Enumerable.TakeWhile(Enumerable.Repeat(() => { }, 50), (_, i) => i < 1), // Defeat optimisation for TryGetNonEnumeratedCount() + new List { () => { } }, + }; +#pragma warning restore IDE0028 + + [Theory] + [MemberData(nameof(EmptyCallbacks))] + public void WithDisposableCallbacks_NoCallbacks_ReturnsOriginalComponent(IEnumerable callbacks) { var component = Substitute.For(); - var result = PipelineComponentFactory.WithDisposableCallbacks(component, new List()); + var result = PipelineComponentFactory.WithDisposableCallbacks(component, callbacks); result.Should().BeSameAs(component); } - [Fact] - public void PipelineComponentFactory_Should_Return_WrapperComponent_With_Callbacks() + [Theory] + [MemberData(nameof(NonEmptyCallbacks))] + public void PipelineComponentFactory_Should_Return_WrapperComponent_With_Callbacks(IEnumerable callbacks) { var component = Substitute.For(); - var callbacks = new List { () => { } }; var result = PipelineComponentFactory.WithDisposableCallbacks(component, callbacks); result.Should().BeOfType(); } + + private sealed class EmptyActionEnumerable : IEnumerable, IEnumerator + { + public Action Current => null!; + + object IEnumerator.Current => null!; + + public void Dispose() + { + // No-op + } + + public IEnumerator GetEnumerator() => this; + + public bool MoveNext() => false; + + public void Reset() + { + // No-op + } + + IEnumerator IEnumerable.GetEnumerator() => this; + } } diff --git a/test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs b/test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs index e149792fbf1..4179e5932d8 100644 --- a/test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs +++ b/test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs @@ -1,8 +1,10 @@ using System.Globalization; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using NSubstitute; using Polly.DependencyInjection; using Polly.Registry; using Polly.Telemetry; @@ -14,7 +16,7 @@ public class PollyServiceCollectionExtensionTests private const string Key = "my-pipeline"; private ServiceCollection _services; - public PollyServiceCollectionExtensionTests() => _services = new ServiceCollection(); + public PollyServiceCollectionExtensionTests() => _services = []; [Fact] public void AddResiliencePipeline_ArgValidation() @@ -23,7 +25,7 @@ public void AddResiliencePipeline_ArgValidation() Assert.Throws(() => AddResiliencePipeline(Key)); Assert.Throws(() => AddResiliencePipeline(Key)); - _services = new ServiceCollection(); + _services = []; Assert.Throws(() => _services.AddResiliencePipeline( Key, (Action>)null!)); @@ -250,6 +252,37 @@ public void AddResiliencePipeline_Multiple_Ok() .HaveCount(30); } + [InlineData(true)] + [InlineData(false)] + [Theory] + public void AddResiliencePipeline_EnsureTimeProvider(bool timeProviderRegistered) + { + var timeProvider = Substitute.For(); + var asserted = false; + + if (timeProviderRegistered) + { + _services.TryAddSingleton(timeProvider); + } + + _services.AddResiliencePipeline("dummy", builder => + { + if (timeProviderRegistered) + { + builder.TimeProvider.Should().Be(timeProvider); + } + else + { + builder.TimeProvider.Should().BeNull(); + } + + asserted = true; + }); + + CreateProvider().GetPipeline("dummy"); + asserted.Should().BeTrue(); + } + [Fact] public void AddResiliencePipelineRegistry_Ok() { diff --git a/test/Polly.Extensions.Tests/Polly.Extensions.Tests.csproj b/test/Polly.Extensions.Tests/Polly.Extensions.Tests.csproj index 0dbfcf24307..b33283d0398 100644 --- a/test/Polly.Extensions.Tests/Polly.Extensions.Tests.csproj +++ b/test/Polly.Extensions.Tests/Polly.Extensions.Tests.csproj @@ -1,6 +1,6 @@  - net7.0;net6.0 + net8.0;net7.0;net6.0 $(TargetFrameworks);net481 Test enable diff --git a/test/Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj b/test/Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj index 002a146ce14..412e2580fb0 100644 --- a/test/Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj +++ b/test/Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj @@ -1,6 +1,6 @@  - net7.0;net6.0 + net8.0;net7.0;net6.0 $(TargetFrameworks);net481 Test enable diff --git a/test/Polly.Specs/Polly.Specs.csproj b/test/Polly.Specs/Polly.Specs.csproj index 9f173f8b1d6..43cf95ff43e 100644 --- a/test/Polly.Specs/Polly.Specs.csproj +++ b/test/Polly.Specs/Polly.Specs.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0 + net6.0;net7.0;net8.0 $(TargetFrameworks);net481 enable Test diff --git a/test/Polly.TestUtils/Polly.TestUtils.csproj b/test/Polly.TestUtils/Polly.TestUtils.csproj index 85f854e91bb..66f550f0165 100644 --- a/test/Polly.TestUtils/Polly.TestUtils.csproj +++ b/test/Polly.TestUtils/Polly.TestUtils.csproj @@ -1,6 +1,6 @@  - net7.0;net6.0 + net8.0;net7.0;net6.0 $(TargetFrameworks);net481 Library enable diff --git a/test/Polly.Testing.Tests/Polly.Testing.Tests.csproj b/test/Polly.Testing.Tests/Polly.Testing.Tests.csproj index 2469343b97e..7fd94fffdc4 100644 --- a/test/Polly.Testing.Tests/Polly.Testing.Tests.csproj +++ b/test/Polly.Testing.Tests/Polly.Testing.Tests.csproj @@ -1,6 +1,6 @@  - net7.0;net6.0 + net8.0;net7.0;net6.0 $(TargetFrameworks);net481 Test enable