From 36f47bc8a34af61eda5fe2134ca253739ca3231a Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Sun, 18 Jun 2023 21:26:42 +0200 Subject: [PATCH] Introduce OutcomeResilienceStrategy --- ...ltipleStrategiesBenchmark-report-github.md | 11 +-- ...Benchmarks.RetryBenchmark-report-github.md | 14 ++-- .../MultipleStrategiesBenchmark.cs | 15 ++++ .../Utils/Helper.MultipleStrategies.cs | 55 +++++++++++-- .../CircuitBreakerResilienceStrategy.cs | 25 +++--- ...akerResilienceStrategyBuilderExtensions.cs | 18 ++-- .../Controller/CircuitStateController.cs | 38 ++++----- .../CircuitBreaker/Health/HealthMetrics.cs | 2 +- .../SimpleCircuitBreakerStrategyOptions.cs | 2 +- src/Polly.Core/Fallback/FallbackHandler.cs | 8 +- .../Fallback/FallbackResilienceStrategy.cs | 27 +++--- ...backResilienceStrategyBuilderExtensions.cs | 7 +- .../Controller/HedgingExecutionContext.cs | 10 +-- .../Hedging/Controller/HedgingHandler.cs | 16 ++-- .../Hedging/Controller/TaskExecution.cs | 30 +++---- .../Hedging/HedgingResilienceStrategy.cs | 37 ++++----- ...gingResilienceStrategyBuilderExtensions.cs | 7 +- src/Polly.Core/Outcome.cs | 25 +++++- src/Polly.Core/Polly.Core.csproj | 2 +- .../ResilienceStrategyBuilderContext.cs | 17 ---- .../Retry/RetryResilienceStrategy.cs | 52 +++++------- ...etryResilienceStrategyBuilderExtensions.cs | 10 +-- src/Polly.Core/Utils/EventInvoker.cs | 39 --------- src/Polly.Core/Utils/GeneratorInvoker.cs | 47 ----------- .../Utils/OutcomeResilienceStrategy.cs | 82 +++++++++++++++++++ src/Polly.Core/Utils/PredicateInvoker.cs | 39 --------- src/Polly.Extensions/Polly.Extensions.csproj | 2 +- .../Polly.RateLimiting.csproj | 2 +- src/Polly/Polly.csproj | 2 +- ...itBreakerResilienceStrategyBuilderTests.cs | 8 +- .../CircuitBreakerResilienceStrategyTests.cs | 8 +- .../Controller/CircuitStateControllerTests.cs | 46 +++++------ .../SimpleCircuitBreakerOptionsTests.cs | 6 +- .../Fallback/FallbackHandlerTests.cs | 20 ----- .../Fallback/FallbackHelper.cs | 3 +- .../FallbackResilienceStrategyTests.cs | 8 +- .../HedgingExecutionContextTests.cs | 4 +- .../Hedging/Controller/TaskExecutionTests.cs | 10 +-- .../Hedging/HedgingHandlerTests.cs | 11 ++- .../Polly.Core.Tests/Hedging/HedgingHelper.cs | 3 +- .../Hedging/HedgingResilienceStrategyTests.cs | 16 ++-- ...esilienceStrategyBuilderExtensionsTests.cs | 8 +- .../Retry/RetryResilienceStrategyTests.cs | 12 +-- .../Utils/EventInvokerTests.cs | 69 ---------------- .../Utils/GeneratorInvokerTests.cs | 66 --------------- .../Utils/OutcomeResilienceStrategyTests.cs | 80 ++++++++++++++++++ .../Utils/PredicateInvokerTests.cs | 65 --------------- 47 files changed, 465 insertions(+), 619 deletions(-) delete mode 100644 src/Polly.Core/Utils/EventInvoker.cs delete mode 100644 src/Polly.Core/Utils/GeneratorInvoker.cs create mode 100644 src/Polly.Core/Utils/OutcomeResilienceStrategy.cs delete mode 100644 src/Polly.Core/Utils/PredicateInvoker.cs delete mode 100644 test/Polly.Core.Tests/Utils/EventInvokerTests.cs delete mode 100644 test/Polly.Core.Tests/Utils/GeneratorInvokerTests.cs create mode 100644 test/Polly.Core.Tests/Utils/OutcomeResilienceStrategyTests.cs delete mode 100644 test/Polly.Core.Tests/Utils/PredicateInvokerTests.cs diff --git a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md index f4743763425..743e847e00b 100644 --- a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md +++ b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md @@ -9,8 +9,9 @@ Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15 LaunchCount=2 WarmupCount=10 ``` -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|------------------------------------- |---------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| -| ExecuteStrategyPipeline_V7 | 2.220 μs | 0.0164 μs | 0.0236 μs | 1.00 | 0.00 | 0.1106 | 2824 B | 1.00 | -| ExecuteStrategyPipeline_V8 | 1.901 μs | 0.0089 μs | 0.0127 μs | 0.86 | 0.01 | - | 40 B | 0.01 | -| ExecuteStrategyPipeline_Telemetry_V8 | 2.947 μs | 0.0077 μs | 0.0115 μs | 1.33 | 0.02 | - | 40 B | 0.01 | +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|-------------------------------------- |---------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| +| ExecuteStrategyPipeline_V7 | 2.488 μs | 0.0316 μs | 0.0463 μs | 1.00 | 0.00 | 0.1106 | 2824 B | 1.00 | +| ExecuteStrategyPipeline_V8 | 1.913 μs | 0.0066 μs | 0.0093 μs | 0.77 | 0.01 | - | 40 B | 0.01 | +| ExecuteStrategyPipeline_Telemetry_V8 | 2.431 μs | 0.0088 μs | 0.0129 μs | 0.98 | 0.02 | - | 40 B | 0.01 | +| ExecuteStrategyPipeline_NonGeneric_V8 | 2.207 μs | 0.0068 μs | 0.0102 μs | 0.89 | 0.01 | - | 40 B | 0.01 | diff --git a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.RetryBenchmark-report-github.md b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.RetryBenchmark-report-github.md index 03f4b55b33e..15dab53b596 100644 --- a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.RetryBenchmark-report-github.md +++ b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.RetryBenchmark-report-github.md @@ -1,15 +1,15 @@ ``` ini -BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2) +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1848/22H2/2022Update/SunValley2) Intel Core i9-10885H CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores -.NET SDK=7.0.203 - [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 +.NET SDK=7.0.304 + [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15 LaunchCount=2 WarmupCount=10 ``` -| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|---------------- |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| -| ExecuteRetry_V7 | 264.2 ns | 10.80 ns | 15.83 ns | 255.3 ns | 1.00 | 0.00 | 0.0658 | 552 B | 1.00 | -| ExecuteRetry_V8 | 244.7 ns | 7.75 ns | 10.61 ns | 244.1 ns | 0.93 | 0.05 | - | - | 0.00 | +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|---------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|------------:| +| ExecuteRetry_V7 | 210.9 ns | 4.37 ns | 6.27 ns | 1.00 | 0.00 | 0.0658 | 552 B | 1.00 | +| ExecuteRetry_V8 | 224.1 ns | 2.34 ns | 3.43 ns | 1.06 | 0.04 | - | - | 0.00 | diff --git a/bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs b/bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs index 8fd1769a4f0..a1d4d594fd7 100644 --- a/bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs +++ b/bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs @@ -8,6 +8,7 @@ public class MultipleStrategiesBenchmark private object? _strategyV7; private object? _strategyV8; private object? _strategyTelemetryV8; + private ResilienceStrategy? _nonGeneric; [GlobalSetup] public void Setup() @@ -16,6 +17,7 @@ public void Setup() _strategyV7 = Helper.CreateStrategyPipeline(PollyVersion.V7, false); _strategyV8 = Helper.CreateStrategyPipeline(PollyVersion.V8, false); _strategyTelemetryV8 = Helper.CreateStrategyPipeline(PollyVersion.V8, true); + _nonGeneric = Helper.CreateNonGenericStrategyPipeline(); } [GlobalCleanup] @@ -29,4 +31,17 @@ public void Setup() [Benchmark] public ValueTask ExecuteStrategyPipeline_Telemetry_V8() => _strategyTelemetryV8!.ExecuteAsync(PollyVersion.V8); + + [Benchmark] + public async ValueTask ExecuteStrategyPipeline_NonGeneric_V8() + { + var context = ResilienceContext.Get(); + + await _nonGeneric!.ExecuteOutcomeAsync( + static (_, _) => new ValueTask>(new Outcome("dummy")), + context, + string.Empty).ConfigureAwait(false); + + ResilienceContext.Return(context); + } } diff --git a/bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs b/bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs index a6f774a9de2..5e2dbc20fe3 100644 --- a/bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs +++ b/bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs @@ -22,11 +22,18 @@ internal static partial class Helper PermitLimit = 10 }) .AddTimeout(TimeSpan.FromSeconds(10)) - .AddRetry( - predicate => predicate.Handle().HandleResult(Failure), - RetryBackoffType.Constant, - 3, - TimeSpan.FromSeconds(1)) + .AddRetry(new() + { + BackoffType = RetryBackoffType.Constant, + RetryCount = 3, + BaseDelay = TimeSpan.FromSeconds(1), + ShouldHandle = args => args switch + { + { Exception: InvalidOperationException } => PredicateResult.True, + { Result: var result } when result == Failure => PredicateResult.True, + _ => PredicateResult.False + } + }) .AddTimeout(TimeSpan.FromSeconds(1)) .AddAdvancedCircuitBreaker(new() { @@ -49,4 +56,42 @@ internal static partial class Helper }), _ => throw new NotSupportedException() }; + + public static ResilienceStrategy CreateNonGenericStrategyPipeline() + { + return new ResilienceStrategyBuilder() + .AddConcurrencyLimiter(new ConcurrencyLimiterOptions + { + QueueLimit = 10, + PermitLimit = 10 + }) + .AddTimeout(TimeSpan.FromSeconds(10)) + .AddRetry(new() + { + BackoffType = RetryBackoffType.Constant, + RetryCount = 3, + BaseDelay = TimeSpan.FromSeconds(1), + ShouldHandle = args => args switch + { + { Exception: InvalidOperationException } => PredicateResult.True, + { Result: string result } when result == Failure => PredicateResult.True, + _ => PredicateResult.False + } + }) + .AddTimeout(TimeSpan.FromSeconds(1)) + .AddAdvancedCircuitBreaker(new() + { + FailureThreshold = 0.5, + SamplingDuration = TimeSpan.FromSeconds(30), + MinimumThroughput = 10, + BreakDuration = TimeSpan.FromSeconds(5), + ShouldHandle = args => args switch + { + { Exception: InvalidOperationException } => PredicateResult.True, + { Result: string result } when result == Failure => PredicateResult.True, + _ => PredicateResult.False + } + }) + .Build(); + } } diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs index 0577d249386..90327f7d725 100644 --- a/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs @@ -1,15 +1,17 @@ namespace Polly.CircuitBreaker; -internal sealed class CircuitBreakerResilienceStrategy : ResilienceStrategy +internal sealed class CircuitBreakerResilienceStrategy : OutcomeResilienceStrategy { - private readonly PredicateInvoker _handler; - private readonly CircuitStateController _controller; + private readonly Func, ValueTask> _handler; + private readonly CircuitStateController _controller; public CircuitBreakerResilienceStrategy( - PredicateInvoker handler, - CircuitStateController controller, + Func, ValueTask> handler, + CircuitStateController controller, CircuitBreakerStateProvider? stateProvider, - CircuitBreakerManualControl? manualControl) + CircuitBreakerManualControl? manualControl, + bool isGeneric) + : base(isGeneric) { _handler = handler; _controller = controller; @@ -21,20 +23,17 @@ public CircuitBreakerResilienceStrategy( _controller.Dispose); } - protected internal override async ValueTask> ExecuteCoreAsync( - Func>> callback, - ResilienceContext context, - TState state) + protected override async ValueTask> ExecuteCallbackAsync(Func>> callback, ResilienceContext context, TState state) { - if (await _controller.OnActionPreExecuteAsync(context).ConfigureAwait(context.ContinueOnCapturedContext) is Outcome outcome) + if (await _controller.OnActionPreExecuteAsync(context).ConfigureAwait(context.ContinueOnCapturedContext) is Outcome outcome) { return outcome; } outcome = await ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); - var args = new OutcomeArguments(context, outcome, new CircuitBreakerPredicateArguments()); - if (await _handler.HandleAsync(args).ConfigureAwait(context.ContinueOnCapturedContext)) + var args = new OutcomeArguments(context, outcome, new CircuitBreakerPredicateArguments()); + if (await _handler(args).ConfigureAwait(context.ContinueOnCapturedContext)) { await _controller.OnActionFailureAsync(outcome, context).ConfigureAwait(context.ContinueOnCapturedContext); } diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderExtensions.cs index 0cd6f7586d1..67d0388888f 100644 --- a/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderExtensions.cs @@ -121,22 +121,26 @@ private static TBuilder AddSimpleCircuitBreakerCore(this TBui return builder.AddStrategy(context => CreateStrategy(context, options, new ConsecutiveFailuresCircuitBehavior(options.FailureThreshold)), options); } - internal static CircuitBreakerResilienceStrategy CreateStrategy(ResilienceStrategyBuilderContext context, CircuitBreakerStrategyOptions options, CircuitBehavior behavior) + internal static CircuitBreakerResilienceStrategy CreateStrategy( + ResilienceStrategyBuilderContext context, + CircuitBreakerStrategyOptions options, + CircuitBehavior behavior) { - var controller = new CircuitStateController( + var controller = new CircuitStateController( options.BreakDuration, - context.CreateInvoker(options.OnOpened), - context.CreateInvoker(options.OnClosed), + options.OnOpened, + options.OnClosed, options.OnHalfOpened, behavior, context.TimeProvider, context.Telemetry); - return new CircuitBreakerResilienceStrategy( - context.CreateInvoker(options.ShouldHandle)!, + return new CircuitBreakerResilienceStrategy( + options.ShouldHandle!, controller, options.StateProvider, - options.ManualControl); + options.ManualControl, + context.IsGenericBuilder); } } diff --git a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs index f3e1c749d81..967505b5df1 100644 --- a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs +++ b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs @@ -5,12 +5,12 @@ namespace Polly.CircuitBreaker; /// /// Thread-safe controller that holds and manages the circuit breaker state transitions. /// -internal sealed class CircuitStateController : IDisposable +internal sealed class CircuitStateController : IDisposable { private readonly object _lock = new(); private readonly ScheduledTaskExecutor _executor = new(); - private readonly EventInvoker? _onOpened; - private readonly EventInvoker? _onClosed; + private readonly Func, ValueTask>? _onOpened; + private readonly Func, ValueTask>? _onClosed; private readonly Func? _onHalfOpen; private readonly TimeProvider _timeProvider; private readonly ResilienceStrategyTelemetry _telemetry; @@ -24,8 +24,8 @@ internal sealed class CircuitStateController : IDisposable public CircuitStateController( TimeSpan breakDuration, - EventInvoker? onOpened, - EventInvoker? onClosed, + Func, ValueTask>? onOpened, + Func, ValueTask>? onClosed, Func? onHalfOpen, CircuitBehavior behavior, TimeProvider timeProvider, @@ -90,7 +90,7 @@ public ValueTask IsolateCircuitAsync(ResilienceContext context) lock (_lock) { SetLastHandledOutcome_NeedsLock(new Outcome(new IsolatedCircuitException())); - OpenCircuitFor_NeedsLock(new Outcome(VoidResult.Instance), TimeSpan.MaxValue, manual: true, context, out task); + OpenCircuitFor_NeedsLock(new Outcome(default(T)), TimeSpan.MaxValue, manual: true, context, out task); _circuitState = CircuitState.Isolated; } @@ -107,13 +107,13 @@ public ValueTask CloseCircuitAsync(ResilienceContext context) lock (_lock) { - CloseCircuit_NeedsLock(new Outcome(VoidResult.Instance), manual: true, context, out task); + CloseCircuit_NeedsLock(new Outcome(default(T)), manual: true, context, out task); } return ExecuteScheduledTaskAsync(task, context); } - public async ValueTask?> OnActionPreExecuteAsync(ResilienceContext context) + public async ValueTask?> OnActionPreExecuteAsync(ResilienceContext context) { EnsureNotDisposed(); @@ -150,13 +150,13 @@ public ValueTask CloseCircuitAsync(ResilienceContext context) if (exception is not null) { - return new Outcome(exception); + return new Outcome(exception); } return null; } - public ValueTask OnActionSuccessAsync(Outcome outcome, ResilienceContext context) + public ValueTask OnActionSuccessAsync(Outcome outcome, ResilienceContext context) { EnsureNotDisposed(); @@ -182,7 +182,7 @@ public ValueTask OnActionSuccessAsync(Outcome outcome, Resilie return ExecuteScheduledTaskAsync(task, context); } - public ValueTask OnActionFailureAsync(Outcome outcome, ResilienceContext context) + public ValueTask OnActionFailureAsync(Outcome outcome, ResilienceContext context) { EnsureNotDisposed(); @@ -251,11 +251,11 @@ private void EnsureNotDisposed() { if (_disposed) { - throw new ObjectDisposedException(nameof(CircuitStateController)); + throw new ObjectDisposedException(nameof(CircuitStateController)); } } - private void CloseCircuit_NeedsLock(Outcome outcome, bool manual, ResilienceContext context, out Task? scheduledTask) + private void CloseCircuit_NeedsLock(Outcome outcome, bool manual, ResilienceContext context, out Task? scheduledTask) { scheduledTask = null; @@ -269,12 +269,12 @@ private void CloseCircuit_NeedsLock(Outcome outcome, bool manu if (priorState != CircuitState.Closed) { - var args = new OutcomeArguments(context, outcome, new OnCircuitClosedArguments(manual)); + var args = new OutcomeArguments(context, outcome, new OnCircuitClosedArguments(manual)); _telemetry.Report(CircuitBreakerConstants.OnCircuitClosed, args); if (_onClosed is not null) { - _executor.ScheduleTask(() => _onClosed.HandleAsync(args).AsTask(), context, out scheduledTask); + _executor.ScheduleTask(() => _onClosed(args).AsTask(), context, out scheduledTask); } } } @@ -310,12 +310,12 @@ private void SetLastHandledOutcome_NeedsLock(Outcome outcome) private BrokenCircuitException GetBreakingException_NeedsLock() => _breakingException ?? new BrokenCircuitException(); - private void OpenCircuit_NeedsLock(Outcome outcome, bool manual, ResilienceContext context, out Task? scheduledTask) + private void OpenCircuit_NeedsLock(Outcome outcome, bool manual, ResilienceContext context, out Task? scheduledTask) { OpenCircuitFor_NeedsLock(outcome, _breakDuration, manual, context, out scheduledTask); } - private void OpenCircuitFor_NeedsLock(Outcome outcome, TimeSpan breakDuration, bool manual, ResilienceContext context, out Task? scheduledTask) + private void OpenCircuitFor_NeedsLock(Outcome outcome, TimeSpan breakDuration, bool manual, ResilienceContext context, out Task? scheduledTask) { scheduledTask = null; var utcNow = _timeProvider.UtcNow; @@ -325,12 +325,12 @@ private void OpenCircuitFor_NeedsLock(Outcome outcome, TimeSpa var transitionedState = _circuitState; _circuitState = CircuitState.Open; - var args = new OutcomeArguments(context, outcome, new OnCircuitOpenedArguments(breakDuration, manual)); + var args = new OutcomeArguments(context, outcome, new OnCircuitOpenedArguments(breakDuration, manual)); _telemetry.Report(CircuitBreakerConstants.OnCircuitOpened, args); if (_onOpened is not null) { - _executor.ScheduleTask(() => _onOpened.HandleAsync(args).AsTask(), context, out scheduledTask); + _executor.ScheduleTask(() => _onOpened(args).AsTask(), context, out scheduledTask); } } } diff --git a/src/Polly.Core/CircuitBreaker/Health/HealthMetrics.cs b/src/Polly.Core/CircuitBreaker/Health/HealthMetrics.cs index 0d9f03d97c9..b5480b45cac 100644 --- a/src/Polly.Core/CircuitBreaker/Health/HealthMetrics.cs +++ b/src/Polly.Core/CircuitBreaker/Health/HealthMetrics.cs @@ -2,7 +2,7 @@ namespace Polly.CircuitBreaker.Health; /// /// The health metrics for advanced circuit breaker. -/// All operations here are executed from under a lock and are thread safe. +/// All operations here are executed from under a lock and are thread safe. /// internal abstract class HealthMetrics { diff --git a/src/Polly.Core/CircuitBreaker/SimpleCircuitBreakerStrategyOptions.cs b/src/Polly.Core/CircuitBreaker/SimpleCircuitBreakerStrategyOptions.cs index f1e32fee11f..a299eef3be6 100644 --- a/src/Polly.Core/CircuitBreaker/SimpleCircuitBreakerStrategyOptions.cs +++ b/src/Polly.Core/CircuitBreaker/SimpleCircuitBreakerStrategyOptions.cs @@ -1,6 +1,6 @@ namespace Polly.CircuitBreaker; /// -public class SimpleCircuitBreakerStrategyOptions : SimpleCircuitBreakerStrategyOptions +public class SimpleCircuitBreakerStrategyOptions : SimpleCircuitBreakerStrategyOptions { } diff --git a/src/Polly.Core/Fallback/FallbackHandler.cs b/src/Polly.Core/Fallback/FallbackHandler.cs index 5a30e6595f1..3753cd7d191 100644 --- a/src/Polly.Core/Fallback/FallbackHandler.cs +++ b/src/Polly.Core/Fallback/FallbackHandler.cs @@ -1,16 +1,10 @@ namespace Polly.Fallback; internal sealed record class FallbackHandler( - PredicateInvoker ShouldHandle, + Func, ValueTask> ShouldHandle, Func, ValueTask>> ActionGenerator, bool IsGeneric) { - public bool HandlesFallback() => IsGeneric switch - { - true => typeof(TResult) == typeof(T), - false => true - }; - public async ValueTask> GetFallbackOutcomeAsync(OutcomeArguments args) { var copiedArgs = new OutcomeArguments( diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs index 9fc8ad509f1..b84173ebfd5 100644 --- a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs @@ -4,43 +4,36 @@ namespace Polly.Fallback; #pragma warning disable CA1031 // Do not catch general exception types -internal sealed class FallbackResilienceStrategy : ResilienceStrategy +internal sealed class FallbackResilienceStrategy : OutcomeResilienceStrategy { private readonly FallbackHandler _handler; - private readonly EventInvoker? _onFallback; + private readonly Func, ValueTask>? _onFallback; private readonly ResilienceStrategyTelemetry _telemetry; - public FallbackResilienceStrategy(FallbackHandler handler, EventInvoker? onFallback, ResilienceStrategyTelemetry telemetry) + public FallbackResilienceStrategy(FallbackHandler handler, Func, ValueTask>? onFallback, ResilienceStrategyTelemetry telemetry, bool isGeneric) + : base(isGeneric) { _handler = handler; _onFallback = onFallback; _telemetry = telemetry; } - protected internal override async ValueTask> ExecuteCoreAsync( - Func>> callback, - ResilienceContext context, - TState state) + protected override async ValueTask> ExecuteCallbackAsync(Func>> callback, ResilienceContext context, TState state) { - if (!_handler.HandlesFallback()) - { - return await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); - } - var outcome = await ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); - var handleFallbackArgs = new OutcomeArguments(context, outcome, new FallbackPredicateArguments()); - if (!await _handler.ShouldHandle.HandleAsync(handleFallbackArgs).ConfigureAwait(context.ContinueOnCapturedContext)) + var handleFallbackArgs = new OutcomeArguments(context, outcome, new FallbackPredicateArguments()); + if (!await _handler.ShouldHandle(handleFallbackArgs).ConfigureAwait(context.ContinueOnCapturedContext)) { return outcome; } - var onFallbackArgs = new OutcomeArguments(context, outcome, new OnFallbackArguments()); + var onFallbackArgs = new OutcomeArguments(context, outcome, new OnFallbackArguments()); _telemetry.Report(FallbackConstants.OnFallback, onFallbackArgs); if (_onFallback is not null) { - await _onFallback.HandleAsync(onFallbackArgs).ConfigureAwait(context.ContinueOnCapturedContext); + await _onFallback(onFallbackArgs).ConfigureAwait(context.ContinueOnCapturedContext); } try @@ -49,7 +42,7 @@ protected internal override async ValueTask> ExecuteCoreAsync(e); + return new Outcome(e); } } } diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs index 298a0ca1f4b..5a7327db278 100644 --- a/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs @@ -79,14 +79,15 @@ internal static void AddFallbackCore(this ResilienceStrategyBuilderBase builder.AddStrategy(context => { var handler = new FallbackHandler( - context.CreateInvoker(options.ShouldHandle)!, + options.ShouldHandle!, options.FallbackAction!, IsGeneric: context.IsGenericBuilder); return new FallbackResilienceStrategy( handler, - context.CreateInvoker(options.OnFallback), - context.Telemetry); + options.OnFallback, + context.Telemetry, + context.IsGenericBuilder); }, options); } diff --git a/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs b/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs index a846c52b1dd..2a2ed1454c8 100644 --- a/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs +++ b/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs @@ -49,13 +49,13 @@ internal void Initialize(ResilienceContext context) private bool ContinueOnCapturedContext => Snapshot.Context.ContinueOnCapturedContext; - public async ValueTask> LoadExecutionAsync( - Func>> primaryCallback, + public async ValueTask> LoadExecutionAsync( + Func>> primaryCallback, TState state) { if (LoadedTasks >= _maxAttempts) { - return CreateExecutionInfoWhenNoExecution(); + return CreateExecutionInfoWhenNoExecution(); } // determine what type of task we are creating @@ -72,12 +72,12 @@ public async ValueTask> LoadExecutionAsync(execution, true, null); + return new ExecutionInfo(execution, true, null); } else { _executionPool.Return(execution); - return CreateExecutionInfoWhenNoExecution(); + return CreateExecutionInfoWhenNoExecution(); } } diff --git a/src/Polly.Core/Hedging/Controller/HedgingHandler.cs b/src/Polly.Core/Hedging/Controller/HedgingHandler.cs index 5eb0be8c72a..0d9a54262af 100644 --- a/src/Polly.Core/Hedging/Controller/HedgingHandler.cs +++ b/src/Polly.Core/Hedging/Controller/HedgingHandler.cs @@ -1,17 +1,11 @@ namespace Polly.Hedging.Utils; internal sealed record class HedgingHandler( - PredicateInvoker ShouldHandle, + Func, ValueTask> ShouldHandle, Func, Func>>?> ActionGenerator, bool IsGeneric) { - public bool HandlesHedging() => IsGeneric switch - { - true => typeof(TResult) == typeof(T), - false => true - }; - - public Func>>? GenerateAction(HedgingActionGeneratorArguments args) + public Func>>? GenerateAction(HedgingActionGeneratorArguments args) { if (IsGeneric) { @@ -21,13 +15,13 @@ internal sealed record class HedgingHandler( args.Attempt, (Func>>)(object)args.Callback); - return (Func>>?)(object)ActionGenerator(copiedArgs)!; + return (Func>>?)(object)ActionGenerator(copiedArgs)!; } return CreateNonGenericAction(args); } - private Func>>? CreateNonGenericAction(HedgingActionGeneratorArguments args) + private Func>>? CreateNonGenericAction(HedgingActionGeneratorArguments args) { var generator = (Func, Func>>?>)(object)ActionGenerator; var action = generator(new HedgingActionGeneratorArguments(args.PrimaryContext, args.ActionContext, args.Attempt, async context => @@ -44,7 +38,7 @@ internal sealed record class HedgingHandler( return async () => { var outcome = await action().ConfigureAwait(args.ActionContext.ContinueOnCapturedContext); - return outcome.AsOutcome(); + return outcome.AsOutcome(); }; } } diff --git a/src/Polly.Core/Hedging/Controller/TaskExecution.cs b/src/Polly.Core/Hedging/Controller/TaskExecution.cs index 4aa7eaca3a8..862ad8bb8fb 100644 --- a/src/Polly.Core/Hedging/Controller/TaskExecution.cs +++ b/src/Polly.Core/Hedging/Controller/TaskExecution.cs @@ -88,10 +88,10 @@ public void Cancel() } } - public async ValueTask InitializeAsync( + public async ValueTask InitializeAsync( HedgedTaskType type, ContextSnapshot snapshot, - Func>> primaryCallback, + Func>> primaryCallback, TState state, int attempt) { @@ -110,7 +110,7 @@ public async ValueTask InitializeAsync( if (type == HedgedTaskType.Secondary) { - Func>>? action = null; + Func>>? action = null; try { @@ -124,7 +124,7 @@ public async ValueTask InitializeAsync( catch (Exception e) { _stopExecutionTimestamp = _timeProvider.GetTimestamp(); - ExecutionTaskSafe = ExecuteCreateActionException(e); + ExecutionTaskSafe = ExecuteCreateActionException(e); return true; } @@ -187,9 +187,9 @@ public async ValueTask ResetAsync() _stopExecutionTimestamp = 0; } - private async Task ExecuteSecondaryActionAsync(Func>> action) + private async Task ExecuteSecondaryActionAsync(Func>> action) { - Outcome outcome; + Outcome outcome; try { @@ -197,21 +197,21 @@ private async Task ExecuteSecondaryActionAsync(Func(e); + outcome = new Outcome(e); } _stopExecutionTimestamp = _timeProvider.GetTimestamp(); await UpdateOutcomeAsync(outcome).ConfigureAwait(Context.ContinueOnCapturedContext); } - private async Task ExecuteCreateActionException(Exception e) + private async Task ExecuteCreateActionException(Exception e) { - await UpdateOutcomeAsync(new Outcome(e)).ConfigureAwait(Context.ContinueOnCapturedContext); + await UpdateOutcomeAsync(new Outcome(e)).ConfigureAwait(Context.ContinueOnCapturedContext); } - private async Task ExecutePrimaryActionAsync(Func>> primaryCallback, TState state) + private async Task ExecutePrimaryActionAsync(Func>> primaryCallback, TState state) { - Outcome outcome; + Outcome outcome; try { @@ -219,18 +219,18 @@ private async Task ExecutePrimaryActionAsync(Func(e); + outcome = new Outcome(e); } _stopExecutionTimestamp = _timeProvider.GetTimestamp(); await UpdateOutcomeAsync(outcome).ConfigureAwait(Context.ContinueOnCapturedContext); } - private async Task UpdateOutcomeAsync(Outcome outcome) + private async Task UpdateOutcomeAsync(Outcome outcome) { - var args = new OutcomeArguments(Context, outcome, new HedgingPredicateArguments()); + var args = new OutcomeArguments(Context, outcome, new HedgingPredicateArguments()); Outcome = outcome.AsOutcome(); - IsHandled = await _handler.ShouldHandle.HandleAsync(args).ConfigureAwait(Context.ContinueOnCapturedContext); + IsHandled = await _handler.ShouldHandle(args).ConfigureAwait(Context.ContinueOnCapturedContext); TelemetryUtil.ReportExecutionAttempt(_telemetry, Context, outcome, Attempt, ExecutionTime, IsHandled); } diff --git a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs index 9a9e6dd6e41..2a0800bcab7 100644 --- a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs +++ b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs @@ -5,7 +5,7 @@ namespace Polly.Hedging; -internal sealed class HedgingResilienceStrategy : ResilienceStrategy +internal sealed class HedgingResilienceStrategy : OutcomeResilienceStrategy { private readonly TimeProvider _timeProvider; private readonly ResilienceStrategyTelemetry _telemetry; @@ -15,10 +15,12 @@ public HedgingResilienceStrategy( TimeSpan hedgingDelay, int maxHedgedAttempts, HedgingHandler hedgingHandler, - EventInvoker? onHedging, + Func, ValueTask>? onHedging, Func>? hedgingDelayGenerator, TimeProvider timeProvider, - ResilienceStrategyTelemetry telemetry) + ResilienceStrategyTelemetry telemetry, + bool isGeneric) + : base(isGeneric) { HedgingDelay = hedgingDelay; MaxHedgedAttempts = maxHedgedAttempts; @@ -39,19 +41,14 @@ public HedgingResilienceStrategy( public HedgingHandler HedgingHandler { get; } - public EventInvoker? OnHedging { get; } + public Func, ValueTask>? OnHedging { get; } [ExcludeFromCodeCoverage] // coverlet issue - protected internal override async ValueTask> ExecuteCoreAsync( - Func>> callback, + protected override async ValueTask> ExecuteCallbackAsync( + Func>> callback, ResilienceContext context, TState state) { - if (!HedgingHandler.HandlesHedging()) - { - return await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); - } - // create hedging execution context var hedgingContext = _controller.GetContext(context); @@ -65,9 +62,9 @@ protected internal override async ValueTask> ExecuteCoreAsync> ExecuteCoreAsync( + private async ValueTask> ExecuteCoreAsync( HedgingExecutionContext hedgingContext, - Func>> callback, + Func>> callback, ResilienceContext context, TState state) { @@ -80,12 +77,12 @@ private async ValueTask> ExecuteCoreAsync( var start = _timeProvider.GetTimestamp(); if (cancellationToken.IsCancellationRequested) { - return new Outcome(new OperationCanceledException(cancellationToken).TrySetStackTrace()); + return new Outcome(new OperationCanceledException(cancellationToken).TrySetStackTrace()); } var loadedExecution = await hedgingContext.LoadExecutionAsync(callback, state).ConfigureAwait(context.ContinueOnCapturedContext); - if (loadedExecution.Outcome is Outcome outcome) + if (loadedExecution.Outcome is Outcome outcome) { return outcome; } @@ -98,12 +95,12 @@ private async ValueTask> ExecuteCoreAsync( // We will create additional hedged task in the next iteration. await HandleOnHedgingAsync( context, - new Outcome(default(TResult)), + new Outcome(default(T)), new OnHedgingArguments(attempt, HasOutcome: false, ExecutionTime: delay)).ConfigureAwait(context.ContinueOnCapturedContext); continue; } - outcome = execution.Outcome.AsOutcome(); + outcome = execution.Outcome.AsOutcome(); if (!execution.IsHandled) { @@ -119,9 +116,9 @@ await HandleOnHedgingAsync( } } - private async ValueTask HandleOnHedgingAsync(ResilienceContext context, Outcome outcome, OnHedgingArguments args) + private async ValueTask HandleOnHedgingAsync(ResilienceContext context, Outcome outcome, OnHedgingArguments args) { - var onHedgingArgs = new OutcomeArguments( + var onHedgingArgs = new OutcomeArguments( context, outcome, args); @@ -133,7 +130,7 @@ private async ValueTask HandleOnHedgingAsync(ResilienceContext context, // If nothing has been returned or thrown yet, the result is a transient failure, // and other hedged request will be awaited. // Before it, one needs to perform the task adjacent to each hedged call. - await OnHedging.HandleAsync(onHedgingArgs).ConfigureAwait(context.ContinueOnCapturedContext); + await OnHedging(onHedgingArgs).ConfigureAwait(context.ContinueOnCapturedContext); } } diff --git a/src/Polly.Core/Hedging/HedgingResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Hedging/HedgingResilienceStrategyBuilderExtensions.cs index 4c43896da91..59587546475 100644 --- a/src/Polly.Core/Hedging/HedgingResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Hedging/HedgingResilienceStrategyBuilderExtensions.cs @@ -49,7 +49,7 @@ internal static void AddHedgingCore(this ResilienceStrategyBuilderBase builder.AddStrategy(context => { var handler = new HedgingHandler( - context.CreateInvoker(options.ShouldHandle)!, + options.ShouldHandle!, options.HedgingActionGenerator, context.IsGenericBuilder); @@ -57,10 +57,11 @@ internal static void AddHedgingCore(this ResilienceStrategyBuilderBase options.HedgingDelay, options.MaxHedgedAttempts, handler, - context.CreateInvoker(options.OnHedging), + options.OnHedging, options.HedgingDelayGenerator, context.TimeProvider, - context.Telemetry); + context.Telemetry, + context.IsGenericBuilder); }, options); } diff --git a/src/Polly.Core/Outcome.cs b/src/Polly.Core/Outcome.cs index f48006c565d..b81f3ce87a5 100644 --- a/src/Polly.Core/Outcome.cs +++ b/src/Polly.Core/Outcome.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1815 // Override equals and operator equals on value types using System; +using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; namespace Polly; @@ -105,7 +106,25 @@ internal TResult GetResultOrRethrow() internal Outcome AsOutcome() => AsOutcome(); - internal Outcome AsOutcome() => ExceptionDispatchInfo != null - ? new Outcome(ExceptionDispatchInfo) - : new Outcome((T)(object)Result!); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Outcome AsOutcome() + { + if (ExceptionDispatchInfo is not null) + { + return new Outcome(ExceptionDispatchInfo); + } + + if (Result is null) + { + return new Outcome(default(T)); + } + + if (typeof(T) == typeof(TResult)) + { + var result = Result; + return new Outcome(Unsafe.As(ref result)); + } + + return new Outcome((T)(object)Result); + } } diff --git a/src/Polly.Core/Polly.Core.csproj b/src/Polly.Core/Polly.Core.csproj index c9747fcc2c7..7943580f009 100644 --- a/src/Polly.Core/Polly.Core.csproj +++ b/src/Polly.Core/Polly.Core.csproj @@ -1,7 +1,7 @@  - net7.0;net6.0;netstandard2.0;net472;net462 + net7.0;net6.0;netstandard2.0;net472 Polly.Core Polly enable diff --git a/src/Polly.Core/ResilienceStrategyBuilderContext.cs b/src/Polly.Core/ResilienceStrategyBuilderContext.cs index 5f58df92d61..aff0987f78d 100644 --- a/src/Polly.Core/ResilienceStrategyBuilderContext.cs +++ b/src/Polly.Core/ResilienceStrategyBuilderContext.cs @@ -56,21 +56,4 @@ internal ResilienceStrategyBuilderContext( internal TimeProvider TimeProvider { get; } internal bool IsGenericBuilder { get; } - - internal PredicateInvoker? CreateInvoker(Func, ValueTask>? predicate) - { - return PredicateInvoker.Create(predicate, IsGenericBuilder); - } - - internal EventInvoker? CreateInvoker(Func, ValueTask>? callback) - { - return EventInvoker.Create(callback, IsGenericBuilder); - } - - internal GeneratorInvoker? CreateInvoker( - Func, ValueTask>? generator, - TValue defaultValue) - { - return GeneratorInvoker.Create(generator, defaultValue, IsGenericBuilder); - } } diff --git a/src/Polly.Core/Retry/RetryResilienceStrategy.cs b/src/Polly.Core/Retry/RetryResilienceStrategy.cs index 418c1b73020..092f3db58d0 100644 --- a/src/Polly.Core/Retry/RetryResilienceStrategy.cs +++ b/src/Polly.Core/Retry/RetryResilienceStrategy.cs @@ -1,37 +1,32 @@ using Polly.Telemetry; +using Polly.Utils; namespace Polly.Retry; -#pragma warning disable S107 // Methods should not have too many parameters - -internal sealed class RetryResilienceStrategy : ResilienceStrategy +internal sealed class RetryResilienceStrategy : OutcomeResilienceStrategy { private readonly TimeProvider _timeProvider; private readonly ResilienceStrategyTelemetry _telemetry; private readonly RandomUtil _randomUtil; public RetryResilienceStrategy( - TimeSpan baseDelay, - RetryBackoffType backoffType, - int retryCount, - PredicateInvoker shouldRetry, - EventInvoker? onRetry, - GeneratorInvoker? delayGenerator, + RetryStrategyOptions options, + bool isGeneric, TimeProvider timeProvider, ResilienceStrategyTelemetry telemetry, RandomUtil randomUtil) + : base(isGeneric) { - BaseDelay = baseDelay; - BackoffType = backoffType; - RetryCount = retryCount; - ShouldRetry = shouldRetry; - OnRetry = onRetry; - DelayGenerator = delayGenerator; + ShouldHandle = options.ShouldHandle; + BaseDelay = options.BaseDelay; + BackoffType = options.BackoffType; + RetryCount = options.RetryCount; + OnRetry = options.OnRetry; + DelayGenerator = options.RetryDelayGenerator; _timeProvider = timeProvider; _telemetry = telemetry; _randomUtil = randomUtil; - } public TimeSpan BaseDelay { get; } @@ -40,16 +35,13 @@ public RetryResilienceStrategy( public int RetryCount { get; } - public PredicateInvoker ShouldRetry { get; } + public Func, ValueTask> ShouldHandle { get; } - public GeneratorInvoker? DelayGenerator { get; } + public Func, ValueTask>? DelayGenerator { get; } - public EventInvoker? OnRetry { get; } + public Func, ValueTask>? OnRetry { get; } - protected internal override async ValueTask> ExecuteCoreAsync( - Func>> callback, - ResilienceContext context, - TState state) + protected override async ValueTask> ExecuteCallbackAsync(Func>> callback, ResilienceContext context, TState state) { double retryState = 0; @@ -59,8 +51,8 @@ protected internal override async ValueTask> ExecuteCoreAsync(context, outcome, new RetryPredicateArguments(attempt)); - var handle = await ShouldRetry.HandleAsync(shouldRetryArgs).ConfigureAwait(context.ContinueOnCapturedContext); + var shouldRetryArgs = new OutcomeArguments(context, outcome, new RetryPredicateArguments(attempt)); + var handle = await ShouldHandle(shouldRetryArgs).ConfigureAwait(context.ContinueOnCapturedContext); var executionTime = _timeProvider.GetElapsedTime(startTimestamp); TelemetryUtil.ReportExecutionAttempt(_telemetry, context, outcome, attempt, executionTime, handle); @@ -73,20 +65,20 @@ protected internal override async ValueTask> ExecuteCoreAsync(context, outcome, new RetryDelayArguments(attempt, delay)); - var newDelay = await DelayGenerator.HandleAsync(delayArgs).ConfigureAwait(false); + var delayArgs = new OutcomeArguments(context, outcome, new RetryDelayArguments(attempt, delay)); + var newDelay = await DelayGenerator(delayArgs).ConfigureAwait(false); if (RetryHelper.IsValidDelay(newDelay)) { delay = newDelay; } } - var onRetryArgs = new OutcomeArguments(context, outcome, new OnRetryArguments(attempt, delay, executionTime)); + var onRetryArgs = new OutcomeArguments(context, outcome, new OnRetryArguments(attempt, delay, executionTime)); _telemetry.Report(RetryConstants.OnRetryEvent, onRetryArgs); if (OnRetry is not null) { - await OnRetry.HandleAsync(onRetryArgs).ConfigureAwait(context.ContinueOnCapturedContext); + await OnRetry(onRetryArgs).ConfigureAwait(context.ContinueOnCapturedContext); } if (outcome.TryGetResult(out var resultValue)) @@ -102,7 +94,7 @@ protected internal override async ValueTask> ExecuteCoreAsync(e); + return new Outcome(e); } } diff --git a/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs index f4b16215ec5..7902e775ca5 100644 --- a/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs @@ -79,13 +79,9 @@ private static TBuilder AddRetryCore(this TBuilder builder, R where TBuilder : ResilienceStrategyBuilderBase { return builder.AddStrategy(context => - new RetryResilienceStrategy( - options.BaseDelay, - options.BackoffType, - options.RetryCount, - context.CreateInvoker(options.ShouldHandle)!, - context.CreateInvoker(options.OnRetry), - context.CreateInvoker(options.RetryDelayGenerator, TimeSpan.MinValue), + new RetryResilienceStrategy( + options, + context.IsGenericBuilder, context.TimeProvider, context.Telemetry, RandomUtil.Instance), diff --git a/src/Polly.Core/Utils/EventInvoker.cs b/src/Polly.Core/Utils/EventInvoker.cs deleted file mode 100644 index 1b82166c9d5..00000000000 --- a/src/Polly.Core/Utils/EventInvoker.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Polly.Utils; - -internal abstract class EventInvoker -{ - public static EventInvoker? Create(Func, ValueTask>? callback, bool isGeneric) => callback switch - { - Func, ValueTask> generic when !isGeneric => new NonGenericEventInvoker(generic), - { } => new GenericEventInvoker(callback), - _ => null, - }; - - public abstract ValueTask HandleAsync(OutcomeArguments args); - - private sealed class NonGenericEventInvoker : EventInvoker - { - private readonly Func, ValueTask> _callback; - - public NonGenericEventInvoker(Func, ValueTask> callback) => _callback = callback; - - public override ValueTask HandleAsync(OutcomeArguments args) => _callback(args.AsObjectArguments()); - } - - private sealed class GenericEventInvoker : EventInvoker - { - private readonly object _callback; - - public GenericEventInvoker(Func, ValueTask> callback) => _callback = callback; - - public override ValueTask HandleAsync(OutcomeArguments args) - { - if (typeof(TResult) == typeof(T)) - { - return ((Func, ValueTask>)_callback)(args); - } - - return default; - } - } -} diff --git a/src/Polly.Core/Utils/GeneratorInvoker.cs b/src/Polly.Core/Utils/GeneratorInvoker.cs deleted file mode 100644 index c7a6b593b52..00000000000 --- a/src/Polly.Core/Utils/GeneratorInvoker.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Polly.Utils; - -internal abstract class GeneratorInvoker -{ - public static GeneratorInvoker? Create( - Func, ValueTask>? generator, - TValue defaultValue, - bool isGeneric) => generator switch - { - Func, ValueTask> objectGenerator when !isGeneric => new NonGenericGeneratorInvoker(objectGenerator), - { } => new GenericGeneratorInvoker(generator, defaultValue), - _ => null - }; - - public abstract ValueTask HandleAsync(OutcomeArguments args); - - private sealed class NonGenericGeneratorInvoker : GeneratorInvoker - { - private readonly Func, ValueTask> _generator; - - public NonGenericGeneratorInvoker(Func, ValueTask> generator) => _generator = generator; - - public override ValueTask HandleAsync(OutcomeArguments args) => _generator(args.AsObjectArguments()); - } - - private sealed class GenericGeneratorInvoker : GeneratorInvoker - { - private readonly object _generator; - private readonly TValue _defaultValue; - - public GenericGeneratorInvoker(Func, ValueTask> generator, TValue defaultValue) - { - _generator = generator; - _defaultValue = defaultValue; - } - - public override ValueTask HandleAsync(OutcomeArguments args) - { - if (typeof(TResult) == typeof(T)) - { - return ((Func, ValueTask>)_generator)(args); - } - - return new(_defaultValue); - } - } -} diff --git a/src/Polly.Core/Utils/OutcomeResilienceStrategy.cs b/src/Polly.Core/Utils/OutcomeResilienceStrategy.cs new file mode 100644 index 00000000000..7d73c7ae77d --- /dev/null +++ b/src/Polly.Core/Utils/OutcomeResilienceStrategy.cs @@ -0,0 +1,82 @@ +using System.Runtime.CompilerServices; + +namespace Polly.Utils; + +/// +/// This base strategy class is used to simplify the implementation of generic (reactive) +/// strategies by limiting the number of generic types this strategy receives. +/// +/// The type of result this strategy handles. +/// +/// For strategies that handle all result types the generic parameter must be of type . +/// +internal abstract class OutcomeResilienceStrategy : ResilienceStrategy +{ + private readonly bool _isGeneric; + + protected OutcomeResilienceStrategy(bool isGeneric) + { + if (!isGeneric && typeof(T) != typeof(object)) + { + throw new NotSupportedException("For non-generic strategies the generic paramater should be 'object' type."); + } + + _isGeneric = isGeneric; + } + + protected internal sealed override ValueTask> ExecuteCoreAsync( + Func>> callback, + ResilienceContext context, + TState state) + { + if (_isGeneric) + { + if (typeof(TResult) != typeof(T)) + { + return callback(context, state); + } + + // cast is safe here, because TResult and T are the same type + var callbackCasted = (Func>>)(object)callback; + var valueTask = ExecuteCallbackAsync(callbackCasted, context, state); + + return ConvertValueTask(valueTask, context); + } + else + { + var valueTask = ExecuteCallbackAsync( + static async (context, state) => + { + var outcome = await state.callback(context, state.state).ConfigureAwait(context.ContinueOnCapturedContext); + + // cast the outcome to "object" based one (T) + return outcome.AsOutcome(); + }, + context, + (callback, state)); + + return ConvertValueTask(valueTask, context); + } + } + + protected abstract ValueTask> ExecuteCallbackAsync( + Func>> callback, + ResilienceContext context, + TState state); + + private static ValueTask> ConvertValueTask(ValueTask> valueTask, ResilienceContext resilienceContext) + { + if (valueTask.IsCompletedSuccessfully) + { + return new ValueTask>(valueTask.Result.AsOutcome()); + } + + return ConvertValueTaskAsync(valueTask, resilienceContext); + + static async ValueTask> ConvertValueTaskAsync(ValueTask> valueTask, ResilienceContext resilienceContext) + { + var outcome = await valueTask.ConfigureAwait(resilienceContext.ContinueOnCapturedContext); + return outcome.AsOutcome(); + } + } +} diff --git a/src/Polly.Core/Utils/PredicateInvoker.cs b/src/Polly.Core/Utils/PredicateInvoker.cs deleted file mode 100644 index 50ef298bed7..00000000000 --- a/src/Polly.Core/Utils/PredicateInvoker.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Polly.Utils; - -internal abstract class PredicateInvoker -{ - public static PredicateInvoker? Create(Func, ValueTask>? predicate, bool isGeneric) => predicate switch - { - Func, ValueTask> objectPredicate when !isGeneric => new NonGenericPredicateInvoker(objectPredicate), - { } => new GenericPredicateInvoker(predicate), - _ => null, - }; - - public abstract ValueTask HandleAsync(OutcomeArguments args); - - private sealed class NonGenericPredicateInvoker : PredicateInvoker - { - private readonly Func, ValueTask> _predicate; - - public NonGenericPredicateInvoker(Func, ValueTask> predicate) => _predicate = predicate; - - public override ValueTask HandleAsync(OutcomeArguments args) => _predicate(args.AsObjectArguments()); - } - - private sealed class GenericPredicateInvoker : PredicateInvoker - { - private readonly object _predicate; - - public GenericPredicateInvoker(Func, ValueTask> predicate) => _predicate = predicate; - - public override ValueTask HandleAsync(OutcomeArguments args) - { - if (typeof(TResult) == typeof(T)) - { - return ((Func, ValueTask>)_predicate)(args); - } - - return PredicateResult.False; - } - } -} diff --git a/src/Polly.Extensions/Polly.Extensions.csproj b/src/Polly.Extensions/Polly.Extensions.csproj index 922cb3ab629..f9a0e6bb8c3 100644 --- a/src/Polly.Extensions/Polly.Extensions.csproj +++ b/src/Polly.Extensions/Polly.Extensions.csproj @@ -1,6 +1,6 @@  - net7.0;net6.0;netstandard2.0;net472;net462 + net7.0;net6.0;netstandard2.0;net472 Polly.Extensions Polly.Extensions enable diff --git a/src/Polly.RateLimiting/Polly.RateLimiting.csproj b/src/Polly.RateLimiting/Polly.RateLimiting.csproj index c4165fe48eb..1f2f42418d0 100644 --- a/src/Polly.RateLimiting/Polly.RateLimiting.csproj +++ b/src/Polly.RateLimiting/Polly.RateLimiting.csproj @@ -1,6 +1,6 @@  - net7.0;net6.0;netstandard2.0;net472;net462 + net7.0;net6.0;netstandard2.0;net472 Polly.RateLimiting Polly.RateLimiting enable diff --git a/src/Polly/Polly.csproj b/src/Polly/Polly.csproj index 74ddd16d483..13d17a1d56f 100644 --- a/src/Polly/Polly.csproj +++ b/src/Polly/Polly.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net472;net462; + netstandard2.0;net472; Polly Library 70 diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs index ef77a83a16b..f202eb96c5c 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderTests.cs @@ -39,7 +39,7 @@ public void AddCircuitBreaker_Configure(Action builde var strategy = builder.Build(); - strategy.Should().BeOfType(); + strategy.Should().BeOfType>(); } [MemberData(nameof(ConfigureDataGeneric))] @@ -52,7 +52,7 @@ public void AddCircuitBreaker_Generic_Configure(Action(); + strategy.Should().BeOfType>(); } [Fact] @@ -165,12 +165,12 @@ public void AddAdvancedCircuitBreaker_IntegrationTest() opened.Should().Be(1); halfOpened.Should().Be(0); closed.Should().Be(0); - Assert.Throws>(() => strategy.Execute(_ => 0)); + Assert.Throws>(() => strategy.Execute(_ => 0)); // Circuit Half Opened time += options.BreakDuration; strategy.Execute(_ => -1); - Assert.Throws>(() => strategy.Execute(_ => 0)); + Assert.Throws>(() => strategy.Execute(_ => 0)); opened.Should().Be(2); halfOpened.Should().Be(1); closed.Should().Be(0); diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs index 3df4b07576d..a33961d4bed 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs @@ -11,7 +11,7 @@ public class CircuitBreakerResilienceStrategyTests : IDisposable private readonly Mock _behavior; private readonly ResilienceStrategyTelemetry _telemetry; private readonly SimpleCircuitBreakerStrategyOptions _options; - private readonly CircuitStateController _controller; + private readonly CircuitStateController _controller; public CircuitBreakerResilienceStrategyTests() { @@ -20,7 +20,7 @@ public CircuitBreakerResilienceStrategyTests() _behavior = new Mock(MockBehavior.Strict); _telemetry = TestUtilities.CreateResilienceTelemetry(Mock.Of()); _options = new SimpleCircuitBreakerStrategyOptions(); - _controller = new CircuitStateController( + _controller = new CircuitStateController( CircuitBreakerConstants.DefaultBreakDuration, null, null, @@ -133,8 +133,8 @@ public void Execute_Ok() Create().Invoking(s => s.Execute(_ => { })).Should().NotThrow(); } - private CircuitBreakerResilienceStrategy Create() + private CircuitBreakerResilienceStrategy Create() { - return new(PredicateInvoker.Create(_options.ShouldHandle!, false)!, _controller, _options.StateProvider, _options.ManualControl); + return new(_options.ShouldHandle!, _controller, _options.StateProvider, _options.ManualControl, true); } } diff --git a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs index 6b99fbf40ef..7962d504e8d 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs @@ -7,7 +7,7 @@ namespace Polly.Core.Tests.CircuitBreaker.Controller; public class CircuitStateControllerTests { private readonly FakeTimeProvider _timeProvider = new(); - private readonly CircuitBreakerStrategyOptions _options = new SimpleCircuitBreakerStrategyOptions(); + private readonly CircuitBreakerStrategyOptions _options = new SimpleCircuitBreakerStrategyOptions(); private readonly Mock _circuitBehavior = new(MockBehavior.Strict); private readonly Action _onTelemetry = _ => { }; private DateTimeOffset _utcNow = DateTimeOffset.UtcNow; @@ -51,13 +51,13 @@ public async Task IsolateAsync_Ok() controller.CircuitState.Should().Be(CircuitState.Isolated); called.Should().BeTrue(); - var outcome = await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); + var outcome = await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); outcome.Value.Exception.Should().BeOfType(); // now close it _circuitBehavior.Setup(v => v.OnCircuitClosed()); await controller.CloseCircuitAsync(ResilienceContext.Get()); - await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); + await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); context.ResilienceEvents.Should().Contain(new ResilienceEvent("OnCircuitOpened")); } @@ -88,7 +88,7 @@ public async Task BreakAsync_Ok() // assert called.Should().BeTrue(); - await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); + await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); _circuitBehavior.VerifyAll(); context.ResilienceEvents.Should().Contain(new ResilienceEvent("OnCircuitClosed")); } @@ -105,7 +105,7 @@ public async Task Disposed_EnsureThrows() await Assert.ThrowsAsync(async () => await controller.CloseCircuitAsync(ResilienceContext.Get())); await Assert.ThrowsAsync(async () => await controller.IsolateCircuitAsync(ResilienceContext.Get())); - await Assert.ThrowsAsync(async () => await controller.OnActionPreExecuteAsync(ResilienceContext.Get())); + await Assert.ThrowsAsync(async () => await controller.OnActionPreExecuteAsync(ResilienceContext.Get())); await Assert.ThrowsAsync(async () => await controller.OnActionSuccessAsync(new Outcome(10), ResilienceContext.Get())); await Assert.ThrowsAsync(async () => await controller.OnActionFailureAsync(new Outcome(10), ResilienceContext.Get())); } @@ -116,7 +116,7 @@ public async Task OnActionPreExecute_CircuitOpenedByValue() using var controller = CreateController(); await OpenCircuit(controller, new Outcome(99)); - var error = (BrokenCircuitException)(await controller.OnActionPreExecuteAsync(ResilienceContext.Get())).Value.Exception!; + var error = (BrokenCircuitException)(await controller.OnActionPreExecuteAsync(ResilienceContext.Get())).Value.Exception!; error.Should().BeOfType>(); error.Result.Should().Be(99); @@ -164,7 +164,7 @@ public async Task OnActionPreExecute_CircuitOpenedByException() using var controller = CreateController(); await OpenCircuit(controller, new Outcome(new InvalidOperationException())); - var error = (BrokenCircuitException)(await controller.OnActionPreExecuteAsync(ResilienceContext.Get())).Value.Exception!; + var error = (BrokenCircuitException)(await controller.OnActionPreExecuteAsync(ResilienceContext.Get())).Value.Exception!; error.InnerException.Should().BeOfType(); } @@ -214,8 +214,8 @@ public async Task OnActionPreExecute_HalfOpen() AdvanceTime(_options.BreakDuration); // act - await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); - var error = (await controller.OnActionPreExecuteAsync(ResilienceContext.Get())).Value.Exception; + await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); + var error = (await controller.OnActionPreExecuteAsync(ResilienceContext.Get())).Value.Exception; error.Should().BeOfType>(); // assert @@ -291,10 +291,10 @@ public async Task OnActionFailureAsync_EnsureCorrectBehavior(CircuitState state, _circuitBehavior.Setup(v => v.OnActionFailure(state, out shouldBreak)); // act - await controller.OnActionFailureAsync(new Outcome("dummy"), ResilienceContext.Get()); + await controller.OnActionFailureAsync(new Outcome(99), ResilienceContext.Get()); // assert - controller.LastHandledOutcome!.Value.Result.Should().Be("dummy"); + controller.LastHandledOutcome!.Value.Result.Should().Be(99); controller.CircuitState.Should().Be(expectedState); _circuitBehavior.VerifyAll(); @@ -322,7 +322,7 @@ public async Task OnActionFailureAsync_EnsureBreakDurationNotOverflow(bool overf _circuitBehavior.Setup(v => v.OnActionFailure(CircuitState.HalfOpen, out shouldBreak)); // act - await controller.OnActionFailureAsync(new Outcome("dummy"), ResilienceContext.Get()); + await controller.OnActionFailureAsync(new Outcome(99), ResilienceContext.Get()); // assert var blockedTill = GetBlockedTill(controller); @@ -347,11 +347,11 @@ public async Task OnActionFailureAsync_VoidResult_EnsureBreakingExceptionNotSet( _circuitBehavior.Setup(v => v.OnActionFailure(CircuitState.Open, out shouldBreak)); // act - await controller.OnActionFailureAsync(new Outcome(VoidResult.Instance), ResilienceContext.Get()); + await controller.OnActionFailureAsync(new Outcome(99), ResilienceContext.Get()); // assert controller.LastException.Should().BeNull(); - var outcome = await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); + var outcome = await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); outcome.Value.Exception.Should().BeOfType(); } @@ -383,12 +383,12 @@ public async Task Flow_Closed_HalfOpen_Open_HalfOpen_Closed() // execution rejected AdvanceTime(TimeSpan.FromMilliseconds(1)); - var outcome = await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); + var outcome = await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); outcome.Value.Exception.Should().BeOfType>(); // wait and try, transition to half open AdvanceTime(_options.BreakDuration + _options.BreakDuration); - await controller.OnActionPreExecuteAsync(context); + await controller.OnActionPreExecuteAsync(context); controller.CircuitState.Should().Be(CircuitState.HalfOpen); // close circuit @@ -398,10 +398,10 @@ public async Task Flow_Closed_HalfOpen_Open_HalfOpen_Closed() controller.CircuitState.Should().Be(CircuitState.Closed); } - private static DateTimeOffset? GetBlockedTill(CircuitStateController controller) => + private static DateTimeOffset? GetBlockedTill(CircuitStateController controller) => (DateTimeOffset?)controller.GetType().GetField("_blockedUntil", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(controller)!; - private async Task TransitionToState(CircuitStateController controller, CircuitState state) + private async Task TransitionToState(CircuitStateController controller, CircuitState state) { switch (state) { @@ -413,7 +413,7 @@ private async Task TransitionToState(CircuitStateController controller, CircuitS case CircuitState.HalfOpen: await OpenCircuit(controller); AdvanceTime(_options.BreakDuration); - await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); + await controller.OnActionPreExecuteAsync(ResilienceContext.Get()); break; case CircuitState.Isolated: await controller.IsolateCircuitAsync(ResilienceContext.Get()); @@ -423,7 +423,7 @@ private async Task TransitionToState(CircuitStateController controller, CircuitS controller.CircuitState.Should().Be(state); } - private async Task OpenCircuit(CircuitStateController controller, Outcome? outcome = null) + private async Task OpenCircuit(CircuitStateController controller, Outcome? outcome = null) { bool breakCircuit = true; _circuitBehavior.Setup(v => v.OnActionFailure(CircuitState.Closed, out breakCircuit)); @@ -432,10 +432,10 @@ private async Task OpenCircuit(CircuitStateController controller, Outcome? private void AdvanceTime(TimeSpan timespan) => _utcNow += timespan; - private CircuitStateController CreateController() => new( + private CircuitStateController CreateController() => new( _options.BreakDuration, - EventInvoker.Create(_options.OnOpened, false), - EventInvoker.Create(_options.OnClosed, false), + _options.OnOpened, + _options.OnClosed, _options.OnHalfOpened, _circuitBehavior.Object, _timeProvider.Object, diff --git a/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs b/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs index d60f7050984..1c377065132 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/SimpleCircuitBreakerOptionsTests.cs @@ -34,9 +34,9 @@ public async Task ShouldHandle_EnsureDefaults() var args = new CircuitBreakerPredicateArguments(); var context = ResilienceContext.Get(); - (await options.ShouldHandle(new(context, new Outcome("dummy"), args))).Should().Be(false); - (await options.ShouldHandle(new(context, new Outcome(new OperationCanceledException()), args))).Should().Be(false); - (await options.ShouldHandle(new(context, new Outcome(new InvalidOperationException()), args))).Should().Be(true); + (await options.ShouldHandle(new(context, new Outcome(0), args))).Should().Be(false); + (await options.ShouldHandle(new(context, new Outcome(new OperationCanceledException()), args))).Should().Be(false); + (await options.ShouldHandle(new(context, new Outcome(new InvalidOperationException()), args))).Should().Be(true); } [Fact] diff --git a/test/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs b/test/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs index 7fea4fd2730..246974b537e 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs @@ -3,26 +3,6 @@ namespace Polly.Core.Tests.Fallback; public class FallbackHandlerTests { - [Fact] - public void HandlesFallback_Generic_Ok() - { - var handler = FallbackHelper.CreateHandler(_ => true, () => "secondary".AsOutcome(), true); - - handler.HandlesFallback().Should().Be(true); - handler.HandlesFallback().Should().Be(false); - handler.HandlesFallback().Should().Be(false); - } - - [Fact] - public void HandlesFallback_NonGeneric_Ok() - { - var handler = FallbackHelper.CreateHandler(_ => true, () => "secondary".AsOutcome(), false); - - handler.HandlesFallback().Should().Be(true); - handler.HandlesFallback().Should().Be(true); - handler.HandlesFallback().Should().Be(true); - } - [Fact] public async Task GenerateAction_Generic_Ok() { diff --git a/test/Polly.Core.Tests/Fallback/FallbackHelper.cs b/test/Polly.Core.Tests/Fallback/FallbackHelper.cs index 2ab9383cac5..332b0b02b50 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackHelper.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackHelper.cs @@ -1,5 +1,4 @@ using Polly.Fallback; -using Polly.Utils; namespace Polly.Core.Tests.Fallback; @@ -11,7 +10,7 @@ public static FallbackHandler CreateHandler( bool isGeneric = true) { return new FallbackHandler( - PredicateInvoker.Create(args => new ValueTask(shouldHandle(args.Outcome!)), true)!, + args => new ValueTask(shouldHandle(args.Outcome)), _ => fallback().AsValueTask(), isGeneric); } diff --git a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs index d4477c051e5..fb50f634b9a 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs @@ -1,12 +1,11 @@ using Polly.Fallback; using Polly.Telemetry; -using Polly.Utils; namespace Polly.Core.Tests.Fallback; public class FallbackResilienceStrategyTests { - private readonly FallbackStrategyOptions _options = new(); + private readonly FallbackStrategyOptions _options = new(); private readonly List _args = new(); private readonly ResilienceStrategyTelemetry _telemetry; private FallbackHandler? _handler; @@ -103,6 +102,7 @@ private void SetHandler( private FallbackResilienceStrategy Create() => new( _handler!, - EventInvoker.Create(_options.OnFallback, false), - _telemetry); + _options.OnFallback, + _telemetry, + true); } diff --git a/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs b/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs index c279c17d35f..8668936b967 100644 --- a/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs +++ b/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs @@ -93,13 +93,13 @@ public async Task TryWaitForCompletedExecutionAsync_FinishedTask_Ok() { var context = Create(); context.Initialize(_resilienceContext); - await context.LoadExecutionAsync((_, _) => new Outcome("dummy").AsValueTask(), "state"); + await context.LoadExecutionAsync((_, _) => new DisposableResult("dummy").AsOutcomeAsync(), "state"); var task = await context.TryWaitForCompletedExecutionAsync(TimeSpan.Zero); task.Should().NotBeNull(); task!.ExecutionTaskSafe!.IsCompleted.Should().BeTrue(); - task.Outcome.Result.Should().Be("dummy"); + task.Outcome.AsOutcome().Result!.Name.Should().Be("dummy"); task.AcceptOutcome(); context.LoadedTasks.Should().Be(1); } diff --git a/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs b/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs index a0ddc532e15..e5232821cd9 100644 --- a/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs +++ b/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs @@ -71,7 +71,7 @@ await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot, public async Task Initialize_PrimaryCallbackThrows_EnsureExceptionHandled() { var execution = Create(); - await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot, + await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot, (_, _) => throw new InvalidOperationException(), "dummy-state", 1); @@ -94,7 +94,7 @@ public async Task Initialize_Secondary_Ok(string value, bool handled) return () => new DisposableResult { Name = value }.AsOutcomeAsync(); }; - (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); + (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); await execution.ExecutionTaskSafe!; @@ -109,7 +109,7 @@ public async Task Initialize_SecondaryWhenTaskGeneratorReturnsNull_Ok() var execution = Create(); Generator = args => null; - (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeFalse(); + (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeFalse(); execution.Invoking(e => e.Context).Should().Throw(); } @@ -146,7 +146,7 @@ public async Task Initialize_SecondaryWhenTaskGeneratorThrows_EnsureOutcome() var execution = Create(); Generator = args => throw new FormatException(); - (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); + (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); await execution.ExecutionTaskSafe!; execution.Outcome.Exception.Should().BeOfType(); @@ -158,7 +158,7 @@ public async Task Initialize_ExecutionTaskDoesNotThrows() var execution = Create(); Generator = args => throw new FormatException(); - (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); + (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); await execution.ExecutionTaskSafe!.Invoking(async t => await t).Should().NotThrowAsync(); } diff --git a/test/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs b/test/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs index b24be55c7de..ce75d09e56b 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs @@ -1,7 +1,6 @@ using FluentAssertions; using Polly.Hedging; using Polly.Hedging.Utils; -using Polly.Utils; namespace Polly.Core.Tests.Hedging; @@ -11,7 +10,7 @@ public class HedgingHandlerTests public async Task GenerateAction_Generic_Ok() { var handler = new HedgingHandler( - PredicateInvoker.Create(args => PredicateResult.True, true)!, + args => PredicateResult.True, args => () => "ok".AsOutcomeAsync(), true); @@ -27,7 +26,7 @@ public async Task GenerateAction_Generic_Ok() public async Task GenerateAction_NonGeneric_Ok(bool nullAction) { var handler = new HedgingHandler( - PredicateInvoker.Create(args => PredicateResult.True, false)!, + args => PredicateResult.True, args => { if (nullAction) @@ -39,7 +38,7 @@ public async Task GenerateAction_NonGeneric_Ok(bool nullAction) }, false); - var action = handler.GenerateAction(new HedgingActionGeneratorArguments(ResilienceContext.Get(), ResilienceContext.Get(), 0, _ => "primary".AsOutcomeAsync()))!; + var action = handler.GenerateAction(new HedgingActionGeneratorArguments(ResilienceContext.Get(), ResilienceContext.Get(), 0, _ => ((object)"primary").AsOutcomeAsync()))!; if (nullAction) { action.Should().BeNull(); @@ -55,11 +54,11 @@ public async Task GenerateAction_NonGeneric_Ok(bool nullAction) public async Task GenerateAction_NonGeneric_FromCallback() { var handler = new HedgingHandler( - PredicateInvoker.Create(args => PredicateResult.True, false)!, + args => PredicateResult.True, args => () => args.Callback(args.ActionContext), false); - var action = handler.GenerateAction(new HedgingActionGeneratorArguments(ResilienceContext.Get(), ResilienceContext.Get(), 0, _ => "callback".AsOutcomeAsync()))!; + var action = handler.GenerateAction(new HedgingActionGeneratorArguments(ResilienceContext.Get(), ResilienceContext.Get(), 0, _ => ((object)"callback").AsOutcomeAsync()))!; var res = await action(); res.Result.Should().Be("callback"); } diff --git a/test/Polly.Core.Tests/Hedging/HedgingHelper.cs b/test/Polly.Core.Tests/Hedging/HedgingHelper.cs index 75950b0f965..81eb49833de 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingHelper.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingHelper.cs @@ -1,6 +1,5 @@ using Polly.Hedging; using Polly.Hedging.Utils; -using Polly.Utils; namespace Polly.Core.Tests.Hedging; @@ -11,7 +10,7 @@ public static HedgingHandler CreateHandler( Func, Func>>?> generator) { return new HedgingHandler( - PredicateInvoker.Create(args => new ValueTask(shouldHandle(args.Outcome!)), true)!, + args => new ValueTask(shouldHandle(args.Outcome!))!, generator, true); } diff --git a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index f88df19454a..461ffc35929 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -2,7 +2,6 @@ using Polly.Hedging.Utils; using Polly.Telemetry; using Polly.TestUtils; -using Polly.Utils; using Xunit.Abstractions; namespace Polly.Core.Tests.Hedging; @@ -16,7 +15,7 @@ public class HedgingResilienceStrategyTests : IDisposable private static readonly TimeSpan LongDelay = TimeSpan.FromDays(1); private static readonly TimeSpan AssertTimeout = TimeSpan.FromSeconds(15); - private readonly HedgingStrategyOptions _options = new(); + private readonly HedgingStrategyOptions _options = new(); private readonly List _events = new(); private readonly ResilienceStrategyTelemetry _telemetry; private readonly HedgingTimeProvider _timeProvider; @@ -313,7 +312,7 @@ public async Task ExecuteAsync_EnsureDiscardedResultDisposed() }; }); - var strategy = Create(handler); + var strategy = Create(handler, null); // act var resultTask = strategy.ExecuteAsync(async token => @@ -937,14 +936,17 @@ private void ConfigureHedging(TimeSpan delay) => ConfigureHedging(args => async return "secondary".AsOutcome(); }); - private HedgingResilienceStrategy Create() => Create(_handler!); + private HedgingResilienceStrategy Create() => Create(_handler!, _options.OnHedging); - private HedgingResilienceStrategy Create(HedgingHandler handler) => new( + private HedgingResilienceStrategy Create( + HedgingHandler handler, + Func, ValueTask>? onHedging) => new( _options.HedgingDelay, _options.MaxHedgedAttempts, handler, - EventInvoker.Create(_options.OnHedging, false), + onHedging, _options.HedgingDelayGenerator, _timeProvider, - _telemetry); + _telemetry, + true); } diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs index 1721de8383e..221324e0fee 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs @@ -73,9 +73,9 @@ public void AddRetry_DefaultOptions_Ok() AssertStrategy(builder, options.BackoffType, options.RetryCount, options.BaseDelay); } - private static void AssertStrategy(ResilienceStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay, Action? assert = null) + private static void AssertStrategy(ResilienceStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay, Action>? assert = null) { - var strategy = (RetryResilienceStrategy)builder.Build(); + var strategy = (RetryResilienceStrategy)builder.Build(); strategy.BackoffType.Should().Be(type); strategy.RetryCount.Should().Be(retries); @@ -84,9 +84,9 @@ private static void AssertStrategy(ResilienceStrategyBuilder builder, RetryBacko assert?.Invoke(strategy); } - private static void AssertStrategy(ResilienceStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay, Action? assert = null) + private static void AssertStrategy(ResilienceStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay, Action>? assert = null) { - var strategy = (RetryResilienceStrategy)builder.Build().Strategy; + var strategy = (RetryResilienceStrategy)builder.Build().Strategy; strategy.BackoffType.Should().Be(type); strategy.RetryCount.Should().Be(retries); diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs index 009616d0b77..8a6cbcbc35c 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs @@ -313,15 +313,11 @@ public void RetryDelayGenerator_EnsureCorrectArguments() private void SetupNoDelay() => _options.RetryDelayGenerator = _ => new ValueTask(TimeSpan.Zero); - private RetryResilienceStrategy CreateSut(TimeProvider? timeProvider = null) + private RetryResilienceStrategy CreateSut(TimeProvider? timeProvider = null) { - return new RetryResilienceStrategy( - _options.BaseDelay, - _options.BackoffType, - _options.RetryCount, - PredicateInvoker.Create(_options.ShouldHandle!, false)!, - EventInvoker.Create(_options.OnRetry, false), - GeneratorInvoker.Create(_options.RetryDelayGenerator, TimeSpan.MinValue, false), + return new RetryResilienceStrategy( + _options, + false, timeProvider ?? _timeProvider.Object, _telemetry, RandomUtil.Instance); diff --git a/test/Polly.Core.Tests/Utils/EventInvokerTests.cs b/test/Polly.Core.Tests/Utils/EventInvokerTests.cs deleted file mode 100644 index a7e88e80daf..00000000000 --- a/test/Polly.Core.Tests/Utils/EventInvokerTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Polly.Utils; - -namespace Polly.Core.Tests.Utils; - -public class EventInvokerTests -{ - [Fact] - public void NullCallback_Ok() - { - EventInvoker.Create(null, isGeneric: true).Should().BeNull(); - EventInvoker.Create(null, isGeneric: true).Should().BeNull(); - } - - [Fact] - public async Task HandleAsync_NonGeneric_Ok() - { - var context = ResilienceContext.Get(); - var args = new TestArguments(); - var called = false; - var invoker = EventInvoker.Create(args => - { - args.Result.Should().Be(10); - args.Context.Should().NotBeNull(); - called = true; - return default; - }, - false)!; - - await invoker.HandleAsync(new(context, new Outcome(10), args)); - called.Should().Be(true); - } - - [Fact] - public async Task HandleAsync_Generic_Ok() - { - var context = ResilienceContext.Get(); - var args = new TestArguments(); - var called = false; - var invoker = EventInvoker.Create(args => - { - args.Context.Should().NotBeNull(); - args.Result.Should().Be(10); - called = true; - return default; - }, - true)!; - - await invoker.HandleAsync(new(context, new Outcome(10), args)); - called.Should().Be(true); - - called = false; - await invoker.HandleAsync(new(ResilienceContext.Get(), new Outcome("dummy"), args)); - called.Should().Be(false); - } - - [Fact] - public async Task HandleAsync_GenericObject_Ok() - { - var context = ResilienceContext.Get(); - var called = false; - var args = new TestArguments(); - var invoker = EventInvoker.Create(_ => { called = true; return default; }, true); - await invoker!.HandleAsync(new(context, new Outcome("dummy"), args)); - called.Should().BeFalse(); - - await invoker!.HandleAsync(new(context, new Outcome("dummy"), args)); - called.Should().BeTrue(); - } -} diff --git a/test/Polly.Core.Tests/Utils/GeneratorInvokerTests.cs b/test/Polly.Core.Tests/Utils/GeneratorInvokerTests.cs deleted file mode 100644 index 4fbd65f4908..00000000000 --- a/test/Polly.Core.Tests/Utils/GeneratorInvokerTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Polly.Utils; - -namespace Polly.Core.Tests.Utils; - -public class GeneratorInvokerTests -{ - [Fact] - public void NullCallback_Ok() - { - GeneratorInvoker.Create(null, "default", false).Should().BeNull(); - GeneratorInvoker.Create(null, "default", true).Should().BeNull(); - } - - [Fact] - public async Task HandleAsync_NonGeneric_Ok() - { - var context = ResilienceContext.Get(); - var args = new TestArguments(); - var invoker = GeneratorInvoker.Create(args => - { - args.Context.Should().NotBeNull(); - args.Result.Should().Be(10); - - return new ValueTask("generated-value"); - }, - "default", - false)!; - - var outcomeArgs = new OutcomeArguments(context, new Outcome(10), args); - (await invoker.HandleAsync(outcomeArgs)).Should().Be("generated-value"); - } - - [Fact] - public async Task HandleAsync_Generic_Ok() - { - var context = ResilienceContext.Get(); - var args = new TestArguments(); - var invoker = GeneratorInvoker.Create(args => - { - args.Context.Should().NotBeNull(); - args.Result.Should().Be(10); - - return new ValueTask("generated-value"); - }, - "default", - true)!; - - (await invoker.HandleAsync(new(context, new Outcome(10), args))).Should().Be("generated-value"); - (await invoker.HandleAsync(new(context, new Outcome("dummy"), args))).Should().Be("default"); - - invoker = GeneratorInvoker.Create(_ => new ValueTask("dummy"), "default", true); - (await invoker!.HandleAsync(new(context, new Outcome("dummy"), args))).Should().Be("default"); - (await invoker!.HandleAsync(new(context, new Outcome("dummy"), args))).Should().Be("dummy"); - } - - [Fact] - public async Task HandleAsync_GenericObject_Ok() - { - var context = ResilienceContext.Get(); - - var args = new TestArguments(); - var invoker = GeneratorInvoker.Create(_ => new ValueTask("dummy"), "default", true); - (await invoker!.HandleAsync(new(context, new Outcome("dummy"), args))).Should().Be("default"); - (await invoker!.HandleAsync(new(context, new Outcome("dummy"), args))).Should().Be("dummy"); - } -} diff --git a/test/Polly.Core.Tests/Utils/OutcomeResilienceStrategyTests.cs b/test/Polly.Core.Tests/Utils/OutcomeResilienceStrategyTests.cs new file mode 100644 index 00000000000..419f82294c7 --- /dev/null +++ b/test/Polly.Core.Tests/Utils/OutcomeResilienceStrategyTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading.Tasks; +using Polly.Utils; + +namespace Polly.Core.Tests.Utils; + +public class OutcomeResilienceStrategyTests +{ + [Fact] + public void Ctor_Ok() + { + new Strategy(args => { }, true).Should().NotBeNull(); + new Strategy(args => { }, false).Should().NotBeNull(); + new Strategy(args => { }, true).Should().NotBeNull(); + } + + [Fact] + public void Ctor_InvalidArgs_Throws() + { + this.Invoking(_ => new Strategy(_ => { }, false)).Should().Throw(); + } + + [Fact] + public void Execute_NonGeneric_Ok() + { + var values = new List(); + + var strategy = new Strategy(outcome => + { + values.Add(outcome.Result); + }, + false); + + strategy.Execute(args => "dummy"); + strategy.Execute(args => 0); + strategy.Execute(args => null); + strategy.Execute(args => true); + + values[0].Should().Be("dummy"); + values[1].Should().Be(0); + values[2].Should().BeNull(); + values[3].Should().Be(true); + } + + [Fact] + public void Execute_Generic_Ok() + { + var values = new List(); + + var strategy = new Strategy(outcome => + { + values.Add(outcome.Result); + }, + true); + + strategy.Execute(args => "dummy"); + strategy.Execute(args => 0); + strategy.Execute(args => null); + strategy.Execute(args => true); + + values.Should().HaveCount(1); + values[0].Should().Be("dummy"); + } + + private class Strategy : OutcomeResilienceStrategy + { + private readonly Action> _onOutcome; + + public Strategy(Action> onOutcome, bool isGeneric) + : base(isGeneric) => _onOutcome = onOutcome; + + protected override async ValueTask> ExecuteCallbackAsync(Func>> callback, ResilienceContext context, TState state) + { + var outcome = await callback(context, state); + _onOutcome(outcome); + return outcome; + } + } + +} diff --git a/test/Polly.Core.Tests/Utils/PredicateInvokerTests.cs b/test/Polly.Core.Tests/Utils/PredicateInvokerTests.cs deleted file mode 100644 index ff229cef433..00000000000 --- a/test/Polly.Core.Tests/Utils/PredicateInvokerTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Polly.Utils; - -namespace Polly.Core.Tests.Utils; - -public class PredicateInvokerTests -{ - [Fact] - public void NullCallback_Ok() - { - PredicateInvoker.Create(null, isGeneric: true).Should().BeNull(); - PredicateInvoker.Create(null, isGeneric: true).Should().BeNull(); - } - - [Fact] - public async Task HandleAsync_NonGeneric_Ok() - { - var context = ResilienceContext.Get(); - var args = new TestArguments(); - var called = false; - var invoker = PredicateInvoker.Create(args => - { - args.Result.Should().Be(10); - args.Context.Should().NotBeNull(); - called = true; - return new ValueTask(true); - }, - false); - - (await invoker!.HandleAsync(new(context, new Outcome(10), args))).Should().Be(true); - called.Should().Be(true); - } - - [Fact] - public async Task HandleAsync_Generic_Ok() - { - var context = ResilienceContext.Get(); - var args = new TestArguments(); - var called = false; - var invoker = PredicateInvoker.Create(args => - { - args.Result.Should().Be(10); - args.Context.Should().NotBeNull(); - called = true; - return new ValueTask(true); - }, - true); - - (await invoker!.HandleAsync(new(context, new Outcome(10), args))).Should().Be(true); - called.Should().Be(true); - - called = false; - (await invoker.HandleAsync(new(context, new Outcome("dummy"), args))).Should().Be(false); - called.Should().Be(false); - } - - [Fact] - public async Task HandleAsync_GenericObject_Ok() - { - var context = ResilienceContext.Get(); - var args = new TestArguments(); - var invoker = PredicateInvoker.Create(_ => PredicateResult.True, true); - (await invoker!.HandleAsync(new(context, new Outcome("dummy"), args))).Should().BeFalse(); - (await invoker!.HandleAsync(new(context, new Outcome("dummy"), args))).Should().BeTrue(); - } -}