diff --git a/src/Polly/Bulkhead/BulkheadSemaphoreFactory.cs b/src/Polly/Bulkhead/BulkheadSemaphoreFactory.cs index 4427c2458cd..2a340a1e8ca 100644 --- a/src/Polly/Bulkhead/BulkheadSemaphoreFactory.cs +++ b/src/Polly/Bulkhead/BulkheadSemaphoreFactory.cs @@ -7,9 +7,7 @@ public static (SemaphoreSlim MaxParallelizationSemaphore, SemaphoreSlim MaxQueue { var maxParallelizationSemaphore = new SemaphoreSlim(maxParallelization, maxParallelization); - var maxQueuingCompounded = maxQueueingActions <= int.MaxValue - maxParallelization - ? maxQueueingActions + maxParallelization - : int.MaxValue; + var maxQueuingCompounded = Math.Min(maxQueueingActions + maxParallelization, int.MaxValue); var maxQueuedActionsSemaphore = new SemaphoreSlim(maxQueuingCompounded, maxQueuingCompounded); return (maxParallelizationSemaphore, maxQueuedActionsSemaphore); diff --git a/src/Polly/Caching/AsyncCacheTResultSyntax.cs b/src/Polly/Caching/AsyncCacheTResultSyntax.cs index 2ea8180542c..5437b93cd78 100644 --- a/src/Polly/Caching/AsyncCacheTResultSyntax.cs +++ b/src/Polly/Caching/AsyncCacheTResultSyntax.cs @@ -22,7 +22,7 @@ public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider throw new ArgumentNullException(nameof(cacheProvider)); } - return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); + return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); } /// @@ -45,7 +45,7 @@ public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider throw new ArgumentNullException(nameof(cacheProvider)); } - return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); + return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); } /// @@ -78,7 +78,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), cacheKeyStrategy.GetCacheKey, onCacheError); + return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), cacheKeyStrategy.GetCacheKey, onCacheError); } /// @@ -112,7 +112,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, cacheKeyStrategy.GetCacheKey, onCacheError); + return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, cacheKeyStrategy.GetCacheKey, onCacheError); } /// @@ -136,7 +136,7 @@ public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider throw new ArgumentNullException(nameof(cacheProvider)); } - return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), cacheKeyStrategy, onCacheError); + return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), cacheKeyStrategy, onCacheError); } /// @@ -161,7 +161,7 @@ public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider throw new ArgumentNullException(nameof(cacheProvider)); } - return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, cacheKeyStrategy, onCacheError); + return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, cacheKeyStrategy, onCacheError); } /// @@ -197,7 +197,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheProvider)); } - return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); } /// @@ -234,7 +234,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheProvider)); } - return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); } /// @@ -278,7 +278,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), cacheKeyStrategy.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), cacheKeyStrategy.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); } /// @@ -323,7 +323,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, cacheKeyStrategy.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, cacheKeyStrategy.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); } /// @@ -362,7 +362,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheProvider)); } - return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), cacheKeyStrategy, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + return CacheAsync(cacheProvider.AsyncFor(), new RelativeTtl(ttl), cacheKeyStrategy, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); } /// @@ -402,7 +402,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheProvider)); } - return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, cacheKeyStrategy, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + return CacheAsync(cacheProvider.AsyncFor(), ttlStrategy, cacheKeyStrategy, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); } /// @@ -418,7 +418,7 @@ public static AsyncCachePolicy CacheAsync( /// The policy instance. /// Thrown when is . public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider cacheProvider, TimeSpan ttl, Action? onCacheError = null) => - CacheAsync(cacheProvider, new RelativeTtl(ttl), DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); + CacheAsync(cacheProvider, new RelativeTtl(ttl), DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -434,7 +434,7 @@ public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider< /// Thrown when is . /// Thrown when is . public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider cacheProvider, ITtlStrategy ttlStrategy, Action? onCacheError = null) => - CacheAsync(cacheProvider, ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); + CacheAsync(cacheProvider, ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -450,7 +450,7 @@ public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider< /// Thrown when is . /// Thrown when is . public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider cacheProvider, ITtlStrategy ttlStrategy, Action? onCacheError = null) => - CacheAsync(cacheProvider, ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); + CacheAsync(cacheProvider, ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -477,7 +477,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - return CacheAsync(cacheProvider, new RelativeTtl(ttl), cacheKeyStrategy.GetCacheKey, onCacheError); + return CacheAsync(cacheProvider, new RelativeTtl(ttl), cacheKeyStrategy.GetCacheKey, onCacheError); } /// @@ -506,15 +506,13 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - Action emptyDelegate = (_, _) => { }; - - return CacheAsync( + return CacheAsync( cacheProvider, ttlStrategy, cacheKeyStrategy.GetCacheKey, - emptyDelegate, - emptyDelegate, - emptyDelegate, + EmptyCallback, + EmptyCallback, + EmptyCallback, onCacheError, onCacheError); } @@ -545,15 +543,13 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - Action emptyDelegate = (_, _) => { }; - - return CacheAsync( + return CacheAsync( cacheProvider, ttlStrategy, cacheKeyStrategy.GetCacheKey, - emptyDelegate, - emptyDelegate, - emptyDelegate, + EmptyCallback, + EmptyCallback, + EmptyCallback, onCacheError, onCacheError); } @@ -573,7 +569,7 @@ public static AsyncCachePolicy CacheAsync( /// Thrown when is . /// Thrown when is . public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider cacheProvider, TimeSpan ttl, Func cacheKeyStrategy, Action? onCacheError = null) => - CacheAsync(cacheProvider, new RelativeTtl(ttl), cacheKeyStrategy, onCacheError); + CacheAsync(cacheProvider, new RelativeTtl(ttl), cacheKeyStrategy, onCacheError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -591,11 +587,7 @@ public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider< /// Thrown when is . /// Thrown when is . public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider cacheProvider, ITtlStrategy ttlStrategy, Func cacheKeyStrategy, Action? onCacheError = null) - { - Action emptyDelegate = (_, _) => { }; - - return CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategy, emptyDelegate, emptyDelegate, emptyDelegate, onCacheError, onCacheError); - } + => CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategy, EmptyCallback, EmptyCallback, EmptyCallback, onCacheError, onCacheError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -613,11 +605,7 @@ public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider< /// Thrown when is . /// Thrown when is . public static AsyncCachePolicy CacheAsync(IAsyncCacheProvider cacheProvider, ITtlStrategy ttlStrategy, Func cacheKeyStrategy, Action? onCacheError = null) - { - Action emptyDelegate = (_, _) => { }; - - return CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategy, emptyDelegate, emptyDelegate, emptyDelegate, onCacheError, onCacheError); - } + => CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategy, EmptyCallback, EmptyCallback, EmptyCallback, onCacheError, onCacheError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -646,8 +634,15 @@ public static AsyncCachePolicy CacheAsync( Action onCachePut, Action? onCacheGetError, Action? onCachePutError) => - CacheAsync(cacheProvider, new RelativeTtl(ttl), DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheGet, onCacheMiss, - onCachePut, onCacheGetError, onCachePutError); + CacheAsync( + cacheProvider, + new RelativeTtl(ttl), + DefaultCacheKeyStrategy.Instance.GetCacheKey, + onCacheGet, + onCacheMiss, + onCachePut, + onCacheGetError, + onCachePutError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -677,7 +672,15 @@ public static AsyncCachePolicy CacheAsync( Action onCachePut, Action? onCacheGetError, Action? onCachePutError) => - CacheAsync(cacheProvider, ttlStrategy.For(), DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + CacheAsync( + cacheProvider, + ttlStrategy.For(), + DefaultCacheKeyStrategy.Instance.GetCacheKey, + onCacheGet, + onCacheMiss, + onCachePut, + onCacheGetError, + onCachePutError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -707,7 +710,15 @@ public static AsyncCachePolicy CacheAsync( Action onCachePut, Action? onCacheGetError, Action? onCachePutError) => - CacheAsync(cacheProvider, ttlStrategy, DefaultCacheKeyStrategy.Instance.GetCacheKey, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + CacheAsync( + cacheProvider, + ttlStrategy, + DefaultCacheKeyStrategy.Instance.GetCacheKey, + onCacheGet, + onCacheMiss, + onCachePut, + onCacheGetError, + onCachePutError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -745,7 +756,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - return CacheAsync( + return CacheAsync( cacheProvider, new RelativeTtl(ttl), cacheKeyStrategy.GetCacheKey, @@ -793,7 +804,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - return CacheAsync( + return CacheAsync( cacheProvider, ttlStrategy.For(), cacheKeyStrategy.GetCacheKey, @@ -841,7 +852,7 @@ public static AsyncCachePolicy CacheAsync( throw new ArgumentNullException(nameof(cacheKeyStrategy)); } - return CacheAsync( + return CacheAsync( cacheProvider, ttlStrategy, cacheKeyStrategy.GetCacheKey, @@ -882,8 +893,15 @@ public static AsyncCachePolicy CacheAsync( Action onCachePut, Action? onCacheGetError, Action? onCachePutError) => - CacheAsync(cacheProvider, new RelativeTtl(ttl), cacheKeyStrategy, onCacheGet, onCacheMiss, - onCachePut, onCacheGetError, onCachePutError); + CacheAsync( + cacheProvider, + new RelativeTtl(ttl), + cacheKeyStrategy, + onCacheGet, + onCacheMiss, + onCachePut, + onCacheGetError, + onCachePutError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -916,7 +934,15 @@ public static AsyncCachePolicy CacheAsync( Action onCachePut, Action? onCacheGetError, Action? onCachePutError) => - CacheAsync(cacheProvider, ttlStrategy.For(), cacheKeyStrategy, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); + CacheAsync( + cacheProvider, + ttlStrategy.For(), + cacheKeyStrategy, + onCacheGet, + onCacheMiss, + onCachePut, + onCacheGetError, + onCachePutError); /// /// Builds an that will function like a result cache for delegate executions returning a . @@ -982,4 +1008,9 @@ public static AsyncCachePolicy CacheAsync( return new AsyncCachePolicy(cacheProvider, ttlStrategy, cacheKeyStrategy, onCacheGet, onCacheMiss, onCachePut, onCacheGetError, onCachePutError); } + + private static void EmptyCallback(Context context, string key) + { + // No-op + } } diff --git a/src/Polly/Caching/AsyncGenericCacheProvider.cs b/src/Polly/Caching/AsyncGenericCacheProvider.cs index 090b8cb575a..43bdd89b944 100644 --- a/src/Polly/Caching/AsyncGenericCacheProvider.cs +++ b/src/Polly/Caching/AsyncGenericCacheProvider.cs @@ -14,8 +14,16 @@ internal AsyncGenericCacheProvider(IAsyncCacheProvider nonGenericCacheProvider) async Task<(bool, TCacheFormat?)> IAsyncCacheProvider.TryGetAsync(string key, CancellationToken cancellationToken, bool continueOnCapturedContext) { - (bool cacheHit, object? result) = await _wrappedCacheProvider.TryGetAsync(key, cancellationToken, continueOnCapturedContext).ConfigureAwait(continueOnCapturedContext); - return (cacheHit, (TCacheFormat?)(result ?? default(TCacheFormat))); + (bool cacheHit, object? cached) = await _wrappedCacheProvider.TryGetAsync(key, cancellationToken, continueOnCapturedContext).ConfigureAwait(continueOnCapturedContext); + + TCacheFormat? result = default; + + if (cacheHit) + { + result = (TCacheFormat?)cached; + } + + return (cacheHit, result); } Task IAsyncCacheProvider.PutAsync(string key, TCacheFormat? value, Ttl ttl, CancellationToken cancellationToken, bool continueOnCapturedContext) => diff --git a/src/Polly/Caching/GenericCacheProvider.cs b/src/Polly/Caching/GenericCacheProvider.cs index abfec5768be..621041313be 100644 --- a/src/Polly/Caching/GenericCacheProvider.cs +++ b/src/Polly/Caching/GenericCacheProvider.cs @@ -14,8 +14,16 @@ internal GenericCacheProvider(ISyncCacheProvider nonGenericCacheProvider) => (bool, TCacheFormat?) ISyncCacheProvider.TryGet(string key) { - (bool cacheHit, object? result) = _wrappedCacheProvider.TryGet(key); - return (cacheHit, (TCacheFormat?)(result ?? default(TCacheFormat))); + (bool cacheHit, object? cached) = _wrappedCacheProvider.TryGet(key); + + TCacheFormat? result = default; + + if (cacheHit) + { + result = (TCacheFormat?)cached; + } + + return (cacheHit, result); } void ISyncCacheProvider.Put(string key, TCacheFormat? value, Ttl ttl) => diff --git a/src/Polly/Caching/NonSlidingTtl.cs b/src/Polly/Caching/NonSlidingTtl.cs index 77b7a39b0ee..e3bb0d38eb0 100644 --- a/src/Polly/Caching/NonSlidingTtl.cs +++ b/src/Polly/Caching/NonSlidingTtl.cs @@ -30,8 +30,7 @@ protected NonSlidingTtl(DateTimeOffset absoluteExpirationTime) => /// A representing the remaining Ttl of the cached item. public Ttl GetTtl(Context context, object? result) { - TimeSpan untilPointInTime = absoluteExpirationTime.Subtract(SystemClock.DateTimeOffsetUtcNow()); - TimeSpan remaining = untilPointInTime > TimeSpan.Zero ? untilPointInTime : TimeSpan.Zero; - return new Ttl(remaining, false); + long remaining = Math.Max(0, absoluteExpirationTime.Subtract(SystemClock.DateTimeOffsetUtcNow()).Ticks); + return new Ttl(TimeSpan.FromTicks(remaining), false); } } diff --git a/src/Polly/CircuitBreaker/AdvancedCircuitController.cs b/src/Polly/CircuitBreaker/AdvancedCircuitController.cs index 79e4783007c..9225d42ac1c 100644 --- a/src/Polly/CircuitBreaker/AdvancedCircuitController.cs +++ b/src/Polly/CircuitBreaker/AdvancedCircuitController.cs @@ -2,10 +2,11 @@ internal sealed class AdvancedCircuitController : CircuitStateController { - private const short NumberOfWindows = 10; internal static readonly long ResolutionOfCircuitTimer = TimeSpan.FromMilliseconds(20).Ticks; +#pragma warning disable IDE0032 // Use auto property private readonly IHealthMetrics _metrics; +#pragma warning restore IDE0032 // Use auto property private readonly double _failureThreshold; private readonly int _minimumThroughput; @@ -19,14 +20,16 @@ public AdvancedCircuitController( Action onHalfOpen) : base(durationOfBreak, onBreak, onReset, onHalfOpen) { - _metrics = samplingDuration.Ticks < ResolutionOfCircuitTimer * NumberOfWindows + _metrics = samplingDuration.Ticks < ResolutionOfCircuitTimer * RollingHealthMetrics.WindowCount ? new SingleHealthMetrics(samplingDuration) - : new RollingHealthMetrics(samplingDuration, NumberOfWindows); + : new RollingHealthMetrics(samplingDuration); _failureThreshold = failureThreshold; _minimumThroughput = minimumThroughput; } + internal IHealthMetrics Metrics => _metrics; // For testing + public override void OnCircuitReset(Context context) { using var _ = TimedLock.Lock(Lock); diff --git a/src/Polly/CircuitBreaker/CircuitStateController.cs b/src/Polly/CircuitBreaker/CircuitStateController.cs index 33e9db84335..2585becbc4f 100644 --- a/src/Polly/CircuitBreaker/CircuitStateController.cs +++ b/src/Polly/CircuitBreaker/CircuitStateController.cs @@ -83,10 +83,11 @@ protected void Break_NeedsLock(Context context) => private void BreakFor_NeedsLock(TimeSpan durationOfBreak, Context context) { - bool willDurationTakeUsPastDateTimeMaxValue = durationOfBreak > DateTime.MaxValue - SystemClock.UtcNow(); - BlockedTill = willDurationTakeUsPastDateTimeMaxValue - ? DateTime.MaxValue.Ticks - : (SystemClock.UtcNow() + durationOfBreak).Ticks; + // Prevent overflow if DurationOfBreak goes beyond the maximum possible DateTime + ulong utcNowTicks = (ulong)SystemClock.UtcNow().Ticks; + ulong durationOfBreakTicks = (ulong)durationOfBreak.Ticks; + + BlockedTill = (long)Math.Min(utcNowTicks + durationOfBreakTicks, (ulong)DateTime.MaxValue.Ticks); var transitionedState = InternalCircuitState; InternalCircuitState = CircuitState.Open; diff --git a/src/Polly/CircuitBreaker/RollingHealthMetrics.cs b/src/Polly/CircuitBreaker/RollingHealthMetrics.cs index 23c1d7465b3..4631a24aebf 100644 --- a/src/Polly/CircuitBreaker/RollingHealthMetrics.cs +++ b/src/Polly/CircuitBreaker/RollingHealthMetrics.cs @@ -3,18 +3,21 @@ namespace Polly.CircuitBreaker; internal sealed class RollingHealthMetrics : IHealthMetrics { + internal const short WindowCount = 10; + private readonly long _samplingDuration; private readonly long _windowDuration; private readonly Queue _windows; private HealthCount? _currentWindow; - public RollingHealthMetrics(TimeSpan samplingDuration, short numberOfWindows) + public RollingHealthMetrics(TimeSpan samplingDuration) { _samplingDuration = samplingDuration.Ticks; + _windowDuration = _samplingDuration / WindowCount; - _windowDuration = _samplingDuration / numberOfWindows; - _windows = new(numberOfWindows + 1); + // stryker disable once all : only affects capacity and not logic + _windows = new(WindowCount + 1); } public void IncrementSuccess_NeedsLock() @@ -60,20 +63,24 @@ public HealthCount GetHealthCount_NeedsLock() private HealthCount ActualiseCurrentMetric_NeedsLock() { var now = SystemClock.UtcNow().Ticks; - if (_currentWindow == null || now - _currentWindow.StartedAt >= _windowDuration) + var currentWindow = _currentWindow; + + // stryker disable once all : no means to test this + if (currentWindow == null || now - currentWindow.StartedAt >= _windowDuration) { - _currentWindow = new() + _currentWindow = currentWindow = new() { StartedAt = now }; - _windows.Enqueue(_currentWindow); + _windows.Enqueue(currentWindow); } + // stryker disable once all : no means to test this while (_windows.Count > 0 && now - _windows.Peek().StartedAt >= _samplingDuration) { _windows.Dequeue(); } - return _currentWindow; + return currentWindow; } } diff --git a/src/Polly/Policy.HandleSyntax.cs b/src/Polly/Policy.HandleSyntax.cs index bd1b6ebc4d0..267ac18a7cc 100644 --- a/src/Polly/Policy.HandleSyntax.cs +++ b/src/Polly/Policy.HandleSyntax.cs @@ -123,5 +123,5 @@ public static PolicyBuilder HandleResult(Func resultPred /// This policy filter matches the value returned using .Equals(), ideally suited for value types such as int and enum. To match characteristics of class return types, consider the overload taking a result predicate. /// The PolicyBuilder instance. public static PolicyBuilder HandleResult(TResult result) => - HandleResult(r => (!Equals(r, default(TResult)) && r.Equals(result)) || (Equals(r, default(TResult)) && Equals(result, default(TResult)))); + HandleResult(r => EqualityComparer.Default.Equals(r, result)); } diff --git a/src/Polly/Policy.SyncNonGenericImplementation.cs b/src/Polly/Policy.SyncNonGenericImplementation.cs index ddb86880425..fb1c5331ba1 100644 --- a/src/Polly/Policy.SyncNonGenericImplementation.cs +++ b/src/Polly/Policy.SyncNonGenericImplementation.cs @@ -10,7 +10,7 @@ public abstract partial class Policy /// A token to signal that execution should be cancelled. [DebuggerStepThrough] protected virtual void Implementation(Action action, Context context, CancellationToken cancellationToken) => - Implementation((ctx, token) => + Implementation((ctx, token) => { action(ctx, token); return EmptyStruct.Instance; diff --git a/src/Polly/PolicyBuilder.OrSyntax.cs b/src/Polly/PolicyBuilder.OrSyntax.cs index 5d493c5aac6..19b8ab0148d 100644 --- a/src/Polly/PolicyBuilder.OrSyntax.cs +++ b/src/Polly/PolicyBuilder.OrSyntax.cs @@ -109,7 +109,7 @@ public PolicyBuilder OrResult(Func resultPredic /// This policy filter matches the value returned using .Equals(), ideally suited for value types such as int and enum. To match characteristics of class return types, consider the overload taking a result predicate. /// The PolicyBuilder instance. public PolicyBuilder OrResult(TResult result) => - OrResult(r => (!Equals(r, default(TResult)) && r.Equals(result)) || (Equals(r, default(TResult)) && Equals(result, default(TResult)))); + OrResult(r => EqualityComparer.Default.Equals(r, result)); #endregion } diff --git a/src/Polly/Polly.csproj b/src/Polly/Polly.csproj index 70339dbc451..4078c2f8734 100644 --- a/src/Polly/Polly.csproj +++ b/src/Polly/Polly.csproj @@ -5,7 +5,7 @@ Polly true Library - 80 + 97 true $(NoWarn);RS0037 diff --git a/src/Polly/RateLimit/AsyncRateLimitSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitSyntax.cs index 19eaa6c79c2..8263e68eb62 100644 --- a/src/Polly/RateLimit/AsyncRateLimitSyntax.cs +++ b/src/Polly/RateLimit/AsyncRateLimitSyntax.cs @@ -49,7 +49,7 @@ public static AsyncRateLimitPolicy RateLimitAsync( throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, "The number of executions per timespan must be positive."); } - IRateLimiter rateLimiter = RateLimiterFactory.Create(onePer, maxBurst); + IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(onePer, maxBurst); return new AsyncRateLimitPolicy(rateLimiter); } diff --git a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs index f54c79cdbe3..d918834ae80 100644 --- a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs +++ b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs @@ -85,7 +85,7 @@ public static AsyncRateLimitPolicy RateLimitAsync( throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, "The number of executions per timespan must be positive."); } - IRateLimiter rateLimiter = RateLimiterFactory.Create(onePer, maxBurst); + IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(onePer, maxBurst); return new AsyncRateLimitPolicy(rateLimiter, retryAfterFactory); } diff --git a/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs index 351edf7b297..7c724f58b3d 100644 --- a/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs +++ b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs @@ -28,11 +28,6 @@ internal sealed class LockFreeTokenBucketRateLimiter : IRateLimiter /// public LockFreeTokenBucketRateLimiter(TimeSpan onePer, long bucketCapacity) { - if (onePer <= TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(onePer), onePer, $"The {nameof(LockFreeTokenBucketRateLimiter)} must specify a positive TimeSpan for how often an execution is permitted."); - } - _addTokenTickInterval = onePer.Ticks; _bucketCapacity = bucketCapacity; diff --git a/src/Polly/RateLimit/RateLimitSyntax.cs b/src/Polly/RateLimit/RateLimitSyntax.cs index 9ca08f04a9b..060f7a0c5eb 100644 --- a/src/Polly/RateLimit/RateLimitSyntax.cs +++ b/src/Polly/RateLimit/RateLimitSyntax.cs @@ -49,7 +49,7 @@ public static RateLimitPolicy RateLimit( throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, "The number of executions per timespan must be positive."); } - IRateLimiter rateLimiter = RateLimiterFactory.Create(onePer, maxBurst); + IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(onePer, maxBurst); return new RateLimitPolicy(rateLimiter); } diff --git a/src/Polly/RateLimit/RateLimitTResultSyntax.cs b/src/Polly/RateLimit/RateLimitTResultSyntax.cs index 98f243f59c7..343369498bd 100644 --- a/src/Polly/RateLimit/RateLimitTResultSyntax.cs +++ b/src/Polly/RateLimit/RateLimitTResultSyntax.cs @@ -85,7 +85,7 @@ public static RateLimitPolicy RateLimit( throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, "The number of executions per timespan must be positive."); } - IRateLimiter rateLimiter = RateLimiterFactory.Create(onePer, maxBurst); + IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(onePer, maxBurst); return new RateLimitPolicy(rateLimiter, retryAfterFactory); } diff --git a/src/Polly/RateLimit/RateLimiterFactory.cs b/src/Polly/RateLimit/RateLimiterFactory.cs deleted file mode 100644 index 2375af528e8..00000000000 --- a/src/Polly/RateLimit/RateLimiterFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable enable -namespace Polly.RateLimit; - -internal static class RateLimiterFactory -{ - public static IRateLimiter Create(TimeSpan onePer, int bucketCapacity) => - new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); -} diff --git a/src/Polly/Registry/PolicyRegistry.cs b/src/Polly/Registry/PolicyRegistry.cs index 36948bf4dd0..21306f75062 100644 --- a/src/Polly/Registry/PolicyRegistry.cs +++ b/src/Polly/Registry/PolicyRegistry.cs @@ -113,8 +113,14 @@ public TPolicy Get(string key) public bool TryGet(string key, out TPolicy policy) where TPolicy : IsPolicy { + policy = default; bool got = _registry.TryGetValue(key, out IsPolicy value); - policy = got ? (TPolicy)value : default; + + if (got) + { + policy = (TPolicy)value; + } + return got; } @@ -158,8 +164,14 @@ public bool TryRemove(string key, out TPolicy policy) { var registry = ThrowIfNotConcurrentImplementation(); + policy = default; bool got = registry.TryRemove(key, out IsPolicy value); - policy = got ? (TPolicy)value : default; + + if (got) + { + policy = (TPolicy)value; + } + return got; } diff --git a/src/Polly/Retry/AsyncRetrySyntax.cs b/src/Polly/Retry/AsyncRetrySyntax.cs index 8f7d13651e1..e3182973aed 100644 --- a/src/Polly/Retry/AsyncRetrySyntax.cs +++ b/src/Polly/Retry/AsyncRetrySyntax.cs @@ -20,11 +20,7 @@ public static AsyncRetryPolicy RetryAsync(this PolicyBuilder policyBuilder) => /// The retry count. /// The policy instance. public static AsyncRetryPolicy RetryAsync(this PolicyBuilder policyBuilder, int retryCount) - { - Action doNothing = (_, _, _) => { }; - - return policyBuilder.RetryAsync(retryCount, onRetry: doNothing); - } + => policyBuilder.RetryAsync(retryCount, onRetry: EmptyHandler); /// /// Builds an that will retry once @@ -172,11 +168,7 @@ public static AsyncRetryPolicy RetryAsync(this PolicyBuilder policyBuilder, int /// The policy builder. /// The policy instance. public static AsyncRetryPolicy RetryForeverAsync(this PolicyBuilder policyBuilder) - { - Action doNothing = _ => { }; - - return policyBuilder.RetryForeverAsync(doNothing); - } + => policyBuilder.RetryForeverAsync(static (_) => { }); /// /// Builds an that will retry indefinitely @@ -348,11 +340,7 @@ public static AsyncRetryPolicy RetryForeverAsync(this PolicyBuilder policyBuilde /// The function that provides the duration to wait for a particular retry attempt. /// The policy instance. public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func sleepDurationProvider) - { - Action doNothing = (_, _) => { }; - - return policyBuilder.WaitAndRetryAsync(retryCount, sleepDurationProvider, doNothing); - } + => policyBuilder.WaitAndRetryAsync(retryCount, sleepDurationProvider, EmptyHandler); /// /// Builds an that will wait and retry times @@ -659,8 +647,11 @@ public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilde /// The policy instance. /// retryCount;Value must be greater than or equal to zero. /// Thrown when or is . - public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, - Func sleepDurationProvider, Func onRetryAsync) + public static AsyncRetryPolicy WaitAndRetryAsync( + this PolicyBuilder policyBuilder, + int retryCount, + Func sleepDurationProvider, + Func onRetryAsync) { if (retryCount < 0) { @@ -693,11 +684,7 @@ public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilde /// The sleep durations to wait for on each retry. /// The policy instance. public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, IEnumerable sleepDurations) - { - Action doNothing = (_, _, _, _) => { }; - - return policyBuilder.WaitAndRetryAsync(sleepDurations, doNothing); - } + => policyBuilder.WaitAndRetryAsync(sleepDurations, EmptyHandlerWithContext); /// /// Builds an that will wait and retry as many times as there are provided @@ -859,16 +846,7 @@ public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilde /// The policy instance. /// Thrown when is . public static AsyncRetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func sleepDurationProvider) - { - if (sleepDurationProvider == null) - { - throw new ArgumentNullException(nameof(sleepDurationProvider)); - } - - Action doNothing = (_, _) => { }; - - return policyBuilder.WaitAndRetryForeverAsync(sleepDurationProvider, doNothing); - } + => policyBuilder.WaitAndRetryForeverAsync(sleepDurationProvider, EmptyHandler); /// /// Builds an that will wait and retry indefinitely until the action succeeds. @@ -880,16 +858,7 @@ public static AsyncRetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder polic /// The policy instance. /// Thrown when is . public static AsyncRetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func sleepDurationProvider) - { - if (sleepDurationProvider == null) - { - throw new ArgumentNullException(nameof(sleepDurationProvider)); - } - - Action doNothing = (_, _, _) => { }; - - return policyBuilder.WaitAndRetryForeverAsync(sleepDurationProvider, doNothing); - } + => policyBuilder.WaitAndRetryForeverAsync(sleepDurationProvider, EmptyHandlerWithContext); /// /// Builds an that will wait and retry indefinitely @@ -1176,5 +1145,20 @@ public static AsyncRetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder polic (exception, timespan, i, ctx) => onRetryAsync(exception, i, timespan, ctx), sleepDurationProvider: sleepDurationProvider); } + + private static void EmptyHandler(Exception exception, int retryCount, Context context) + { + // No-op + } + + private static void EmptyHandler(Exception exception, TimeSpan duration) + { + // No-op + } + + private static void EmptyHandlerWithContext(Exception exception, TimeSpan duration, Context context) + { + // No-op + } } diff --git a/src/Polly/Retry/AsyncRetryTResultSyntax.cs b/src/Polly/Retry/AsyncRetryTResultSyntax.cs index 799e3b3ee87..a55aabe0186 100644 --- a/src/Polly/Retry/AsyncRetryTResultSyntax.cs +++ b/src/Polly/Retry/AsyncRetryTResultSyntax.cs @@ -533,8 +533,11 @@ public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBu /// The policy instance. /// retryCount;Value must be greater than or equal to zero. /// Thrown when or is . - public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, - Func sleepDurationProvider, Func, TimeSpan, int, Context, Task> onRetryAsync) + public static AsyncRetryPolicy WaitAndRetryAsync( + this PolicyBuilder policyBuilder, + int retryCount, + Func sleepDurationProvider, + Func, TimeSpan, int, Context, Task> onRetryAsync) { if (retryCount < 0) { @@ -575,8 +578,11 @@ public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBu /// The policy instance. /// retryCount;Value must be greater than or equal to zero. /// Thrown when or is . - public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, - Func sleepDurationProvider, Action, TimeSpan, Context> onRetry) + public static AsyncRetryPolicy WaitAndRetryAsync( + this PolicyBuilder policyBuilder, + int retryCount, + Func sleepDurationProvider, + Action, TimeSpan, Context> onRetry) { if (onRetry == null) { diff --git a/src/Polly/Retry/RetryTResultSyntax.cs b/src/Polly/Retry/RetryTResultSyntax.cs index 0f6e29fbd35..8ba74b79a84 100644 --- a/src/Polly/Retry/RetryTResultSyntax.cs +++ b/src/Polly/Retry/RetryTResultSyntax.cs @@ -22,11 +22,7 @@ public static RetryPolicy Retry(this PolicyBuilder po /// The retry count. /// The policy instance. public static RetryPolicy Retry(this PolicyBuilder policyBuilder, int retryCount) - { - Action, int> doNothing = (_, _) => { }; - - return policyBuilder.Retry(retryCount, doNothing); - } + => policyBuilder.Retry(retryCount, static (_, _) => { }); /// /// Builds a that will retry once @@ -114,11 +110,7 @@ public static RetryPolicy Retry(this PolicyBuilder po /// The policy builder. /// The policy instance. public static RetryPolicy RetryForever(this PolicyBuilder policyBuilder) - { - Action> doNothing = _ => { }; - - return policyBuilder.RetryForever(doNothing); - } + => policyBuilder.RetryForever(static _ => { }); /// /// Builds a that will retry indefinitely @@ -211,11 +203,7 @@ public static RetryPolicy RetryForever(this PolicyBuilderThe function that provides the duration to wait for a particular retry attempt. /// The policy instance. public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func sleepDurationProvider) - { - Action, TimeSpan, int, Context> doNothing = (_, _, _, _) => { }; - - return policyBuilder.WaitAndRetry(retryCount, sleepDurationProvider, doNothing); - } + => policyBuilder.WaitAndRetry(retryCount, sleepDurationProvider, EmptyHandlerWithContext); /// /// Builds a that will wait and retry times @@ -323,11 +311,7 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilderThe function that provides the duration to wait for a particular retry attempt. /// The policy instance. public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func sleepDurationProvider) - { - Action, TimeSpan, int, Context> doNothing = (_, _, _, _) => { }; - - return policyBuilder.WaitAndRetry(retryCount, sleepDurationProvider, doNothing); - } + => policyBuilder.WaitAndRetry(retryCount, sleepDurationProvider, EmptyHandlerWithContext); /// /// Builds a that will wait and retry times @@ -387,11 +371,7 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilderThe function that provides the duration to wait for a particular retry attempt. /// The policy instance. public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func, Context, TimeSpan> sleepDurationProvider) - { - Action, TimeSpan, int, Context> doNothing = (_, _, _, _) => { }; - - return policyBuilder.WaitAndRetry(retryCount, sleepDurationProvider, doNothing); - } + => policyBuilder.WaitAndRetry(retryCount, sleepDurationProvider, EmptyHandlerWithContext); /// /// Builds a that will wait and retry times @@ -467,11 +447,7 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilderThe sleep durations to wait for on each retry. /// The policy instance. public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, IEnumerable sleepDurations) - { - Action, TimeSpan> doNothing = (_, _) => { }; - - return policyBuilder.WaitAndRetry(sleepDurations, doNothing); - } + => policyBuilder.WaitAndRetry(sleepDurations, EmptyHandler); /// /// Builds a that will wait and retry as many times as there are provided @@ -555,16 +531,7 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilderThe policy instance. /// Thrown when is . public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, Func sleepDurationProvider) - { - if (sleepDurationProvider == null) - { - throw new ArgumentNullException(nameof(sleepDurationProvider)); - } - - Action, TimeSpan> doNothing = (_, _) => { }; - - return policyBuilder.WaitAndRetryForever(sleepDurationProvider, doNothing); - } + => policyBuilder.WaitAndRetryForever(sleepDurationProvider, EmptyHandler); /// /// Builds a that will wait and retry indefinitely until the action succeeds. @@ -577,16 +544,7 @@ public static RetryPolicy WaitAndRetryForever(this PolicyBuild /// The policy instance. /// Thrown when is . public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, Func sleepDurationProvider) - { - if (sleepDurationProvider == null) - { - throw new ArgumentNullException(nameof(sleepDurationProvider)); - } - - Action, TimeSpan, Context> doNothing = (_, _, _) => { }; - - return policyBuilder.WaitAndRetryForever(sleepDurationProvider, doNothing); - } + => policyBuilder.WaitAndRetryForever(sleepDurationProvider, static (_, _, _) => { }); /// /// Builds a that will wait and retry indefinitely until the action succeeds, @@ -759,4 +717,14 @@ public static RetryPolicy WaitAndRetryForever(this PolicyBuild (exception, timespan, i, ctx) => onRetry(exception, i, timespan, ctx), sleepDurationProvider: sleepDurationProvider); } + + private static void EmptyHandler(DelegateResult result, TimeSpan retryAfter) + { + // No-op + } + + private static void EmptyHandlerWithContext(DelegateResult result, TimeSpan retryAfter, int attempts, Context context) + { + // No-op + } } diff --git a/src/Polly/Timeout/AsyncTimeoutEngine.cs b/src/Polly/Timeout/AsyncTimeoutEngine.cs index dfa168fd4b1..4e4a29fe14b 100644 --- a/src/Polly/Timeout/AsyncTimeoutEngine.cs +++ b/src/Polly/Timeout/AsyncTimeoutEngine.cs @@ -56,6 +56,7 @@ internal static async Task ImplementationAsync( // If timeoutCancellationTokenSource was canceled & our combined token hasn't been signaled, cancel it. // This avoids the exception propagating before the linked token can signal the downstream to cancel. // See https://github.com/App-vNext/Polly/issues/722. + // stryker disable once all : no means to test this if (!combinedTokenSource.IsCancellationRequested && timeoutCancellationTokenSource.IsCancellationRequested) { #if NET8_0_OR_GREATER @@ -72,12 +73,13 @@ private static Task AsTask(this CancellationToken cancellation var tcs = new TaskCompletionSource(); // A generalised version of this method would include a hotpath returning a canceled task (rather than setting up a registration) if (cancellationToken.IsCancellationRequested) on entry. This is omitted, since we only start the timeout countdown in the token _after calling this method. + CancellationTokenRegistration registration = default; - IDisposable registration = null; + // stryker disable once all : no means to test this registration = cancellationToken.Register(() => { tcs.TrySetCanceled(); - registration?.Dispose(); + registration.Dispose(); }, useSynchronizationContext: false); return tcs.Task; diff --git a/src/Polly/Timeout/AsyncTimeoutSyntax.cs b/src/Polly/Timeout/AsyncTimeoutSyntax.cs index 96ee4d14671..eebee5757b1 100644 --- a/src/Polly/Timeout/AsyncTimeoutSyntax.cs +++ b/src/Polly/Timeout/AsyncTimeoutSyntax.cs @@ -40,8 +40,7 @@ public static AsyncTimeoutPolicy TimeoutAsync(int seconds, TimeoutStrategy timeo /// The policy instance. /// seconds;Value must be greater than zero. /// Thrown when is . - public static AsyncTimeoutPolicy TimeoutAsync(int seconds, Func onTimeoutAsync) + public static AsyncTimeoutPolicy TimeoutAsync(int seconds, Func onTimeoutAsync) { TimeoutValidator.ValidateSecondsTimeout(seconds); if (onTimeoutAsync == null) diff --git a/src/Polly/Timeout/TimeoutSyntax.cs b/src/Polly/Timeout/TimeoutSyntax.cs index 4bd230d23e0..22630d1e9bd 100644 --- a/src/Polly/Timeout/TimeoutSyntax.cs +++ b/src/Polly/Timeout/TimeoutSyntax.cs @@ -11,9 +11,7 @@ public partial class Policy public static TimeoutPolicy Timeout(int seconds) { TimeoutValidator.ValidateSecondsTimeout(seconds); - Action doNothing = (_, _, _, _) => { }; - - return Timeout(_ => TimeSpan.FromSeconds(seconds), TimeoutStrategy.Optimistic, doNothing); + return Timeout(_ => TimeSpan.FromSeconds(seconds), TimeoutStrategy.Optimistic, EmptyHandler); } /// @@ -26,9 +24,7 @@ public static TimeoutPolicy Timeout(int seconds) public static TimeoutPolicy Timeout(int seconds, TimeoutStrategy timeoutStrategy) { TimeoutValidator.ValidateSecondsTimeout(seconds); - Action doNothing = (_, _, _, _) => { }; - - return Timeout(_ => TimeSpan.FromSeconds(seconds), timeoutStrategy, doNothing); + return Timeout(_ => TimeSpan.FromSeconds(seconds), timeoutStrategy, EmptyHandler); } /// @@ -114,9 +110,7 @@ public static TimeoutPolicy Timeout(TimeSpan timeout) #pragma warning restore S3872 { TimeoutValidator.ValidateTimeSpanTimeout(timeout); - Action doNothing = (_, _, _, _) => { }; - - return Timeout(_ => timeout, TimeoutStrategy.Optimistic, doNothing); + return Timeout(_ => timeout, TimeoutStrategy.Optimistic, EmptyHandler); } #pragma warning disable S3872 @@ -131,9 +125,7 @@ public static TimeoutPolicy Timeout(TimeSpan timeout, TimeoutStrategy timeoutStr #pragma warning restore S3872 { TimeoutValidator.ValidateTimeSpanTimeout(timeout); - Action doNothing = (_, _, _, _) => { }; - - return Timeout(_ => timeout, timeoutStrategy, doNothing); + return Timeout(_ => timeout, timeoutStrategy, EmptyHandler); } #pragma warning disable S3872 @@ -223,8 +215,7 @@ public static TimeoutPolicy Timeout(Func timeoutProvider) throw new ArgumentNullException(nameof(timeoutProvider)); } - Action doNothing = (_, _, _, _) => { }; - return Timeout(_ => timeoutProvider(), TimeoutStrategy.Optimistic, doNothing); + return Timeout(_ => timeoutProvider(), TimeoutStrategy.Optimistic, EmptyHandler); } /// @@ -241,8 +232,7 @@ public static TimeoutPolicy Timeout(Func timeoutProvider, TimeoutStrat throw new ArgumentNullException(nameof(timeoutProvider)); } - Action doNothing = (_, _, _, _) => { }; - return Timeout(_ => timeoutProvider(), timeoutStrategy, doNothing); + return Timeout(_ => timeoutProvider(), timeoutStrategy, EmptyHandler); } /// @@ -330,10 +320,7 @@ public static TimeoutPolicy Timeout(Func timeoutProvider, TimeoutStrat /// Thrown when is . /// The policy instance. public static TimeoutPolicy Timeout(Func timeoutProvider) - { - Action doNothing = (_, _, _, _) => { }; - return Timeout(timeoutProvider, TimeoutStrategy.Optimistic, doNothing); - } + => Timeout(timeoutProvider, TimeoutStrategy.Optimistic, EmptyHandler); /// /// Builds a that will wait for a delegate to complete for a specified period of time. A will be thrown if the delegate does not complete within the configured timeout. @@ -343,10 +330,7 @@ public static TimeoutPolicy Timeout(Func timeoutProvider) /// The policy instance. /// Thrown when is . public static TimeoutPolicy Timeout(Func timeoutProvider, TimeoutStrategy timeoutStrategy) - { - Action doNothing = (_, _, _, _) => { }; - return Timeout(timeoutProvider, timeoutStrategy, doNothing); - } + => Timeout(timeoutProvider, timeoutStrategy, EmptyHandler); /// /// Builds a that will wait for a delegate to complete for a specified period of time. A will be thrown if the delegate does not complete within the configured timeout. @@ -422,4 +406,9 @@ public static TimeoutPolicy Timeout( timeoutStrategy, onTimeout); } + + private static void EmptyHandler(Context context, TimeSpan timeout, Task task, Exception exception) + { + // No-op + } } diff --git a/test/Polly.Specs/Caching/AbsoluteTtlSpecs.cs b/test/Polly.Specs/Caching/AbsoluteTtlSpecs.cs index 63c0072be70..7256e7628cb 100644 --- a/test/Polly.Specs/Caching/AbsoluteTtlSpecs.cs +++ b/test/Polly.Specs/Caching/AbsoluteTtlSpecs.cs @@ -53,6 +53,19 @@ public void Should_return_timespan_reflecting_time_until_expiry() actual.SlidingExpiration.ShouldBeFalse(); } + [Fact] + public void Should_return_zero_ttl_if_configured_to_expire_now() + { + SystemClock.DateTimeOffsetUtcNow = () => new(2025, 02, 16, 12, 34, 56, TimeSpan.Zero); + + AbsoluteTtl ttlStrategy = new AbsoluteTtl(SystemClock.DateTimeOffsetUtcNow()); + + var actual = ttlStrategy.GetTtl(new Context("someOperationKey"), null); + + actual.Timespan.ShouldBe(TimeSpan.Zero); + actual.SlidingExpiration.ShouldBeFalse(); + } + public void Dispose() => SystemClock.Reset(); } diff --git a/test/Polly.Specs/Caching/AsyncSerializingCacheProviderSpecs.cs b/test/Polly.Specs/Caching/AsyncSerializingCacheProviderSpecs.cs index c809c59dbec..833c560f713 100644 --- a/test/Polly.Specs/Caching/AsyncSerializingCacheProviderSpecs.cs +++ b/test/Polly.Specs/Caching/AsyncSerializingCacheProviderSpecs.cs @@ -1,5 +1,6 @@ namespace Polly.Specs.Caching; +[Collection(Constants.SystemClockDependentTestCollection)] public class AsyncSerializingCacheProviderSpecs { #region Object-to-TSerialized serializer @@ -425,5 +426,69 @@ public async Task Double_generic_SerializingCacheProvider_from_extension_syntax_ fromCache.ShouldBe(ResultPrimitive.Undefined); } + [Fact] + public void TryGetAsync_for_sync_provider_returns_false_and_default_if_key_not_found() + { + // Arrange + var cacheProvider = new StubCacheProvider(); + var genericCacheProvider = cacheProvider.For(); + + // Act + (var cacheHit, var cacheValue) = genericCacheProvider.TryGet("key"); + + // Assert + cacheHit.ShouldBeFalse(); + cacheValue.ShouldBeNull(); + } + + [Fact] + public async Task TryGetAsync_for_async_provider_returns_false_and_default_if_key_not_found() + { + // Arrange + var cacheProvider = new StubCacheProvider(); + var asyncCacheProvider = cacheProvider.AsyncFor(); + + // Act + (var cacheHit, var cacheValue) = await asyncCacheProvider.TryGetAsync("key", CancellationToken.None, false); + + // Assert + cacheHit.ShouldBeFalse(); + cacheValue.ShouldBeNull(); + } + + [Fact] + public void TryGetAsync_for_sync_provider_returns_true_and_value_if_key_found() + { + // Arrange + var cacheProvider = new StubCacheProvider(); + var genericCacheProvider = cacheProvider.For(); + + genericCacheProvider.Put("key", "value", new Ttl(TimeSpan.MaxValue)); + + // Act + (var cacheHit, var cacheValue) = genericCacheProvider.TryGet("key"); + + // Assert + cacheHit.ShouldBeTrue(); + cacheValue.ShouldBe("value"); + } + + [Fact] + public async Task TryGetAsync_for_async_provider_returns_true_and_value_if_key_found() + { + // Arrange + var cacheProvider = new StubCacheProvider(); + var asyncCacheProvider = cacheProvider.AsyncFor(); + + await asyncCacheProvider.PutAsync("key", "value", new Ttl(TimeSpan.MaxValue), CancellationToken.None, false); + + // Act + (var cacheHit, var cacheValue) = await asyncCacheProvider.TryGetAsync("key", CancellationToken.None, false); + + // Assert + cacheHit.ShouldBeTrue(); + cacheValue.ShouldBe("value"); + } + #endregion } diff --git a/test/Polly.Specs/Caching/CacheAsyncSpecs.cs b/test/Polly.Specs/Caching/CacheAsyncSpecs.cs index 14e7ef0ce4c..6970e93559e 100644 --- a/test/Polly.Specs/Caching/CacheAsyncSpecs.cs +++ b/test/Polly.Specs/Caching/CacheAsyncSpecs.cs @@ -107,6 +107,9 @@ public void Should_throw_when_cache_provider_is_null() action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategyFunc, onCache, onCache, onCache, onCacheError, onCacheError); Should.Throw(action).ParamName.ShouldBe(CacheProviderExpected); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategyFunc, onCache, onCache, onCache, onCacheError, onCacheError); + Should.Throw(action).ParamName.ShouldBe(CacheProviderExpected); } [Fact] diff --git a/test/Polly.Specs/Caching/CacheTResultAsyncSpecs.cs b/test/Polly.Specs/Caching/CacheTResultAsyncSpecs.cs index 61473fe8192..7415a7927db 100644 --- a/test/Polly.Specs/Caching/CacheTResultAsyncSpecs.cs +++ b/test/Polly.Specs/Caching/CacheTResultAsyncSpecs.cs @@ -49,21 +49,226 @@ public void Should_throw_when_action_is_null() .ParamName.ShouldBe("action"); } + [Fact] + public void Should_not_throw_when_arguments_valid() + { + IAsyncCacheProvider cacheProvider = new StubCacheProvider(); + IAsyncCacheProvider genericCacheProvider = cacheProvider.AsyncFor(); + Action action = () => Policy.CacheAsync(cacheProvider, TimeSpan.MaxValue); + Should.NotThrow(action); + + var ttl = TimeSpan.MaxValue; + ITtlStrategy ttlStrategy = Substitute.For(); + Func cacheKeyStrategyFunc = (_) => string.Empty; + var cacheKeyStrategy = Substitute.For(); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategyFunc); + Should.NotThrow(action); + + Action emptyAction = (_, _) => { }; + Action emptyHandler = (_, _, _) => { }; + + action = () => Policy.CacheAsync(cacheProvider, ttl, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttl, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategy, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategy, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategyFunc, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy.For(), cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy.For()); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl, cacheKeyStrategyFunc); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy, cacheKeyStrategyFunc); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy.For(), cacheKeyStrategyFunc); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl, cacheKeyStrategyFunc); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy.For(), cacheKeyStrategyFunc); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl, emptyAction, emptyAction, emptyAction, null, null); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy, emptyAction, emptyAction, emptyAction, null, null); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy.For(), emptyAction, emptyAction, emptyAction, null, null); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl, cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, null, null); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy, cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, null, null); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy.For(), cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, null, null); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl, cacheKeyStrategyFunc, emptyAction, emptyAction, emptyAction, null, null); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl, cacheKeyStrategyFunc, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl, cacheKeyStrategy, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy, cacheKeyStrategy, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy.For(), cacheKeyStrategy, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttl, cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy.For(), cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.NotThrow(action); + } + [Fact] public void Should_throw_when_cache_provider_is_null() { IAsyncCacheProvider cacheProvider = null!; + Action action = () => Policy.CacheAsync(cacheProvider, TimeSpan.MaxValue); Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + var ttl = TimeSpan.MaxValue; + ITtlStrategy ttlStrategy = Substitute.For(); + Func cacheKeyStrategy = (_) => string.Empty; + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategy); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + Action emptyAction = (_, _) => { }; + Action emptyHandler = (_, _, _) => { }; + + action = () => Policy.CacheAsync(cacheProvider, ttl, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttl, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttl, Substitute.For(), emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, Substitute.For(), emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttl, cacheKeyStrategy, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttl, Substitute.For(), emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy, Substitute.For(), emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(null!, ttlStrategy.For(), cacheKeyStrategy, emptyAction, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + + action = () => Policy.CacheAsync(cacheProvider, ttlStrategy); + Should.Throw(action).ParamName.ShouldBe("cacheProvider"); + } + + [Fact] + public void Should_throw_if_delegates_are_null() + { + IAsyncCacheProvider asyncCacheProvider = new StubCacheProvider().AsyncFor(); + ITtlStrategy ttlStrategy = Substitute.For().For(); + Func cacheKeyStrategy = (_) => string.Empty; + + Action emptyAction = (_, _) => { }; + Action emptyHandler = (_, _, _) => { }; + + Action action = () => Policy.CacheAsync(asyncCacheProvider, ttlStrategy, cacheKeyStrategy, null!, emptyAction, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("onCacheGet"); + + action = () => Policy.CacheAsync(asyncCacheProvider, ttlStrategy, cacheKeyStrategy, emptyAction, null!, emptyAction, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("onCacheMiss"); + + action = () => Policy.CacheAsync(asyncCacheProvider, ttlStrategy, cacheKeyStrategy, emptyAction, emptyAction, null!, emptyHandler, emptyHandler); + Should.Throw(action).ParamName.ShouldBe("onCachePut"); } [Fact] public void Should_throw_when_ttl_strategy_is_null() { IAsyncCacheProvider cacheProvider = new StubCacheProvider(); + IAsyncCacheProvider genericCacheProvider = cacheProvider.AsyncFor(); + ITtlStrategy ttlStrategy = null!; Action action = () => Policy.CacheAsync(cacheProvider, ttlStrategy); Should.Throw(action).ParamName.ShouldBe("ttlStrategy"); + + action = () => Policy.CacheAsync(genericCacheProvider, ttlStrategy); + Should.Throw(action).ParamName.ShouldBe("ttlStrategy"); + + ITtlStrategy genericTtlStrategy = null!; + + action = () => Policy.CacheAsync(genericCacheProvider, genericTtlStrategy); + Should.Throw(action).ParamName.ShouldBe("ttlStrategy"); } [Fact] @@ -157,6 +362,28 @@ public void Should_throw_when_cache_key_strategy_is_null() onCacheGetError, onCachePutError); Should.Throw(action).ParamName.ShouldBe(CacheKeyStrategyExpected); + + action = () => Policy.CacheAsync( + Substitute.For(), + ttl, + cacheKeyStrategy, + onCacheGet, + onCacheMiss, + onCachePut, + onCacheGetError, + onCachePutError); + Should.Throw(action).ParamName.ShouldBe(CacheKeyStrategyExpected); + + action = () => Policy.CacheAsync( + Substitute.For(), + ttlStrategy, + cacheKeyStrategy, + onCacheGet, + onCacheMiss, + onCachePut, + onCacheGetError, + onCachePutError); + Should.Throw(action).ParamName.ShouldBe(CacheKeyStrategyExpected); } #endregion diff --git a/test/Polly.Specs/CircuitBreaker/AdvancedCircuitBreakerAsyncSpecs.cs b/test/Polly.Specs/CircuitBreaker/AdvancedCircuitBreakerAsyncSpecs.cs index f7ef1f78ceb..3a0e07ebe75 100644 --- a/test/Polly.Specs/CircuitBreaker/AdvancedCircuitBreakerAsyncSpecs.cs +++ b/test/Polly.Specs/CircuitBreaker/AdvancedCircuitBreakerAsyncSpecs.cs @@ -1,4 +1,5 @@ -using Scenario = Polly.Specs.Helpers.PolicyExtensionsAsync.ExceptionAndOrCancellationScenario; +using System.Globalization; +using Scenario = Polly.Specs.Helpers.PolicyExtensionsAsync.ExceptionAndOrCancellationScenario; namespace Polly.Specs.CircuitBreaker; @@ -2963,6 +2964,155 @@ public async Task Should_honour_and_report_cancellation_during_func_execution() attemptsInvoked.ShouldBe(1); } + [Theory] + [InlineData(-0.1)] + [InlineData(0)] + [InlineData(1.1)] + public void Should_throw_if_failure_threshold_is_outside_allowed_range(double failureThreshold) + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreakerAsync( + failureThreshold, + TimeSpan.FromMinutes(1), + 60, + TimeSpan.FromMinutes(1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.Throw(action) + .ParamName.ShouldBe("failureThreshold"); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + public void Should_throw_if_minimum_throughput_is_outside_allowed_range(int minimumThroughput) + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreakerAsync( + 1, + TimeSpan.FromMinutes(1), + minimumThroughput, + TimeSpan.FromMinutes(1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.Throw(action) + .ParamName.ShouldBe("minimumThroughput"); + } + + [Fact] + public void Should_throw_if_sampling_duration_is_negative() + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreakerAsync( + 1, + TimeSpan.FromSeconds(-1), + 60, + TimeSpan.FromSeconds(1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.Throw(action) + .ParamName.ShouldBe("samplingDuration"); + } + + [Fact] + public void Should_not_throw_if_sampling_duration_is_minimum() + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreakerAsync( + 1, + TimeSpan.FromTicks(AdvancedCircuitController.ResolutionOfCircuitTimer), + 60, + TimeSpan.FromSeconds(1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.NotThrow(action); + } + + [Fact] + public void Should_throw_if_duration_of_break_is_negative() + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreakerAsync( + 1, + TimeSpan.FromMinutes(1), + 60, + TimeSpan.FromSeconds(-1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.Throw(action) + .ParamName.ShouldBe("durationOfBreak"); + } + + [Fact] + public void Should_not_throw_if_duration_of_break_is_zero() + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreakerAsync( + 1, + TimeSpan.FromMinutes(1), + 60, + TimeSpan.Zero, + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.NotThrow(action); + } + + [Fact] + public void Should_throw_if_delegates_are_null() + { + double failureThreshold = 1; + var samplingDuration = TimeSpan.FromMinutes(1); + int mimimumThroughput = 60; + var breakDuration = TimeSpan.FromSeconds(1); + var builder = Policy.HandleResult(ResultPrimitive.Fault); + + Action action = () => builder.AdvancedCircuitBreakerAsync(failureThreshold, samplingDuration, mimimumThroughput, breakDuration, (null as Action, CircuitState, TimeSpan, Context>)!, (_) => { }, () => { }); + Should.Throw(action).ParamName.ShouldBe("onBreak"); + + action = () => builder.AdvancedCircuitBreakerAsync(failureThreshold, samplingDuration, mimimumThroughput, breakDuration, (_, _, _, _) => { }, null!, () => { }); + Should.Throw(action).ParamName.ShouldBe("onReset"); + + action = () => builder.AdvancedCircuitBreakerAsync(failureThreshold, samplingDuration, mimimumThroughput, breakDuration, (_, _, _, _) => { }, (_) => { }, null!); + Should.Throw(action).ParamName.ShouldBe("onHalfOpen"); + } + + [Theory] + [InlineData("00:00:00.001", typeof(SingleHealthMetrics))] + [InlineData("00:00:00.199", typeof(SingleHealthMetrics))] + [InlineData("00:00:00.200", typeof(RollingHealthMetrics))] + [InlineData("00:00:00.201", typeof(RollingHealthMetrics))] + public void AdvancedCircuitController_uses_correct_metrics(string samplingDuration, Type expectedMetricsType) + { + var target = new AdvancedCircuitController( + failureThreshold: 0.5, + samplingDuration: TimeSpan.Parse(samplingDuration, CultureInfo.InvariantCulture), + minimumThroughput: 4, + durationOfBreak: TimeSpan.FromSeconds(30), + onBreak: (_, _, _, _) => { }, + onReset: _ => { }, + onHalfOpen: () => { }); + + target.Metrics.ShouldBeOfType(expectedMetricsType); + } + #endregion public void Dispose() => diff --git a/test/Polly.Specs/CircuitBreaker/AdvancedCircuitBreakerSpecs.cs b/test/Polly.Specs/CircuitBreaker/AdvancedCircuitBreakerSpecs.cs index 7ae329277f7..91dd643c0b0 100644 --- a/test/Polly.Specs/CircuitBreaker/AdvancedCircuitBreakerSpecs.cs +++ b/test/Polly.Specs/CircuitBreaker/AdvancedCircuitBreakerSpecs.cs @@ -2800,6 +2800,136 @@ public void Should_honour_and_report_cancellation_during_func_execution() attemptsInvoked.ShouldBe(1); } + [Theory] + [InlineData(-0.1)] + [InlineData(0)] + [InlineData(1.1)] + public void Should_throw_if_failure_threshold_is_less_than_one(double failureThreshold) + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreaker( + failureThreshold, + TimeSpan.FromMinutes(1), + 60, + TimeSpan.FromMinutes(1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.Throw(action) + .ParamName.ShouldBe("failureThreshold"); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + public void Should_throw_if_minimum_throughput_is_outside_allowed_range(int minimumThroughput) + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreaker( + 1, + TimeSpan.FromMinutes(1), + minimumThroughput, + TimeSpan.FromMinutes(1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.Throw(action) + .ParamName.ShouldBe("minimumThroughput"); + } + + [Fact] + public void Should_throw_if_sampling_duration_is_negative() + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreaker( + 1, + TimeSpan.FromSeconds(-1), + 60, + TimeSpan.FromSeconds(1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.Throw(action) + .ParamName.ShouldBe("samplingDuration"); + } + + [Fact] + public void Should_not_throw_if_sampling_duration_is_minimum() + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreaker( + 1, + TimeSpan.FromTicks(AdvancedCircuitController.ResolutionOfCircuitTimer), + 60, + TimeSpan.FromSeconds(1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.NotThrow(action); + } + + [Fact] + public void Should_throw_if_duration_of_break_is_negative() + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreaker( + 1, + TimeSpan.FromMinutes(1), + 60, + TimeSpan.FromSeconds(-1), + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.Throw(action) + .ParamName.ShouldBe("durationOfBreak"); + } + + [Fact] + public void Should_not_throw_if_duration_of_break_is_zero() + { + Action action = () => Policy + .HandleResult(ResultPrimitive.Fault) + .AdvancedCircuitBreaker( + 1, + TimeSpan.FromMinutes(1), + 60, + TimeSpan.Zero, + (_, _, _, _) => { }, + (_) => { }, + () => { }); + + Should.NotThrow(action); + } + + [Fact] + public void Should_throw_if_delegates_are_null() + { + double failureThreshold = 1; + var samplingDuration = TimeSpan.FromMinutes(1); + int mimimumThroughput = 60; + var breakDuration = TimeSpan.FromSeconds(1); + var builder = Policy.HandleResult(ResultPrimitive.Fault); + + Action action = () => builder.AdvancedCircuitBreaker(failureThreshold, samplingDuration, mimimumThroughput, breakDuration, (null as Action, CircuitState, TimeSpan, Context>)!, (_) => { }, () => { }); + Should.Throw(action).ParamName.ShouldBe("onBreak"); + + action = () => builder.AdvancedCircuitBreaker(failureThreshold, samplingDuration, mimimumThroughput, breakDuration, (_, _, _, _) => { }, null!, () => { }); + Should.Throw(action).ParamName.ShouldBe("onReset"); + + action = () => builder.AdvancedCircuitBreaker(failureThreshold, samplingDuration, mimimumThroughput, breakDuration, (_, _, _, _) => { }, (_) => { }, null!); + Should.Throw(action).ParamName.ShouldBe("onHalfOpen"); + } + #endregion public void Dispose() => diff --git a/test/Polly.Specs/CircuitBreaker/RollingHealthMetricsTests.cs b/test/Polly.Specs/CircuitBreaker/RollingHealthMetricsTests.cs new file mode 100644 index 00000000000..518c97cf970 --- /dev/null +++ b/test/Polly.Specs/CircuitBreaker/RollingHealthMetricsTests.cs @@ -0,0 +1,126 @@ +namespace Polly.Specs.CircuitBreaker; + +[Collection(Constants.SystemClockDependentTestCollection)] +public class RollingHealthMetricsTests : IDisposable +{ + private readonly TimeSpan _samplingDuration = TimeSpan.FromSeconds(10); + private DateTime _utcNow = new(2025, 02, 16, 12, 34, 56, DateTimeKind.Utc); + + public RollingHealthMetricsTests() => SystemClock.UtcNow = () => _utcNow; + + [Fact] + public void Ctor_EnsureDefaults() + { + var metrics = Create(); + var health = metrics.GetHealthCount_NeedsLock(); + health.Successes.ShouldBe(0); + health.Failures.ShouldBe(0); + } + + [Fact] + public void Increment_Ok() + { + var metrics = Create(); + + metrics.IncrementFailure_NeedsLock(); + metrics.IncrementSuccess_NeedsLock(); + metrics.IncrementSuccess_NeedsLock(); + metrics.IncrementSuccess_NeedsLock(); + metrics.IncrementSuccess_NeedsLock(); + + var health = metrics.GetHealthCount_NeedsLock(); + + health.Failures.ShouldBe(1); + health.Successes.ShouldBe(4); + } + + [Fact] + public void GetHealthCount_NeedsLock_EnsureWindowRespected() + { + var metrics = Create(); + var health = new List(); + + var startedAt = _utcNow; + + for (int i = 0; i < 5; i++) + { + if (i < 2) + { + metrics.IncrementFailure_NeedsLock(); + } + else + { + metrics.IncrementSuccess_NeedsLock(); + } + + metrics.IncrementSuccess_NeedsLock(); + _utcNow += TimeSpan.FromSeconds(2); + health.Add(metrics.GetHealthCount_NeedsLock()); + } + + _utcNow += TimeSpan.FromSeconds(2); + health.Add(metrics.GetHealthCount_NeedsLock()); + + health[0].ShouldBeEquivalentTo(new HealthCount { Successes = 1, Failures = 1, StartedAt = startedAt.AddSeconds(0).Ticks }); + health[1].ShouldBeEquivalentTo(new HealthCount { Successes = 2, Failures = 2, StartedAt = startedAt.AddSeconds(0).Ticks }); + health[2].ShouldBeEquivalentTo(new HealthCount { Successes = 4, Failures = 2, StartedAt = startedAt.AddSeconds(0).Ticks }); + health[3].ShouldBeEquivalentTo(new HealthCount { Successes = 6, Failures = 2, StartedAt = startedAt.AddSeconds(0).Ticks }); + health[4].ShouldBeEquivalentTo(new HealthCount { Successes = 7, Failures = 1, StartedAt = startedAt.AddSeconds(2).Ticks }); + health[5].ShouldBeEquivalentTo(new HealthCount { Successes = 6, Failures = 0, StartedAt = startedAt.AddSeconds(4).Ticks }); + } + + [Fact] + public void GetHealthCount_NeedsLock_EnsureWindowCapacityRespected() + { + var delay = TimeSpan.FromSeconds(1); + var metrics = Create(); + + for (int i = 0; i < 10; i++) + { + metrics.IncrementSuccess_NeedsLock(); + _utcNow += delay; + } + + metrics.GetHealthCount_NeedsLock().Successes.ShouldBe(9); + _utcNow += delay; + metrics.GetHealthCount_NeedsLock().Successes.ShouldBe(8); + } + + [Fact] + public void Reset_Ok() + { + var metrics = Create(); + + metrics.IncrementSuccess_NeedsLock(); + metrics.Reset_NeedsLock(); + + _utcNow += _samplingDuration; + _utcNow += _samplingDuration; + + metrics.GetHealthCount_NeedsLock().Successes.ShouldBe(0); + + _utcNow += _samplingDuration; + _utcNow += _samplingDuration; + + metrics.GetHealthCount_NeedsLock().Successes.ShouldBe(0); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public void GetHealthCount_NeedsLock_SamplingDurationRespected(bool variance) + { + var metrics = Create(); + + metrics.IncrementSuccess_NeedsLock(); + metrics.IncrementSuccess_NeedsLock(); + + _utcNow += _samplingDuration + (variance ? TimeSpan.FromMilliseconds(1) : TimeSpan.Zero); + + metrics.GetHealthCount_NeedsLock().ShouldBeEquivalentTo(new HealthCount { StartedAt = _utcNow.Ticks }); + } + + private RollingHealthMetrics Create() => new(_samplingDuration); + + public void Dispose() => SystemClock.Reset(); +} diff --git a/test/Polly.Specs/Fallback/FallbackAsyncSpecs.cs b/test/Polly.Specs/Fallback/FallbackAsyncSpecs.cs index a93061cd200..3fad46ce4d2 100644 --- a/test/Polly.Specs/Fallback/FallbackAsyncSpecs.cs +++ b/test/Polly.Specs/Fallback/FallbackAsyncSpecs.cs @@ -59,6 +59,13 @@ public void Should_throw_when_onFallback_delegate_is_null() Should.Throw(policy) .ParamName.ShouldBe("onFallbackAsync"); + + policy = () => Policy + .Handle() + .FallbackAsync(fallbackActionAsync, onFallbackAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onFallbackAsync"); } [Fact] diff --git a/test/Polly.Specs/Fallback/FallbackSpecs.cs b/test/Polly.Specs/Fallback/FallbackSpecs.cs index 2d323932d97..614d02b9f05 100644 --- a/test/Polly.Specs/Fallback/FallbackSpecs.cs +++ b/test/Polly.Specs/Fallback/FallbackSpecs.cs @@ -6,6 +6,22 @@ public class FallbackSpecs { #region Configuration guard condition tests + [Fact] + public void Should_not_throw_when_fallback_action_is__not_null() + { + Action policy = () => Policy + .Handle() + .Fallback(() => { }); + + Should.NotThrow(policy); + + policy = () => Policy + .Handle() + .Fallback((_) => { }); + + Should.NotThrow(policy); + } + [Fact] public void Should_throw_when_fallback_action_is_null() { @@ -17,6 +33,15 @@ public void Should_throw_when_fallback_action_is_null() Should.Throw(policy) .ParamName.ShouldBe("fallbackAction"); + + Action fallbackActionToken = null!; + + policy = () => Policy + .Handle() + .Fallback(fallbackActionToken); + + Should.Throw(policy) + .ParamName.ShouldBe("fallbackAction"); } [Fact] diff --git a/test/Polly.Specs/Fallback/FallbackTResultAsyncSpecs.cs b/test/Polly.Specs/Fallback/FallbackTResultAsyncSpecs.cs index 622b7af2897..ec823134f8a 100644 --- a/test/Polly.Specs/Fallback/FallbackTResultAsyncSpecs.cs +++ b/test/Polly.Specs/Fallback/FallbackTResultAsyncSpecs.cs @@ -86,6 +86,13 @@ public void Should_throw_when_onFallback_delegate_is_null() Should.Throw(policy) .ParamName.ShouldBe("onFallbackAsync"); + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .FallbackAsync(ResultPrimitive.Substitute, onFallbackAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onFallbackAsync"); } [Fact] @@ -100,6 +107,29 @@ public void Should_throw_when_onFallback_delegate_is_null_with_context() Should.Throw(policy) .ParamName.ShouldBe("onFallbackAsync"); + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .FallbackAsync(ResultPrimitive.Substitute, onFallbackAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onFallbackAsync"); + } + + [Fact] + public void Should_not_throw_when_onFallback_delegate_is_not_null() + { + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .FallbackAsync((_, _) => Task.FromResult(ResultPrimitive.Substitute), (_, _) => TaskHelper.EmptyTask); + + Should.NotThrow(policy); + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .FallbackAsync(ResultPrimitive.Substitute, (_, _) => TaskHelper.EmptyTask); + + Should.NotThrow(policy); } #endregion diff --git a/test/Polly.Specs/Fallback/FallbackTResultSpecs.cs b/test/Polly.Specs/Fallback/FallbackTResultSpecs.cs index 281677f8663..ea4927349b4 100644 --- a/test/Polly.Specs/Fallback/FallbackTResultSpecs.cs +++ b/test/Polly.Specs/Fallback/FallbackTResultSpecs.cs @@ -44,6 +44,31 @@ public void Should_throw_when_fallback_action_is_null() Should.Throw(policy) .ParamName.ShouldBe("fallbackAction"); + + Func fallbackActionToken = null!; + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .Fallback(fallbackActionToken); + + Should.Throw(policy) + .ParamName.ShouldBe("fallbackAction"); + } + + [Fact] + public void Should_not_throw_when_fallback_action_is_not_null() + { + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .Fallback(() => ResultPrimitive.Substitute); + + Should.NotThrow(policy); + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .Fallback((_) => ResultPrimitive.Substitute); + + Should.NotThrow(policy); } [Fact] @@ -127,6 +152,13 @@ public void Should_throw_when_onFallback_delegate_is_null() Should.Throw(policy) .ParamName.ShouldBe("onFallback"); + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .Fallback(ResultPrimitive.Substitute, onFallback); + + Should.Throw(policy) + .ParamName.ShouldBe("onFallback"); } [Fact] @@ -785,6 +817,30 @@ public void Should_handle_cancellation_and_execute_fallback_during_otherwise_non fallbackActionExecuted.ShouldBeTrue(); } + [Fact] + public void Should_handle_exception_correctly_and_return_fallback() + { + var policy = Policy + .HandleResult(ResultPrimitive.Fault) + .Or() + .Fallback(ResultPrimitive.Substitute); + + var result = policy.Execute(() => throw new IOException()); + + result.ShouldBe(ResultPrimitive.Substitute); + } + + [Fact] + public void Should_not_handle_wrong_exception() + { + var policy = Policy + .HandleResult(ResultPrimitive.Fault) + .Or() + .Fallback(ResultPrimitive.Substitute); + + Should.Throw(() => policy.Execute(() => throw new InvalidOperationException())); + } + [Fact] public void Should_not_report_cancellation_and_not_execute_fallback_if_non_faulting_action_execution_completes_and_user_delegate_does_not_observe_the_set_cancellationToken() { diff --git a/test/Polly.Specs/Polly.Specs.csproj b/test/Polly.Specs/Polly.Specs.csproj index 057180192ef..4e9ee7cf1bd 100644 --- a/test/Polly.Specs/Polly.Specs.csproj +++ b/test/Polly.Specs/Polly.Specs.csproj @@ -5,7 +5,7 @@ $(TargetFrameworks);net481 enable Test - 80,70,80 + 85,80,85 [Polly]* true diff --git a/test/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs b/test/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs index ac5bac14a6d..140c02bb80e 100644 --- a/test/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs +++ b/test/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs @@ -37,7 +37,7 @@ public void Should_throw_when_action_is_null() { var flags = BindingFlags.NonPublic | BindingFlags.Instance; Func> action = null!; - IRateLimiter rateLimiter = RateLimiterFactory.Create(TimeSpan.FromSeconds(1), 1); + IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(TimeSpan.FromSeconds(1), 1); var instance = Activator.CreateInstance( typeof(AsyncRateLimitPolicy), @@ -65,6 +65,7 @@ public void Should_throw_when_pertimespan_is_negative() var exception = Should.Throw(() => Policy.RateLimitAsync(1, TimeSpan.FromSeconds(-1), 1)); // Assert + exception.Message.ShouldStartWith("perTimeSpan must be a positive timespan"); exception.ParamName.ShouldBe("perTimeSpan"); exception.ActualValue.ShouldBe(TimeSpan.FromSeconds(-1)); } @@ -76,7 +77,15 @@ public void Should_throw_when_pertimespan_is_zero() var exception = Should.Throw(() => Policy.RateLimitAsync(1, TimeSpan.Zero, 1)); // Assert + exception.Message.ShouldStartWith("perTimeSpan must be a positive timespan"); exception.ParamName.ShouldBe("perTimeSpan"); exception.ActualValue.ShouldBe(TimeSpan.Zero); } + + [Fact] + public void Should_not_throw_when_pertimespan_is_positive() + { + // Act and Assert + Should.NotThrow(() => Policy.RateLimitAsync(1, TimeSpan.FromTicks(1), 1)); + } } diff --git a/test/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs b/test/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs index 3a3f34e0c97..027324e3555 100644 --- a/test/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs +++ b/test/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs @@ -53,7 +53,7 @@ public void Should_throw_when_action_is_null() { var flags = BindingFlags.NonPublic | BindingFlags.Instance; Func> action = null!; - IRateLimiter rateLimiter = RateLimiterFactory.Create(TimeSpan.FromSeconds(1), 1); + IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(TimeSpan.FromSeconds(1), 1); Func? retryAfterFactory = null!; var instance = Activator.CreateInstance( @@ -81,6 +81,7 @@ public void Should_throw_when_pertimespan_is_negative() var exception = Should.Throw(() => Policy.RateLimitAsync(1, TimeSpan.FromSeconds(-1), 1)); // Assert + exception.Message.ShouldStartWith("perTimeSpan must be a positive timespan"); exception.ParamName.ShouldBe("perTimeSpan"); exception.ActualValue.ShouldBe(TimeSpan.FromSeconds(-1)); } @@ -92,7 +93,15 @@ public void Should_throw_when_pertimespan_is_zero() var exception = Should.Throw(() => Policy.RateLimitAsync(1, TimeSpan.Zero, 1)); // Assert + exception.Message.ShouldStartWith("perTimeSpan must be a positive timespan"); exception.ParamName.ShouldBe("perTimeSpan"); exception.ActualValue.ShouldBe(TimeSpan.Zero); } + + [Fact] + public void Should_not_throw_when_pertimespan_is_greater_than_zero() + { + // Act and Assert + Should.NotThrow(() => Policy.RateLimitAsync(1, TimeSpan.FromTicks(1), 1)); + } } diff --git a/test/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs b/test/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs index f60f8955ac3..2fadb2ef93c 100644 --- a/test/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs +++ b/test/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs @@ -37,7 +37,7 @@ public void Should_throw_when_action_is_null() { var flags = BindingFlags.NonPublic | BindingFlags.Instance; Func action = null!; - IRateLimiter rateLimiter = RateLimiterFactory.Create(TimeSpan.FromSeconds(1), 1); + IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(TimeSpan.FromSeconds(1), 1); var instance = Activator.CreateInstance( typeof(RateLimitPolicy), @@ -65,6 +65,7 @@ public void Should_throw_when_pertimespan_is_negative() var exception = Should.Throw(() => Policy.RateLimit(1, TimeSpan.FromSeconds(-1), 1)); // Assert + exception.Message.ShouldStartWith("perTimeSpan must be a positive timespan"); exception.ParamName.ShouldBe("perTimeSpan"); exception.ActualValue.ShouldBe(TimeSpan.FromSeconds(-1)); } @@ -76,7 +77,15 @@ public void Should_throw_when_pertimespan_is_zero() var exception = Should.Throw(() => Policy.RateLimit(1, TimeSpan.Zero, 1)); // Assert + exception.Message.ShouldStartWith("perTimeSpan must be a positive timespan"); exception.ParamName.ShouldBe("perTimeSpan"); exception.ActualValue.ShouldBe(TimeSpan.Zero); } + + [Fact] + public void Should_not_throw_when_pertimespan_is_positive() + { + // Act and Assert + Should.NotThrow(() => Policy.RateLimit(1, TimeSpan.FromTicks(1), 1)); + } } diff --git a/test/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs b/test/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs index c0a7997824a..c8468d459c4 100644 --- a/test/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs +++ b/test/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs @@ -53,7 +53,7 @@ public void Should_throw_when_action_is_null() { var flags = BindingFlags.NonPublic | BindingFlags.Instance; Func action = null!; - IRateLimiter rateLimiter = RateLimiterFactory.Create(TimeSpan.FromSeconds(1), 1); + IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(TimeSpan.FromSeconds(1), 1); Func? retryAfterFactory = null; var instance = Activator.CreateInstance( @@ -81,6 +81,7 @@ public void Should_throw_when_pertimespan_is_negative() var exception = Should.Throw(() => Policy.RateLimit(1, TimeSpan.FromSeconds(-1), 1)); // Assert + exception.Message.ShouldStartWith("perTimeSpan must be a positive timespan"); exception.ParamName.ShouldBe("perTimeSpan"); exception.ActualValue.ShouldBe(TimeSpan.FromSeconds(-1)); } @@ -92,7 +93,15 @@ public void Should_throw_when_pertimespan_is_zero() var exception = Should.Throw(() => Policy.RateLimit(1, TimeSpan.Zero, 1)); // Assert + exception.Message.ShouldStartWith("perTimeSpan must be a positive timespan"); exception.ParamName.ShouldBe("perTimeSpan"); exception.ActualValue.ShouldBe(TimeSpan.Zero); } + + [Fact] + public void Should_not_throw_when_pertimespan_is_greater_than_zero() + { + // Act and Assert + Should.NotThrow(() => Policy.RateLimit(1, TimeSpan.FromTicks(1), 1)); + } } diff --git a/test/Polly.Specs/RateLimit/RateLimitRejectedExceptionTests.cs b/test/Polly.Specs/RateLimit/RateLimitRejectedExceptionTests.cs index 80855cb598f..a8bee7d4a37 100644 --- a/test/Polly.Specs/RateLimit/RateLimitRejectedExceptionTests.cs +++ b/test/Polly.Specs/RateLimit/RateLimitRejectedExceptionTests.cs @@ -27,6 +27,10 @@ public void Ctor_Ok() rate.RetryAfter.ShouldBe(retryAfter); rate.Message.ShouldBe(Dummy); + rate = new RateLimitRejectedException(TimeSpan.Zero, Dummy); + rate.RetryAfter.ShouldBe(TimeSpan.Zero); + rate.Message.ShouldBe(Dummy); + rate = new RateLimitRejectedException(retryAfter, Dummy, exception); rate.RetryAfter.ShouldBe(retryAfter); rate.Message.ShouldBe(Dummy); diff --git a/test/Polly.Specs/Registry/PolicyRegistrySpecs.cs b/test/Polly.Specs/Registry/PolicyRegistrySpecs.cs index f662f5e3971..434f2d02d72 100644 --- a/test/Polly.Specs/Registry/PolicyRegistrySpecs.cs +++ b/test/Polly.Specs/Registry/PolicyRegistrySpecs.cs @@ -163,7 +163,6 @@ public void Should_be_able_to_retrieve_stored_Policy_using_TryGet() [Fact] public void Should_return_false_if_policy_does_not_exist_TryGet() { - var policy = Policy.NoOp(); string key = Guid.NewGuid().ToString(); _registry.TryGet(key, out var outPolicy).ShouldBeFalse(); diff --git a/test/Polly.Specs/ResiliencePipelineConversionExtensionsTests.cs b/test/Polly.Specs/ResiliencePipelineConversionExtensionsTests.cs index d703f159414..4191c8a07f9 100644 --- a/test/Polly.Specs/ResiliencePipelineConversionExtensionsTests.cs +++ b/test/Polly.Specs/ResiliencePipelineConversionExtensionsTests.cs @@ -15,6 +15,7 @@ public class ResiliencePipelineConversionExtensionsTests private readonly ResiliencePipeline _genericStrategy; private bool _isSynchronous; private bool _isVoid; + private bool _continueOnCapturedContext = true; public ResiliencePipelineConversionExtensionsTests() { @@ -27,6 +28,7 @@ public ResiliencePipelineConversionExtensionsTests() context.Properties.Set(Outgoing, "outgoing-value"); context.Properties.GetValue(Incoming, string.Empty).ShouldBe("incoming-value"); context.OperationKey.ShouldBe("op-key"); + context.ContinueOnCapturedContext.ShouldBe(_continueOnCapturedContext); } }; @@ -114,6 +116,8 @@ public async Task AsAsyncPolicy_Ok() { _isVoid = true; _isSynchronous = false; + _continueOnCapturedContext = false; + var context = new Context("op-key") { [Incoming.Key] = "incoming-value" @@ -134,6 +138,8 @@ public async Task AsAsyncPolicy_Generic_Ok() { _isVoid = false; _isSynchronous = false; + _continueOnCapturedContext = false; + var context = new Context("op-key") { [Incoming.Key] = "incoming-value" @@ -154,6 +160,8 @@ public async Task AsAsyncPolicy_Result_Ok() { _isVoid = false; _isSynchronous = false; + _continueOnCapturedContext = false; + var context = new Context("op-key") { [Incoming.Key] = "incoming-value" diff --git a/test/Polly.Specs/Retry/RetryForeverAsyncSpecs.cs b/test/Polly.Specs/Retry/RetryForeverAsyncSpecs.cs index 6a228e04cbc..2d70f969573 100644 --- a/test/Polly.Specs/Retry/RetryForeverAsyncSpecs.cs +++ b/test/Polly.Specs/Retry/RetryForeverAsyncSpecs.cs @@ -180,6 +180,19 @@ public void Should_throw_when_onretry_with_int_and_context_is_null() .ParamName.ShouldBe("onRetry"); } + [Fact] + public void Should_throw_when_onretry_with_exception_and_int_is_null() + { + Func onRetryAsync = null!; + + Action policy = () => Policy + .Handle() + .RetryForeverAsync(onRetryAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetryAsync"); + } + [Fact] public void Should_throw_when_onretryasync_with_context_is_null() { diff --git a/test/Polly.Specs/Retry/RetryForeverSpecs.cs b/test/Polly.Specs/Retry/RetryForeverSpecs.cs index aa1b5dbe54a..d8bd7a61330 100644 --- a/test/Polly.Specs/Retry/RetryForeverSpecs.cs +++ b/test/Polly.Specs/Retry/RetryForeverSpecs.cs @@ -28,6 +28,69 @@ public void Should_throw_when_onretry_action_with_context_is_null() .ParamName.ShouldBe("onRetry"); } + [Fact] + public void Should_throw_when_onretry_action_is_null() + { + Action> nullOnRetry = null!; + + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .RetryForever(nullOnRetry); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + } + + [Fact] + public void Should_throw_when_onretry_action_with_int_is_null() + { + Action, int> nullOnRetry = null!; + + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .RetryForever(nullOnRetry); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + } + + [Fact] + public void Should_throw_when_onretry_for_result_action_with_context_is_null() + { + Action, Context> nullOnRetry = null!; + + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .RetryForever(nullOnRetry); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + } + + [Fact] + public void Should_not_throw_when_onretry_action_with_context_is_valid() + { + Action, Context> onRetry = (_, _) => { }; + + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .RetryForever(onRetry); + + Should.NotThrow(policy); + } + + [Fact] + public void Should_not_throw_when_onretry_action_with_int_is_valid() + { + Action, int> onRetry = (_, _) => { }; + + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .RetryForever(onRetry); + + Should.NotThrow(policy); + } + [Fact] public void Should_not_throw_regardless_of_how_many_times_the_specified_exception_is_raised() { diff --git a/test/Polly.Specs/Retry/RetryTResultMixedResultExceptionSpecs.cs b/test/Polly.Specs/Retry/RetryTResultMixedResultExceptionSpecs.cs index a8f2fce0430..bed67e83d65 100644 --- a/test/Polly.Specs/Retry/RetryTResultMixedResultExceptionSpecs.cs +++ b/test/Polly.Specs/Retry/RetryTResultMixedResultExceptionSpecs.cs @@ -150,6 +150,17 @@ public void Should_throw_if_not_one_of_results_or_exceptions_handled() Should.Throw(() => policy.RaiseResultAndOrExceptionSequence(new ArgumentException(), ResultPrimitive.Good)); } + [Fact] + public void Should_handle_default_value_for_enum() + { + Policy policy = Policy + .Handle() + .OrResult(ResultPrimitive.Fault) + .Retry(2); + + policy.RaiseResultSequence(ResultPrimitive.Fault, ResultPrimitive.Good).ShouldBe(ResultPrimitive.Good); + } + [Fact] public void Should_handle_both_exceptions_and_specified_results_with_predicates() { diff --git a/test/Polly.Specs/Retry/WaitAndRetryAsyncSpecs.cs b/test/Polly.Specs/Retry/WaitAndRetryAsyncSpecs.cs index 00666252d4c..fdba78adb7e 100644 --- a/test/Polly.Specs/Retry/WaitAndRetryAsyncSpecs.cs +++ b/test/Polly.Specs/Retry/WaitAndRetryAsyncSpecs.cs @@ -519,6 +519,29 @@ public void Should_throw_when_retry_count_is_less_than_zero_without_context() Should.Throw(policy) .ParamName.ShouldBe("retryCount"); + + policy = () => Policy + .Handle() + .WaitAndRetryAsync(-1, (_, _, _) => TimeSpan.Zero, (_, _, _, _) => TaskHelper.EmptyTask); + + Should.Throw(policy) + .ParamName.ShouldBe("retryCount"); + } + + [Fact] + public void Should_not_throw_when_retry_count_is_zero_without_context() + { + Action policy = () => Policy + .Handle() + .WaitAndRetryAsync(0, (_, _, _) => TimeSpan.Zero, (_, _, _, _) => TaskHelper.EmptyTask); + + Should.NotThrow(policy); + + policy = () => Policy + .Handle() + .WaitAndRetryAsync(0, (_, _, _) => TimeSpan.Zero, (_, _, _, _) => TaskHelper.EmptyTask); + + Should.NotThrow(policy); } [Fact] @@ -547,6 +570,66 @@ public void Should_throw_when_onretry_action_is_null_without_context_when_using_ .ParamName.ShouldBe("onRetry"); } + [Fact] + public void Should_throw_when_onretryasync_action_is_null_without_context() + { + Func onRetryWithContextAsync = null!; + + Action policy = () => Policy + .Handle() + .WaitAndRetryAsync(1, _ => TimeSpan.Zero, onRetryWithContextAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetryAsync"); + + Func onRetryAsync = null!; + + policy = () => Policy + .Handle() + .WaitAndRetryAsync([], onRetryAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetryAsync"); + + Func onRetryContextAsync = null!; + + policy = () => Policy + .Handle() + .WaitAndRetryAsync([], onRetryContextAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetryAsync"); + } + + [Fact] + public void Should_not_throw_when_onretryasync_action_has_context() + { + Action policy = () => Policy + .Handle() + .WaitAndRetryAsync([], (_, _) => TaskHelper.EmptyTask); + + Should.NotThrow(policy); + + policy = () => Policy + .Handle() + .WaitAndRetryAsync([], (_, _, _) => TaskHelper.EmptyTask); + + Should.NotThrow(policy); + } + + [Fact] + public void Should_throw_when_onretryasync_action_is_null_with_context() + { + Func onRetryAsync = null!; + + Action policy = () => Policy + .Handle() + .WaitAndRetryAsync(1, (_, _) => TimeSpan.Zero, onRetryAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetryAsync"); + } + [Fact] public async Task Should_calculate_retry_timespans_from_current_retry_attempt_and_timespan_provider() { diff --git a/test/Polly.Specs/Retry/WaitAndRetryForeverAsyncSpecs.cs b/test/Polly.Specs/Retry/WaitAndRetryForeverAsyncSpecs.cs index fbb95e1998d..bb314af3755 100644 --- a/test/Polly.Specs/Retry/WaitAndRetryForeverAsyncSpecs.cs +++ b/test/Polly.Specs/Retry/WaitAndRetryForeverAsyncSpecs.cs @@ -10,8 +10,40 @@ public class WaitAndRetryForeverAsyncSpecs : IDisposable [Fact] public void Should_throw_when_sleep_duration_provider_is_null_without_context() { + Func sleepDurationProviderContext = null!; + + Action policy = () => Policy + .Handle() + .WaitAndRetryForeverAsync(sleepDurationProviderContext); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + + Func sleepDurationProvider = null!; Action onRetry = (_, _) => { }; + policy = () => Policy + .Handle() + .WaitAndRetryForeverAsync(sleepDurationProvider, onRetry); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + + Func onRetryAsync = (_, _, _) => TaskHelper.EmptyTask; + + policy = () => Policy + .Handle() + .WaitAndRetryForeverAsync(sleepDurationProvider, onRetryAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + } + + [Fact] + public void Should_throw_when_sleep_duration_provider_is_null_with_exception_and_context() + { + Action onRetry = (_, _, _) => { }; + Action policy = () => Policy .Handle() .WaitAndRetryForeverAsync(null, onRetry); @@ -21,13 +53,13 @@ public void Should_throw_when_sleep_duration_provider_is_null_without_context() } [Fact] - public void Should_throw_when_sleep_duration_provider_is_null_with_context() + public void Should_throw_when_sleep_duration_provider_is_null_with_attempts_and_context() { - Action onRetry = (_, _, _) => { }; + Func sleepDurationProvider = null!; Action policy = () => Policy .Handle() - .WaitAndRetryForeverAsync(null, onRetry); + .WaitAndRetryForeverAsync(sleepDurationProvider); Should.Throw(policy) .ParamName.ShouldBe("sleepDurationProvider"); @@ -81,6 +113,19 @@ public void Should_throw_when_onretry_exception_int_timespan_is_null_with_sleep_ .ParamName.ShouldBe("onRetry"); } + [Fact] + public void Should_throw_when_onretryasync_exception_int_timespan_is_null_with_sleep_duration_provider() + { + Func provider = _ => TimeSpan.Zero; + + Action policy = () => Policy + .Handle() + .WaitAndRetryForeverAsync(provider, default(Func)); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetryAsync"); + } + [Fact] public void Should_throw_when_sleep_duration_provider_int_context_timespan_is_null_with_retry() { diff --git a/test/Polly.Specs/Retry/WaitAndRetryForeverSpecs.cs b/test/Polly.Specs/Retry/WaitAndRetryForeverSpecs.cs index 0c93f9b7697..b01bce504dd 100644 --- a/test/Polly.Specs/Retry/WaitAndRetryForeverSpecs.cs +++ b/test/Polly.Specs/Retry/WaitAndRetryForeverSpecs.cs @@ -18,6 +18,18 @@ public void Should_throw_when_sleep_duration_provider_is_null_without_context() .ParamName.ShouldBe("sleepDurationProvider"); } + [Fact] + public void Should_throw_not_when_sleep_duration_provider_is_not_null_with_context() + { + Func sleepDurationProvider = (_, _) => TimeSpan.Zero; + + Action policy = () => Policy + .Handle() + .WaitAndRetryForever(sleepDurationProvider); + + Should.NotThrow(policy); + } + [Fact] public void Should_throw_when_sleep_duration_provider_is_null_without_context_with_onretry() { diff --git a/test/Polly.Specs/Retry/WaitAndRetryForeverTResultSpecs.cs b/test/Polly.Specs/Retry/WaitAndRetryForeverTResultSpecs.cs index 6776e9983cb..4ac4551f280 100644 --- a/test/Polly.Specs/Retry/WaitAndRetryForeverTResultSpecs.cs +++ b/test/Polly.Specs/Retry/WaitAndRetryForeverTResultSpecs.cs @@ -34,6 +34,106 @@ public void Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fau actualRetryWaits.ShouldBeInOrder(); } + [Fact] + public void Should_throw_when_sleepDurationProvider_is_null() + { + Func sleepDurationProvider = null!; + + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever(sleepDurationProvider); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + + Func sleepDurationProviderContext = null!; + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever(sleepDurationProviderContext); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever(sleepDurationProvider, (_, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever(sleepDurationProvider, (_, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever(sleepDurationProviderContext, (_, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever(sleepDurationProviderContext, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + + Func, Context, TimeSpan> sleepDurationProviderResult = null!; + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever(sleepDurationProviderResult, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + } + + [Fact] + public void Should_throw_when_onRetry_is_null() + { + Action, TimeSpan> onRetry = null!; + + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever((_) => TimeSpan.Zero, onRetry); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + Action, int, TimeSpan> onRetryAttempts = null!; + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever((_) => TimeSpan.Zero, onRetryAttempts); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + Action, int, TimeSpan, Context> onRetryContext = null!; + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever((_, _, _) => TimeSpan.Zero, onRetryContext); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + } + + [Fact] + public void Should_not_throw_when_sleepDurationProvider_is_not_null() + { + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetryForever((_) => TimeSpan.Zero); + + Should.NotThrow(policy); + } + public void Dispose() => SystemClock.Reset(); } diff --git a/test/Polly.Specs/Retry/WaitAndRetrySpecs.cs b/test/Polly.Specs/Retry/WaitAndRetrySpecs.cs index 0828ef6dcf8..70c387ae999 100644 --- a/test/Polly.Specs/Retry/WaitAndRetrySpecs.cs +++ b/test/Polly.Specs/Retry/WaitAndRetrySpecs.cs @@ -495,7 +495,7 @@ public void Should_create_new_context_for_each_call_to_execute() } [Fact] - public void Should_throw_when_retry_count_is_less_than_zero_without_context() + public void Should_throw_when_retry_count_is_less_than_zero() { Action onRetry = (_, _) => { }; @@ -505,19 +505,41 @@ public void Should_throw_when_retry_count_is_less_than_zero_without_context() Should.Throw(policy) .ParamName.ShouldBe("retryCount"); + + Action onRetryContext = (_, _, _) => { }; + + policy = () => + Policy.Handle() + .WaitAndRetry(-1, _ => default, onRetryContext); + + Should.Throw(policy) + .ParamName.ShouldBe("retryCount"); } [Fact] - public void Should_throw_when_retry_count_is_less_than_zero_with_context() + public void Should_not_throw_when_retry_count_is_zero() { - Action onRetry = (_, _, _) => { }; + Action onRetry = (_, _) => { }; Action policy = () => Policy.Handle() - .WaitAndRetry(-1, _ => default, onRetry); + .WaitAndRetry(0, _ => default, onRetry); - Should.Throw(policy) - .ParamName.ShouldBe("retryCount"); + Should.NotThrow(policy); + + Action onRetryContext = (_, _, _) => { }; + + policy = () => + Policy.Handle() + .WaitAndRetry(0, _ => default, onRetryContext); + + Should.NotThrow(policy); + + policy = () => + Policy.Handle() + .WaitAndRetry(0, (_, _, _) => TimeSpan.Zero, (_, _, _, _) => { }); + + Should.NotThrow(policy); } [Fact] diff --git a/test/Polly.Specs/Retry/WaitAndRetryTResultAsyncSpecs.cs b/test/Polly.Specs/Retry/WaitAndRetryTResultAsyncSpecs.cs index fc2dba2cb5e..2a5a952af43 100644 --- a/test/Polly.Specs/Retry/WaitAndRetryTResultAsyncSpecs.cs +++ b/test/Polly.Specs/Retry/WaitAndRetryTResultAsyncSpecs.cs @@ -44,45 +44,122 @@ await policy.ExecuteAsync(async () => [Fact] public void Should_throw_when_retry_count_is_less_than_zero() { - var expectedRetryWaits = new Dictionary - { - [ResultPrimitive.Fault] = TimeSpan.FromSeconds(2), - [ResultPrimitive.FaultAgain] = TimeSpan.FromSeconds(4), - }; + Action configure = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync( + -1, + (_) => TimeSpan.Zero, + (_, _, _, _) => TaskHelper.EmptyTask); - var actualRetryWaits = new List(); + Should.Throw(configure).ParamName.ShouldBe("retryCount"); - Action configure = () => Policy + configure = () => Policy .HandleResult(ResultPrimitive.Fault) - .WaitAndRetryAsync(-1, - (_, outcome, _) => expectedRetryWaits[outcome.Result], - (_, timeSpan, _, _) => - { - actualRetryWaits.Add(timeSpan); - return TaskHelper.EmptyTask; - }); + .WaitAndRetryAsync( + -1, + (_, _, _) => TimeSpan.Zero, + (_, _, _, _) => TaskHelper.EmptyTask); Should.Throw(configure).ParamName.ShouldBe("retryCount"); } [Fact] - public void Should_throw_when_onRetryAsync_is_null() + public void Should_not_throw_when_retry_count_is_zero() { - var expectedRetryWaits = new Dictionary - { - [ResultPrimitive.Fault] = TimeSpan.FromSeconds(2), - [ResultPrimitive.FaultAgain] = TimeSpan.FromSeconds(4), - }; + Action configure = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync( + 0, + (_) => TimeSpan.Zero, + (_, _, _, _) => TaskHelper.EmptyTask); + + Should.NotThrow(configure); + + configure = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync( + 0, + (_, _, _) => TimeSpan.Zero, + (_, _, _, _) => TaskHelper.EmptyTask); + Should.NotThrow(configure); + } + + [Fact] + public void Should_throw_when_onRetryAsync_is_null() + { Action configure = () => Policy .HandleResult(ResultPrimitive.Fault) - .WaitAndRetryAsync(2, - (_, outcome, _) => expectedRetryWaits[outcome.Result], + .WaitAndRetryAsync( + 2, + (_, _, _) => default, null); Should.Throw(configure).ParamName.ShouldBe("onRetryAsync"); } + [Fact] + public void Should_throw_when_onretry_action_is_null() + { + Action, TimeSpan> onRetry = null!; + + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync(1, _ => TimeSpan.Zero, onRetry); + + Should.Throw(policy).ParamName.ShouldBe("onRetry"); + + Func, TimeSpan, Task> onRetryAsync = null!; + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync(1, (_) => TimeSpan.Zero, onRetryAsync); + + Should.Throw(policy).ParamName.ShouldBe("onRetryAsync"); + + Action, TimeSpan, Context> onRetryContext = null!; + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync(1, (_) => TimeSpan.Zero, onRetryContext); + + Should.Throw(policy).ParamName.ShouldBe("onRetry"); + + Func, TimeSpan, Context, Task> onRetryAsyncContext = null!; + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync(1, (_) => TimeSpan.Zero, onRetryAsyncContext); + + Should.Throw(policy).ParamName.ShouldBe("onRetryAsync"); + + Action, TimeSpan, int, Context> onRetryAttemptsResult = null!; + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync(1, (_) => TimeSpan.Zero, onRetryAttemptsResult); + + Should.Throw(policy).ParamName.ShouldBe("onRetry"); + + Action, TimeSpan, Context> onRetryResult = null!; + + policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync(1, (_, _) => TimeSpan.Zero, onRetryResult); + + Should.Throw(policy).ParamName.ShouldBe("onRetry"); + } + + [Fact] + public void Should_not_throw_arguments_valid() + { + Action policy = () => Policy + .HandleResult(ResultPrimitive.Fault) + .WaitAndRetryAsync(1, (_) => TimeSpan.Zero, (_, _, _, _) => { }); + + Should.NotThrow(policy); + } + public void Dispose() => SystemClock.Reset(); } diff --git a/test/Polly.Specs/Retry/WaitAndRetryTResultSpecs.cs b/test/Polly.Specs/Retry/WaitAndRetryTResultSpecs.cs index 874f29a4891..372b5da7405 100644 --- a/test/Polly.Specs/Retry/WaitAndRetryTResultSpecs.cs +++ b/test/Polly.Specs/Retry/WaitAndRetryTResultSpecs.cs @@ -33,6 +33,142 @@ public void Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fau actualRetryWaits.ShouldBeSubsetOf(expectedRetryWaits.Values); } + [Fact] + public void Should_throw_when_retry_count_is_less_than_zero() + { + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(-1, (_, _, _) => default, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("retryCount"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(-1, (_) => default, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("retryCount"); + } + + [Fact] + public void Should_throw_when_sleepDurationProvider_is_null() + { + Func sleepDurationProvider = null!; + + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(1, sleepDurationProvider, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurationProvider"); + } + + [Fact] + public void Should_not_throw_when_sleepDurationProvider_is_not_null() + { + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(1, (_) => TimeSpan.Zero); + + Should.NotThrow(policy); + } + + [Fact] + public void Should_not_throw_when_retry_count_is_zero() + { + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(0, (_, _, _) => default, (_, _, _, _) => { }); + + Should.NotThrow(policy); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(0, (_) => default, (_, _, _, _) => { }); + + Should.NotThrow(policy); + } + + [Fact] + public void Should_throw_when_onRetry_is_null() + { + Action, TimeSpan> onRetry = null!; + + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(1, (_) => TimeSpan.Zero, onRetry); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + Action, TimeSpan, Context> onRetryContext = null!; + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(1, (_) => TimeSpan.Zero, onRetryContext); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + Action, TimeSpan, int, Context> onRetryAttempts = null!; + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(1, (_) => TimeSpan.Zero, onRetryAttempts); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(1, (_, _) => TimeSpan.Zero, onRetryContext); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(1, (_, _, _) => TimeSpan.Zero, onRetryContext); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry([], onRetry); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry([], onRetryContext); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + + policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry([], onRetryAttempts); + + Should.Throw(policy) + .ParamName.ShouldBe("onRetry"); + } + + [Fact] + public void Should_throw_when_sleepDurations_is_null() + { + IEnumerable sleepDurations = null!; + + Action policy = () => + Policy.HandleResult(ResultPrimitive.Fault) + .WaitAndRetry(sleepDurations, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("sleepDurations"); + } + public void Dispose() => SystemClock.Reset(); } diff --git a/test/Polly.Specs/Timeout/TimeoutAsyncSpecs.cs b/test/Polly.Specs/Timeout/TimeoutAsyncSpecs.cs index c5db1eb0278..e183dd96e71 100644 --- a/test/Polly.Specs/Timeout/TimeoutAsyncSpecs.cs +++ b/test/Polly.Specs/Timeout/TimeoutAsyncSpecs.cs @@ -33,6 +33,43 @@ public void Should_throw_when_action_is_null() .ParamName.ShouldBe("action"); } + [Fact] + public void Should_not_throw_with_ontimeoutasync() + { + Action policy = () => Policy.TimeoutAsync(1, (_, _, _) => TaskHelper.EmptyTask); + + Should.NotThrow(policy); + } + + [Fact] + public void Should_throw_when_ontimeoutasync_is_null() + { + Func onTimeoutAsync = null!; + + Action policy = () => Policy.TimeoutAsync(1, onTimeoutAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onTimeoutAsync"); + } + + [Fact] + public void Should_not_throw_with_ontimeoutasync_with_exception() + { + Action policy = () => Policy.TimeoutAsync(1, (_, _, _, _) => TaskHelper.EmptyTask); + + Should.NotThrow(policy); + } + + [Fact] + public void Should_throw_when_ontimeoutasync_is_null_with_ontimeoutasync_with_exception() + { + Func onTimeoutAsync = null!; + Action policy = () => Policy.TimeoutAsync(1, onTimeoutAsync); + + Should.Throw(policy) + .ParamName.ShouldBe("onTimeoutAsync"); + } + [Fact] public void Should_throw_when_timeout_is_zero_by_timespan() { @@ -213,6 +250,16 @@ public void Should_throw_when_onTimeout_is_null_with_seconds_with_full_argument_ .ParamName.ShouldBe("onTimeoutAsync"); } + [Fact] + public void Should_throw_when_timeout_is_zero_by_seconds_with_ontimeoutasync_with_exception() + { + Func onTimeout = (_, _, _, _) => TaskHelper.EmptyTask; + Action policy = () => Policy.TimeoutAsync(0, onTimeout); + + Should.Throw(policy) + .ParamName.ShouldBe("seconds"); + } + [Fact] public void Should_throw_when_timeout_is_zero_by_seconds_with_timeoutstrategy_and_full_argument_list_onTimeout() { @@ -365,9 +412,18 @@ public async Task Should_cancel_downstream_token_on_timeout__pessimistic() var act = () => policy.ExecuteAsync(async (combinedToken) => { - combinedToken.Register(() => isCancelled = true); - await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(1000), combinedToken); - }, CancellationToken); + combinedToken.IsCancellationRequested.ShouldBeFalse(); + + try + { + combinedToken.Register(() => isCancelled = true); + await SystemClock.SleepAsync(TimeSpan.FromMilliseconds(1000), combinedToken); + } + finally + { + combinedToken.IsCancellationRequested.ShouldBeTrue(); + } + }, CancellationToken.None); await Should.ThrowAsync(act); @@ -755,7 +811,6 @@ await Should.ThrowAsync(() => policy.ExecuteAsync(asyn await SystemClock.SleepAsync(thriceShimTimeSpan, CancellationToken); exceptionObservedFromTaskPassedToOnTimeout.ShouldNotBeNull(); exceptionObservedFromTaskPassedToOnTimeout.ShouldBe(exceptionToThrow); - } [Fact] diff --git a/test/Polly.Specs/Timeout/TimeoutSpecs.cs b/test/Polly.Specs/Timeout/TimeoutSpecs.cs index f26ec81aa5f..e42bdb6e97d 100644 --- a/test/Polly.Specs/Timeout/TimeoutSpecs.cs +++ b/test/Polly.Specs/Timeout/TimeoutSpecs.cs @@ -47,6 +47,16 @@ public void Should_throw_when_timeout_is_zero_by_seconds() { Action policy = () => Policy.Timeout(0); + Should.Throw(policy) + .ParamName.ShouldBe("seconds"); + + policy = () => Policy.Timeout(0, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("seconds"); + + policy = () => Policy.Timeout(0, TimeoutStrategy.Pessimistic, (_, _, _, _) => { }); + Should.Throw(policy) .ParamName.ShouldBe("seconds"); } @@ -134,6 +144,14 @@ public void Should_not_throw_when_timeout_is_infinitetimespan_with_timeoutstrate Should.NotThrow(policy); } + [Fact] + public void Should_not_throw_when_timeout_is_greater_than_zero_by_seconds_with_timeoutstrategy_and_handler() + { + Action policy = () => Policy.Timeout(3, TimeoutStrategy.Optimistic, (_, _, _) => { }); + + Should.NotThrow(policy); + } + [Fact] public void Should_throw_when_timeout_is_less_than_zero_by_seconds_with_ontimeout() { @@ -234,6 +252,26 @@ public void Should_throw_when_timeoutProvider_is_null() { Action policy = () => Policy.Timeout((Func)null!); + Should.Throw(policy) + .ParamName.ShouldBe("timeoutProvider"); + + policy = () => Policy.Timeout((Func)null!, TimeoutStrategy.Pessimistic, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("timeoutProvider"); + + policy = () => Policy.Timeout((Func)null!, (_, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("timeoutProvider"); + + policy = () => Policy.Timeout((Func)null!, (_, _, _, _) => { }); + + Should.Throw(policy) + .ParamName.ShouldBe("timeoutProvider"); + + policy = () => Policy.Timeout((Func)null!, TimeoutStrategy.Pessimistic, (_, _, _, _) => { }); + Should.Throw(policy) .ParamName.ShouldBe("timeoutProvider"); } @@ -259,11 +297,40 @@ public void Should_throw_when_onTimeout_overload_is_null_with_timeoutprovider() } [Fact] - public void Should_be_able_to_configure_with_timeout_func() + public void Should_be_able_to_configure_without_an_exception() { Action policy = () => Policy.Timeout(() => TimeSpan.FromSeconds(1)); Should.NotThrow(policy); + + policy = () => Policy.Timeout( + () => TimeSpan.FromSeconds(1), + TimeoutStrategy.Optimistic, + (_, _, _, _) => { }); + + Should.NotThrow(policy); + + policy = () => Policy.Timeout((_) => TimeSpan.FromSeconds(1)); + + Should.NotThrow(policy); + + policy = () => Policy.Timeout( + (_) => TimeSpan.FromSeconds(1), + TimeoutStrategy.Optimistic); + + Should.NotThrow(policy); + + policy = () => Policy.Timeout( + (_) => TimeSpan.FromSeconds(1), + (_, _, _) => { }); + + Should.NotThrow(policy); + + policy = () => Policy.Timeout( + (_) => TimeSpan.FromSeconds(1), + (_, _, _, _) => { }); + + Should.NotThrow(policy); } #endregion @@ -804,6 +871,21 @@ public void Should_call_ontimeout_with_timeout_supplied_different_for_each_execu timeoutPassedToOnTimeout.ShouldBe(timeoutFunc()); } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Should_call_ontimeout_with_timeout_supplied_different_for_each_execution_by_evaluating_func_not_context__optimistic(int programaticallyControlledDelay) + { + Func timeoutProvider = () => TimeSpan.FromSeconds(programaticallyControlledDelay); + + var policy = Policy.Timeout(timeoutProvider, TimeoutStrategy.Optimistic); + + var userCancellationToken = CancellationToken; + + Should.Throw(() => policy.Execute((ct) => SystemClock.Sleep(TimeSpan.FromSeconds(3), ct), userCancellationToken)); + } + [Theory] [InlineData(1)] [InlineData(2)] diff --git a/test/Polly.Specs/Timeout/TimeoutTResultAsyncSpecs.cs b/test/Polly.Specs/Timeout/TimeoutTResultAsyncSpecs.cs index 1243ee41361..834a76e0e58 100644 --- a/test/Polly.Specs/Timeout/TimeoutTResultAsyncSpecs.cs +++ b/test/Polly.Specs/Timeout/TimeoutTResultAsyncSpecs.cs @@ -37,6 +37,11 @@ public void Should_throw_when_timeout_is_zero_by_timespan() { Action policy = () => Policy.TimeoutAsync(TimeSpan.Zero); + Should.Throw(policy) + .ParamName.ShouldBe("timeout"); + + policy = () => Policy.TimeoutAsync(TimeSpan.Zero, (_, _, _, _) => TaskHelper.EmptyTask); + Should.Throw(policy) .ParamName.ShouldBe("timeout"); } @@ -46,6 +51,11 @@ public void Should_throw_when_timeout_is_zero_by_seconds() { Action policy = () => Policy.TimeoutAsync(0); + Should.Throw(policy) + .ParamName.ShouldBe("seconds"); + + policy = () => Policy.TimeoutAsync(0, (_, _, _, _) => TaskHelper.EmptyTask); + Should.Throw(policy) .ParamName.ShouldBe("seconds"); } @@ -53,7 +63,12 @@ public void Should_throw_when_timeout_is_zero_by_seconds() [Fact] public void Should_throw_when_timeout_is_less_than_zero_by_timespan() { - Action policy = () => Policy.TimeoutAsync(-TimeSpan.FromHours(1)); + Action policy = () => Policy.TimeoutAsync(TimeSpan.FromHours(-1)); + + Should.Throw(policy) + .ParamName.ShouldBe("timeout"); + + policy = () => Policy.TimeoutAsync(TimeSpan.FromHours(-1), (_, _, _, _) => TaskHelper.EmptyTask); Should.Throw(policy) .ParamName.ShouldBe("timeout"); @@ -232,6 +247,15 @@ public void Should_throw_when_onTimeout_is_null_with_timespan_with_full_argument .ParamName.ShouldBe("onTimeoutAsync"); } + [Fact] + public void Should_not_throw_when_onTimeout_is_not_null_with_valid_timespan() + { + Func onTimeout = (_, _, _, _) => TaskHelper.EmptyTask; + Action policy = () => Policy.TimeoutAsync(TimeSpan.FromMinutes(0.5), onTimeout); + + Should.NotThrow(policy); + } + [Fact] public void Should_throw_when_onTimeout_is_null_with_seconds() {