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 2206aa4444e..761ed6b8320 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 @@ -1,15 +1,15 @@ ``` ini -BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/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 +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1848/22H2/2022Update/SunValley2), VM=Hyper-V +Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores +.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 | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|--------------------------- |---------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| -| ExecuteStrategyPipeline_V7 | 2.027 μs | 0.0282 μs | 0.0413 μs | 1.00 | 0.00 | 0.3357 | 2824 B | 1.00 | -| ExecuteStrategyPipeline_V8 | 1.783 μs | 0.0254 μs | 0.0372 μs | 0.88 | 0.03 | 0.0038 | 40 B | 0.01 | +| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio | +|--------------------------- |---------:|----------:|----------:|------:|-------:|----------:|------------:| +| ExecuteStrategyPipeline_V7 | 2.227 μs | 0.0077 μs | 0.0116 μs | 1.00 | 0.1106 | 2824 B | 1.00 | +| ExecuteStrategyPipeline_V8 | 1.750 μs | 0.0060 μs | 0.0084 μs | 0.79 | - | 40 B | 0.01 | diff --git a/bench/Polly.Core.Benchmarks/Utils/Helper.Retry.cs b/bench/Polly.Core.Benchmarks/Utils/Helper.Retry.cs index 3a23562904a..a7fc62e5f34 100644 --- a/bench/Polly.Core.Benchmarks/Utils/Helper.Retry.cs +++ b/bench/Polly.Core.Benchmarks/Utils/Helper.Retry.cs @@ -21,7 +21,7 @@ public static object CreateRetry(PollyVersion technology) RetryCount = 3, BackoffType = RetryBackoffType.Constant, BaseDelay = delay, - ShouldRetry = args => args switch + ShouldHandle = args => args switch { { Exception: InvalidOperationException } => PredicateResult.True, { Result: string result } when result == Failure => PredicateResult.True, diff --git a/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs b/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs index 4c0c539fe96..ae5cb70884d 100644 --- a/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs +++ b/src/Polly.Core/CircuitBreaker/CircuitBreakerStrategyOptions.cs @@ -41,12 +41,7 @@ public abstract class CircuitBreakerStrategyOptions : ResilienceStrateg /// This property is required. /// [Required] - public Func, ValueTask> ShouldHandle { get; set; } = args => args.Exception switch - { - OperationCanceledException => PredicateResult.False, - Exception => PredicateResult.True, - _ => PredicateResult.False - }; + public Func, ValueTask> ShouldHandle { get; set; } = DefaultPredicates.HandleOutcome; /// /// Gets or sets the event that is raised when the circuit resets to a state. diff --git a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs index b8c32122daa..f3e1c749d81 100644 --- a/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs +++ b/src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs @@ -1,4 +1,3 @@ -using System.Runtime.ExceptionServices; using Polly.Telemetry; namespace Polly.CircuitBreaker; @@ -151,7 +150,7 @@ public ValueTask CloseCircuitAsync(ResilienceContext context) if (exception is not null) { - return new Outcome(ExceptionDispatchInfo.Capture(exception)); + return new Outcome(exception); } return null; @@ -305,6 +304,8 @@ private void SetLastHandledOutcome_NeedsLock(Outcome outcome) { _breakingException = new BrokenCircuitException(BrokenCircuitException.DefaultMessage, result!); } + + _breakingException?.TrySetStackTrace(); } private BrokenCircuitException GetBreakingException_NeedsLock() => _breakingException ?? new BrokenCircuitException(); diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs index 3d8d06ba808..5cb632c3dfe 100644 --- a/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs +++ b/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs @@ -4,7 +4,4 @@ namespace Polly.CircuitBreaker; /// Arguments used by event. /// /// Indicates whether the circuit was closed manually by using . -/// -/// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. -/// -public readonly record struct OnCircuitClosedArguments(bool IsManual); +public record OnCircuitClosedArguments(bool IsManual); diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs index 3430389d79a..97444487f39 100644 --- a/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs +++ b/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs @@ -3,7 +3,4 @@ namespace Polly.CircuitBreaker; /// /// Arguments used by event. /// -/// -/// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. -/// -public readonly record struct OnCircuitHalfOpenedArguments(); +public record OnCircuitHalfOpenedArguments(); diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs index 28f91d54132..261f18e53cb 100644 --- a/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs +++ b/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs @@ -5,7 +5,4 @@ namespace Polly.CircuitBreaker; /// /// The duration of break. /// Indicates whether the circuit was opened manually by using . -/// -/// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. -/// -public readonly record struct OnCircuitOpenedArguments(TimeSpan BreakDuration, bool IsManual); +public record OnCircuitOpenedArguments(TimeSpan BreakDuration, bool IsManual); diff --git a/src/Polly.Core/Fallback/FallbackHandler.cs b/src/Polly.Core/Fallback/FallbackHandler.cs index 03d7e63fa08..5a30e6595f1 100644 --- a/src/Polly.Core/Fallback/FallbackHandler.cs +++ b/src/Polly.Core/Fallback/FallbackHandler.cs @@ -1,8 +1,8 @@ namespace Polly.Fallback; internal sealed record class FallbackHandler( - PredicateInvoker ShouldHandle, - Func, ValueTask>> ActionGenerator, + PredicateInvoker ShouldHandle, + Func, ValueTask>> ActionGenerator, bool IsGeneric) { public bool HandlesFallback() => IsGeneric switch @@ -11,9 +11,9 @@ internal sealed record class FallbackHandler( false => true }; - public async ValueTask> GetFallbackOutcomeAsync(OutcomeArguments args) + public async ValueTask> GetFallbackOutcomeAsync(OutcomeArguments args) { - var copiedArgs = new OutcomeArguments( + var copiedArgs = new OutcomeArguments( args.Context, args.Outcome.AsOutcome(), args.Arguments); diff --git a/src/Polly.Core/Fallback/HandleFallbackArguments.cs b/src/Polly.Core/Fallback/FallbackPredicateArguments.cs similarity index 81% rename from src/Polly.Core/Fallback/HandleFallbackArguments.cs rename to src/Polly.Core/Fallback/FallbackPredicateArguments.cs index ab4632a2d7e..fb575d72ee9 100644 --- a/src/Polly.Core/Fallback/HandleFallbackArguments.cs +++ b/src/Polly.Core/Fallback/FallbackPredicateArguments.cs @@ -6,4 +6,4 @@ namespace Polly.Fallback; /// /// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. /// -public readonly record struct HandleFallbackArguments(); +public readonly record struct FallbackPredicateArguments(); diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs index d6286e0b739..9fc8ad509f1 100644 --- a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs @@ -1,4 +1,3 @@ -using System.Runtime.ExceptionServices; using Polly.Telemetry; namespace Polly.Fallback; @@ -29,7 +28,7 @@ protected internal override async ValueTask> ExecuteCoreAsync(context, outcome, new HandleFallbackArguments()); + var handleFallbackArgs = new OutcomeArguments(context, outcome, new FallbackPredicateArguments()); if (!await _handler.ShouldHandle.HandleAsync(handleFallbackArgs).ConfigureAwait(context.ContinueOnCapturedContext)) { return outcome; @@ -50,7 +49,7 @@ protected internal override async ValueTask> ExecuteCoreAsync(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } } } diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs index 7a6165cea45..298a0ca1f4b 100644 --- a/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategyBuilderExtensions.cs @@ -20,7 +20,7 @@ public static class FallbackResilienceStrategyBuilderExtensions public static ResilienceStrategyBuilder AddFallback( this ResilienceStrategyBuilder builder, Action> shouldHandle, - Func, ValueTask>> fallbackAction) + Func, ValueTask>> fallbackAction) { Guard.NotNull(builder); Guard.NotNull(shouldHandle); @@ -34,7 +34,7 @@ public static ResilienceStrategyBuilder AddFallback( var predicateBuilder = new PredicateBuilder(); shouldHandle(predicateBuilder); - options.ShouldHandle = predicateBuilder.CreatePredicate(); + options.ShouldHandle = predicateBuilder.CreatePredicate(); return builder.AddFallback(options); } diff --git a/src/Polly.Core/Fallback/FallbackStrategyOptions.TResult.cs b/src/Polly.Core/Fallback/FallbackStrategyOptions.TResult.cs index 95c8a6226c9..759c6eebfaa 100644 --- a/src/Polly.Core/Fallback/FallbackStrategyOptions.TResult.cs +++ b/src/Polly.Core/Fallback/FallbackStrategyOptions.TResult.cs @@ -18,10 +18,11 @@ public class FallbackStrategyOptions : ResilienceStrategyOptions /// Gets or sets the outcome predicate for determining whether a fallback should be executed. /// /// - /// This property is required. Defaults to . + /// Defaults to a delegate that hedges on any exception except . + /// This property is required. /// [Required] - public Func, ValueTask>? ShouldHandle { get; set; } + public Func, ValueTask> ShouldHandle { get; set; } = DefaultPredicates.HandleOutcome; /// /// Gets or sets the fallback action to be executed when the predicate evaluates as true. @@ -30,7 +31,7 @@ public class FallbackStrategyOptions : ResilienceStrategyOptions /// This property is required. Defaults to . /// [Required] - public Func, ValueTask>>? FallbackAction { get; set; } + public Func, ValueTask>>? FallbackAction { get; set; } /// /// Gets or sets the outcome event instance responsible for triggering fallback events. diff --git a/src/Polly.Core/Fallback/OnFallbackArguments.cs b/src/Polly.Core/Fallback/OnFallbackArguments.cs index 4dc5a9197eb..628aaa85718 100644 --- a/src/Polly.Core/Fallback/OnFallbackArguments.cs +++ b/src/Polly.Core/Fallback/OnFallbackArguments.cs @@ -3,7 +3,4 @@ namespace Polly.Fallback; /// /// Represents arguments used in fallback handling scenarios. /// -/// -/// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. -/// -public readonly record struct OnFallbackArguments(); +public record OnFallbackArguments(); diff --git a/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs b/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs index b6a933643bb..a846c52b1dd 100644 --- a/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs +++ b/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs @@ -8,7 +8,7 @@ namespace Polly.Hedging.Utils; /// The context associated with an execution of hedging resilience strategy. /// It holds the resources for all executed hedged tasks (primary + secondary) and is responsible for resource disposal. /// -internal sealed class HedgingExecutionContext +internal sealed class HedgingExecutionContext : IAsyncDisposable { public readonly record struct ExecutionInfo(TaskExecution? Execution, bool Loaded, Outcome? Outcome); @@ -81,7 +81,7 @@ public async ValueTask> LoadExecutionAsync?> TryWaitForCompletedExecutionAsync(TimeSpan hedgingDelay) @@ -226,18 +230,6 @@ private void UpdateOriginalContext() } } - private async Task CleanupInBackgroundAsync() - { - foreach (var task in _tasks) - { - await task.ExecutionTaskSafe!.ConfigureAwait(false); - await task.ResetAsync().ConfigureAwait(false); - _executionPool.Return(task); - } - - Reset(); - } - private void Reset() { _replacedProperties.Clear(); diff --git a/src/Polly.Core/Hedging/Controller/HedgingHandler.cs b/src/Polly.Core/Hedging/Controller/HedgingHandler.cs index b80abbe9972..5eb0be8c72a 100644 --- a/src/Polly.Core/Hedging/Controller/HedgingHandler.cs +++ b/src/Polly.Core/Hedging/Controller/HedgingHandler.cs @@ -1,7 +1,7 @@ namespace Polly.Hedging.Utils; internal sealed record class HedgingHandler( - PredicateInvoker ShouldHandle, + PredicateInvoker ShouldHandle, Func, Func>>?> ActionGenerator, bool IsGeneric) { diff --git a/src/Polly.Core/Hedging/Controller/TaskExecution.cs b/src/Polly.Core/Hedging/Controller/TaskExecution.cs index 2ec0476865d..a75a5201fd3 100644 --- a/src/Polly.Core/Hedging/Controller/TaskExecution.cs +++ b/src/Polly.Core/Hedging/Controller/TaskExecution.cs @@ -208,7 +208,7 @@ private async Task ExecutePrimaryActionAsync(Func(Outcome outcome) { - var args = new OutcomeArguments(Context, outcome, new HandleHedgingArguments()); + var args = new OutcomeArguments(Context, outcome, new HedgingPredicateArguments()); Outcome = outcome.AsOutcome(); IsHandled = await _handler.ShouldHandle.HandleAsync(args).ConfigureAwait(Context.ContinueOnCapturedContext); } diff --git a/src/Polly.Core/Hedging/HandleHedgingArguments.cs b/src/Polly.Core/Hedging/HedgingPredicateArguments.cs similarity index 81% rename from src/Polly.Core/Hedging/HandleHedgingArguments.cs rename to src/Polly.Core/Hedging/HedgingPredicateArguments.cs index b3e63933ca2..1a423192fc9 100644 --- a/src/Polly.Core/Hedging/HandleHedgingArguments.cs +++ b/src/Polly.Core/Hedging/HedgingPredicateArguments.cs @@ -6,4 +6,4 @@ namespace Polly.Hedging; /// /// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. /// -public readonly record struct HandleHedgingArguments(); +public readonly record struct HedgingPredicateArguments(); diff --git a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs index 90c442783f1..5a941f8edb6 100644 --- a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs +++ b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading; using Polly.Hedging.Utils; using Polly.Telemetry; @@ -37,6 +39,7 @@ public HedgingResilienceStrategy( public EventInvoker? OnHedging { get; } + [ExcludeFromCodeCoverage] // coverlet issue protected internal override async ValueTask> ExecuteCoreAsync( Func>> callback, ResilienceContext context, @@ -47,58 +50,67 @@ protected internal override async ValueTask> ExecuteCoreAsync(new OperationCanceledException(cancellationToken)); - } - - if ((await hedgingContext.LoadExecutionAsync(callback, state).ConfigureAwait(context.ContinueOnCapturedContext)).Outcome is Outcome outcome) - { - return outcome; - } - - var delay = await GetHedgingDelayAsync(context, hedgingContext.LoadedTasks).ConfigureAwait(continueOnCapturedContext); - var execution = await hedgingContext.TryWaitForCompletedExecutionAsync(delay).ConfigureAwait(continueOnCapturedContext); - if (execution is null) - { - // If completedHedgedTask is null it indicates that we still do not have any finished hedged task within the hedging delay. - // We will create additional hedged task in the next iteration. - continue; - } - - outcome = execution.Outcome.AsOutcome(); - - if (!execution.IsHandled) - { - execution.AcceptOutcome(); - return outcome; - } - - var onHedgingArgs = new OutcomeArguments(context, outcome, new OnHedgingArguments(context, hedgingContext.LoadedTasks - 1)); - _telemetry.Report(HedgingConstants.OnHedgingEventName, onHedgingArgs); - - if (OnHedging is not null) - { - // 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(continueOnCapturedContext); - } - } + return await ExecuteCoreAsync(hedgingContext, callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); } finally { - hedgingContext.Complete(); + await hedgingContext.DisposeAsync().ConfigureAwait(context.ContinueOnCapturedContext); + } + } + + private async ValueTask> ExecuteCoreAsync( + HedgingExecutionContext hedgingContext, + Func>> callback, + ResilienceContext context, + TState state) + { + while (true) + { + var continueOnCapturedContext = context.ContinueOnCapturedContext; + var cancellationToken = context.CancellationToken; + + if (cancellationToken.IsCancellationRequested) + { + return new Outcome(new OperationCanceledException(cancellationToken).TrySetStackTrace()); + } + + if ((await hedgingContext.LoadExecutionAsync(callback, state).ConfigureAwait(context.ContinueOnCapturedContext)).Outcome is Outcome outcome) + { + return outcome; + } + + var delay = await GetHedgingDelayAsync(context, hedgingContext.LoadedTasks).ConfigureAwait(continueOnCapturedContext); + var execution = await hedgingContext.TryWaitForCompletedExecutionAsync(delay).ConfigureAwait(continueOnCapturedContext); + if (execution is null) + { + // If completedHedgedTask is null it indicates that we still do not have any finished hedged task within the hedging delay. + // We will create additional hedged task in the next iteration. + continue; + } + + outcome = execution.Outcome.AsOutcome(); + + if (!execution.IsHandled) + { + execution.AcceptOutcome(); + return outcome; + } + + var onHedgingArgs = new OutcomeArguments(context, outcome, new OnHedgingArguments(context, hedgingContext.LoadedTasks - 1)); + _telemetry.Report(HedgingConstants.OnHedgingEventName, onHedgingArgs); + + if (OnHedging is not null) + { + // 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(continueOnCapturedContext); + } } } diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs index e580bd40677..5a5fab3750a 100644 --- a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs @@ -49,12 +49,7 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// This property is required. /// [Required] - public Func, ValueTask> ShouldHandle { get; set; } = args => args.Exception switch - { - OperationCanceledException => PredicateResult.False, - Exception => PredicateResult.True, - _ => PredicateResult.False - }; + public Func, ValueTask> ShouldHandle { get; set; } = DefaultPredicates.HandleOutcome; /// /// Gets or sets the hedging action generator that creates hedged actions. @@ -67,6 +62,11 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions { return async () => { + if (args.PrimaryContext.IsSynchronous) + { + return await Task.Run(() => args.Callback(args.ActionContext).AsTask()).ConfigureAwait(args.ActionContext.ContinueOnCapturedContext); + } + return await args.Callback(args.ActionContext).ConfigureAwait(args.ActionContext.ContinueOnCapturedContext); }; }; diff --git a/src/Polly.Core/ResilienceStrategy.Async.ValueTask.cs b/src/Polly.Core/ResilienceStrategy.Async.ValueTask.cs index 4a70c5245c3..dfeed64ad54 100644 --- a/src/Polly.Core/ResilienceStrategy.Async.ValueTask.cs +++ b/src/Polly.Core/ResilienceStrategy.Async.ValueTask.cs @@ -1,5 +1,3 @@ -using System.Runtime.ExceptionServices; - namespace Polly; #pragma warning disable CA1031 // Do not catch general exception types @@ -35,7 +33,7 @@ static async (context, state) => } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -70,7 +68,7 @@ static async (context, state) => } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -109,7 +107,7 @@ static async (context, state) => } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -150,7 +148,7 @@ static async (context, state) => } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, diff --git a/src/Polly.Core/ResilienceStrategy.Async.ValueTaskT.cs b/src/Polly.Core/ResilienceStrategy.Async.ValueTaskT.cs index 3f1ae6de7d8..71f4c27a289 100644 --- a/src/Polly.Core/ResilienceStrategy.Async.ValueTaskT.cs +++ b/src/Polly.Core/ResilienceStrategy.Async.ValueTaskT.cs @@ -1,5 +1,3 @@ -using System.Runtime.ExceptionServices; - namespace Polly; #pragma warning disable CA1031 // Do not catch general exception types @@ -62,7 +60,7 @@ static async (context, state) => } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -97,7 +95,7 @@ static async (context, state) => } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -136,7 +134,7 @@ static async (context, state) => } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -177,7 +175,7 @@ static async (context, state) => } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, diff --git a/src/Polly.Core/ResilienceStrategy.Sync.cs b/src/Polly.Core/ResilienceStrategy.Sync.cs index a336aa4b4d3..68f2d64dc9a 100644 --- a/src/Polly.Core/ResilienceStrategy.Sync.cs +++ b/src/Polly.Core/ResilienceStrategy.Sync.cs @@ -1,5 +1,3 @@ -using System.Runtime.ExceptionServices; - namespace Polly; #pragma warning disable CA1031 // Do not catch general exception types @@ -34,7 +32,7 @@ public void Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -66,7 +64,7 @@ public void Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -102,7 +100,7 @@ public void Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -140,7 +138,7 @@ public void Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -179,7 +177,7 @@ public void Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -214,7 +212,7 @@ public void Execute(Action callback) } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, diff --git a/src/Polly.Core/ResilienceStrategy.SyncT.cs b/src/Polly.Core/ResilienceStrategy.SyncT.cs index 38be106ea80..41b2bdfac18 100644 --- a/src/Polly.Core/ResilienceStrategy.SyncT.cs +++ b/src/Polly.Core/ResilienceStrategy.SyncT.cs @@ -1,5 +1,3 @@ -using System.Runtime.ExceptionServices; - namespace Polly; #pragma warning disable CA1031 // Do not catch general exception types @@ -36,7 +34,7 @@ public TResult Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -70,7 +68,7 @@ public TResult Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -105,7 +103,7 @@ public TResult Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -142,7 +140,7 @@ public TResult Execute(Func callback) } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -181,7 +179,7 @@ public TResult Execute(Func callback, TState s } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, @@ -224,7 +222,7 @@ public TResult Execute( } catch (Exception e) { - return new Outcome(ExceptionDispatchInfo.Capture(e)); + return new Outcome(e); } }, context, diff --git a/src/Polly.Core/Retry/OnRetryArguments.cs b/src/Polly.Core/Retry/OnRetryArguments.cs index 66993588c5a..578106d0885 100644 --- a/src/Polly.Core/Retry/OnRetryArguments.cs +++ b/src/Polly.Core/Retry/OnRetryArguments.cs @@ -5,9 +5,4 @@ namespace Polly.Retry; /// /// The zero-based attempt number. The first attempt is 0, the second attempt is 1, and so on. /// The delay before the next retry. -/// -/// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. -/// -public readonly record struct OnRetryArguments(int Attempt, TimeSpan RetryDelay) -{ -} +public record OnRetryArguments(int Attempt, TimeSpan RetryDelay); diff --git a/src/Polly.Core/Retry/ShouldRetryArguments.cs b/src/Polly.Core/Retry/RetryPredicateArguments.cs similarity index 71% rename from src/Polly.Core/Retry/ShouldRetryArguments.cs rename to src/Polly.Core/Retry/RetryPredicateArguments.cs index 4a3ffd68b29..7a82ff19277 100644 --- a/src/Polly.Core/Retry/ShouldRetryArguments.cs +++ b/src/Polly.Core/Retry/RetryPredicateArguments.cs @@ -1,10 +1,10 @@ namespace Polly.Retry; /// -/// Represents the arguments used by for determining whether a retry should be performed. +/// Represents the arguments used by for determining whether a retry should be performed. /// /// The zero-based attempt number. The first attempt is 0, the second attempt is 1, and so on. /// /// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. /// -public readonly record struct ShouldRetryArguments(int Attempt); +public readonly record struct RetryPredicateArguments(int Attempt); diff --git a/src/Polly.Core/Retry/RetryResilienceStrategy.cs b/src/Polly.Core/Retry/RetryResilienceStrategy.cs index 0b1c3934da1..d653a83f3de 100644 --- a/src/Polly.Core/Retry/RetryResilienceStrategy.cs +++ b/src/Polly.Core/Retry/RetryResilienceStrategy.cs @@ -14,7 +14,7 @@ public RetryResilienceStrategy( TimeSpan baseDelay, RetryBackoffType backoffType, int retryCount, - PredicateInvoker shouldRetry, + PredicateInvoker shouldRetry, EventInvoker? onRetry, GeneratorInvoker? delayGenerator, TimeProvider timeProvider, @@ -40,7 +40,7 @@ public RetryResilienceStrategy( public int RetryCount { get; } - public PredicateInvoker ShouldRetry { get; } + public PredicateInvoker ShouldRetry { get; } public GeneratorInvoker? DelayGenerator { get; } @@ -58,7 +58,7 @@ protected internal override async ValueTask> ExecuteCoreAsync(context, outcome, new ShouldRetryArguments(attempt)); + var shouldRetryArgs = new OutcomeArguments(context, outcome, new RetryPredicateArguments(attempt)); if (context.CancellationToken.IsCancellationRequested || IsLastAttempt(attempt) || !await ShouldRetry.HandleAsync(shouldRetryArgs).ConfigureAwait(context.ContinueOnCapturedContext)) { diff --git a/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs b/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs index d0c1b786880..e47ce44bfe1 100644 --- a/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs +++ b/src/Polly.Core/Retry/RetryResilienceStrategyBuilderExtensions.cs @@ -83,7 +83,7 @@ private static TBuilder AddRetryCore(this TBuilder builder, R options.BaseDelay, options.BackoffType, options.RetryCount, - context.CreateInvoker(options.ShouldRetry)!, + context.CreateInvoker(options.ShouldHandle)!, context.CreateInvoker(options.OnRetry), context.CreateInvoker(options.RetryDelayGenerator, TimeSpan.MinValue), context.TimeProvider, @@ -98,6 +98,6 @@ private static void ConfigureShouldRetry(Action(); shouldRetry(predicate); - options.ShouldRetry = predicate.CreatePredicate(); + options.ShouldHandle = predicate.CreatePredicate(); } } diff --git a/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs b/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs index 48ecd7c3347..3555cfc3513 100644 --- a/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs +++ b/src/Polly.Core/Retry/RetryStrategyOptions.TResult.cs @@ -65,12 +65,7 @@ public class RetryStrategyOptions : ResilienceStrategyOptions /// This property is required. /// [Required] - public Func, ValueTask> ShouldRetry { get; set; } = args => args.Exception switch - { - OperationCanceledException => PredicateResult.False, - Exception => PredicateResult.True, - _ => PredicateResult.False - }; + public Func, ValueTask> ShouldHandle { get; set; } = DefaultPredicates.HandleOutcome; /// /// Gets or sets the generator instance that is used to calculate the time between retries. diff --git a/src/Polly.Core/Timeout/OnTimeoutArguments.cs b/src/Polly.Core/Timeout/OnTimeoutArguments.cs index 669c9f5b543..ab481e10e58 100644 --- a/src/Polly.Core/Timeout/OnTimeoutArguments.cs +++ b/src/Polly.Core/Timeout/OnTimeoutArguments.cs @@ -6,7 +6,4 @@ namespace Polly.Timeout; /// The context associated with the execution of a user-provided callback. /// The original exception that caused the timeout. /// The timeout value assigned. -/// -/// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. -/// -public readonly record struct OnTimeoutArguments(ResilienceContext Context, Exception Exception, TimeSpan Timeout); +public record OnTimeoutArguments(ResilienceContext Context, Exception Exception, TimeSpan Timeout); diff --git a/src/Polly.Core/Timeout/TimeoutResilienceStrategy.cs b/src/Polly.Core/Timeout/TimeoutResilienceStrategy.cs index bc0a44a76e7..79153cbb98f 100644 --- a/src/Polly.Core/Timeout/TimeoutResilienceStrategy.cs +++ b/src/Polly.Core/Timeout/TimeoutResilienceStrategy.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Runtime.ExceptionServices; using Polly.Telemetry; namespace Polly.Timeout; @@ -46,13 +45,17 @@ protected internal override async ValueTask> ExecuteCoreAsync> ExecuteCoreAsync(ExceptionDispatchInfo.Capture(timeoutException)); + return new Outcome(timeoutException.TrySetStackTrace()); } return outcome; @@ -88,13 +91,13 @@ internal async ValueTask GenerateTimeoutAsync(ResilienceContext contex return timeout; } - private static CancellationTokenRegistration? CreateRegistration(CancellationTokenSource cancellationSource, CancellationToken previousToken) + private static CancellationTokenRegistration CreateRegistration(CancellationTokenSource cancellationSource, CancellationToken previousToken) { if (previousToken.CanBeCanceled) { return previousToken.Register(static state => ((CancellationTokenSource)state!).Cancel(), cancellationSource, useSynchronizationContext: false); } - return null; + return default; } } diff --git a/src/Polly.Core/Utils/DefaultPredicates.cs b/src/Polly.Core/Utils/DefaultPredicates.cs new file mode 100644 index 00000000000..bf210b47f52 --- /dev/null +++ b/src/Polly.Core/Utils/DefaultPredicates.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; + +namespace Polly.Utils; + +internal static class DefaultPredicates +{ + public static readonly Func, ValueTask> HandleOutcome = args => args.Exception switch + { + OperationCanceledException => PredicateResult.False, + Exception => PredicateResult.True, + _ => PredicateResult.False + }; +} diff --git a/src/Polly.Core/Utils/ExceptionUtilities.cs b/src/Polly.Core/Utils/ExceptionUtilities.cs new file mode 100644 index 00000000000..44c2d4eab9e --- /dev/null +++ b/src/Polly.Core/Utils/ExceptionUtilities.cs @@ -0,0 +1,37 @@ +namespace Polly.Utils; + +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + +internal static class ExceptionUtilities +{ +#if !NET6_0_OR_GREATER + private static readonly FieldInfo StackTraceString = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance)!; + private static readonly Type TraceFormat = typeof(StackTrace).GetNestedType("TraceFormat", BindingFlags.NonPublic)!; + private static readonly MethodInfo TraceToString = typeof(StackTrace).GetMethod("ToString", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { TraceFormat }, null)!; + private static readonly object[] TraceToStringArgs = new[] { Enum.GetValues(TraceFormat).GetValue(0) }; +#endif + + public static T TrySetStackTrace(this T exception) + where T : Exception + { + if (!string.IsNullOrWhiteSpace(exception.StackTrace)) + { + return exception; + } + +#if NET6_0_OR_GREATER + System.Runtime.ExceptionServices.ExceptionDispatchInfo.SetCurrentStackTrace(exception); +#else + SetStackTrace(exception, new StackTrace()); +#endif + return exception; + } + +#if !NET6_0_OR_GREATER + private static void SetStackTrace(this Exception target, StackTrace stack) + { + var getStackTraceString = TraceToString.Invoke(stack, TraceToStringArgs); + StackTraceString.SetValue(target, getStackTraceString); + } +#endif +} diff --git a/src/Polly.Core/Utils/ReloadableResilienceStrategy.cs b/src/Polly.Core/Utils/ReloadableResilienceStrategy.cs index 79efd802402..0d2d6243786 100644 --- a/src/Polly.Core/Utils/ReloadableResilienceStrategy.cs +++ b/src/Polly.Core/Utils/ReloadableResilienceStrategy.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Polly.Telemetry; namespace Polly.Utils; diff --git a/src/Polly.Core/Utils/ResilienceStrategyPipeline.cs b/src/Polly.Core/Utils/ResilienceStrategyPipeline.cs index 6cf249f5bfe..8dcf9930fff 100644 --- a/src/Polly.Core/Utils/ResilienceStrategyPipeline.cs +++ b/src/Polly.Core/Utils/ResilienceStrategyPipeline.cs @@ -59,7 +59,7 @@ protected internal override ValueTask> ExecuteCoreAsync>(new Outcome(new OperationCanceledException(context.CancellationToken))); + return new ValueTask>(new Outcome(new OperationCanceledException(context.CancellationToken).TrySetStackTrace())); } return _pipeline.ExecuteCoreAsync(callback, context, state); @@ -86,7 +86,7 @@ protected internal override ValueTask> ExecuteCoreAsync>(new Outcome(new OperationCanceledException(context.CancellationToken))); + return new ValueTask>(new Outcome(new OperationCanceledException(context.CancellationToken).TrySetStackTrace())); } return state.Next!.ExecuteCoreAsync(state.callback, context, state.state); diff --git a/src/Polly.Core/Utils/TypeNameFormatter.cs b/src/Polly.Core/Utils/TypeNameFormatter.cs index 6ec5f07fddd..57cc143a602 100644 --- a/src/Polly.Core/Utils/TypeNameFormatter.cs +++ b/src/Polly.Core/Utils/TypeNameFormatter.cs @@ -1,6 +1,4 @@ -using System; - -namespace Polly.Utils; +namespace Polly.Utils; internal static class TypeNameFormatter { diff --git a/src/Polly.Extensions/DependencyInjection/PollyDependencyInjectionKeys.cs b/src/Polly.Extensions/DependencyInjection/PollyDependencyInjectionKeys.cs index ac81a61f614..31a1e6688f2 100644 --- a/src/Polly.Extensions/DependencyInjection/PollyDependencyInjectionKeys.cs +++ b/src/Polly.Extensions/DependencyInjection/PollyDependencyInjectionKeys.cs @@ -3,7 +3,7 @@ namespace Polly.Extensions.DependencyInjection; /// /// The resilience keys used in the dependency injection scenarios. /// -public static class PollyDependencyInjectionKeys +internal static class PollyDependencyInjectionKeys { /// /// The key used to store and access the from . diff --git a/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs b/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs index 1d555c8adf8..55e637fa36a 100644 --- a/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs +++ b/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs @@ -10,12 +10,15 @@ internal class ResilienceTelemetryDiagnosticSource : DiagnosticSource internal static readonly Meter Meter = new(TelemetryUtil.PollyDiagnosticSource, "1.0"); private readonly ILogger _logger; + private readonly Func _resultFormatter; private readonly List> _enrichers; public ResilienceTelemetryDiagnosticSource(TelemetryOptions options) { _enrichers = options.Enrichers.ToList(); _logger = options.LoggerFactory.CreateLogger(TelemetryUtil.PollyDiagnosticSource); + _resultFormatter = options.ResultFormatter; + Counter = Meter.CreateCounter( "resilience-events", description: "Tracks the number of resilience events that occurred in resilience strategies."); @@ -64,7 +67,13 @@ private void LogEvent(TelemetryEventArguments args) } else { - Log.ResilienceEvent(_logger, args.EventName, args.Source.BuilderName, args.Source.StrategyName, args.Source.StrategyType, strategyKey, args.Outcome?.Result, null); + var result = args.Outcome?.Result; + if (result is not null) + { + result = _resultFormatter(args.Context, result); + } + + Log.ResilienceEvent(_logger, args.EventName, args.Source.BuilderName, args.Source.StrategyName, args.Source.StrategyType, strategyKey, result, null); } } } diff --git a/src/Polly.RateLimiting/OnRateLimiterRejectedArguments.cs b/src/Polly.RateLimiting/OnRateLimiterRejectedArguments.cs index 0ee0e66fa42..8771070fa93 100644 --- a/src/Polly.RateLimiting/OnRateLimiterRejectedArguments.cs +++ b/src/Polly.RateLimiting/OnRateLimiterRejectedArguments.cs @@ -8,7 +8,4 @@ namespace Polly.RateLimiting; /// The context associated with the execution of a user-provided callback. /// The lease that has no permits and was rejected by the rate limiter. /// The amount of time to wait before retrying again. This value is retrieved from the by reading the . -/// -/// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility. -/// -public readonly record struct OnRateLimiterRejectedArguments(ResilienceContext Context, RateLimitLease Lease, TimeSpan? RetryAfter); +public record OnRateLimiterRejectedArguments(ResilienceContext Context, RateLimitLease Lease, TimeSpan? RetryAfter); diff --git a/src/Polly.RateLimiting/Polly.RateLimiting.csproj b/src/Polly.RateLimiting/Polly.RateLimiting.csproj index adb54088e78..c4165fe48eb 100644 --- a/src/Polly.RateLimiting/Polly.RateLimiting.csproj +++ b/src/Polly.RateLimiting/Polly.RateLimiting.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Polly.RateLimiting/RateLimiterResilienceStrategy.cs b/src/Polly.RateLimiting/RateLimiterResilienceStrategy.cs index b3c34752a13..859cc79138f 100644 --- a/src/Polly.RateLimiting/RateLimiterResilienceStrategy.cs +++ b/src/Polly.RateLimiting/RateLimiterResilienceStrategy.cs @@ -51,6 +51,8 @@ protected override async ValueTask> ExecuteCoreAsync(retryAfter.HasValue ? new RateLimiterRejectedException(retryAfter.Value) : new RateLimiterRejectedException()); + var exception = retryAfter.HasValue ? new RateLimiterRejectedException(retryAfter.Value) : new RateLimiterRejectedException(); + + return new Outcome(exception.TrySetStackTrace()); } } diff --git a/test/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs b/test/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs index 6ec56fadbff..7fea4fd2730 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackHandlerTests.cs @@ -28,7 +28,7 @@ public async Task GenerateAction_Generic_Ok() { var handler = FallbackHelper.CreateHandler(_ => true, () => "secondary".AsOutcome(), true); var context = ResilienceContext.Get(); - var outcome = await handler.GetFallbackOutcomeAsync(new OutcomeArguments(context, new Outcome("primary"), new HandleFallbackArguments()))!; + var outcome = await handler.GetFallbackOutcomeAsync(new OutcomeArguments(context, new Outcome("primary"), new FallbackPredicateArguments()))!; outcome.Result.Should().Be("secondary"); } @@ -38,7 +38,7 @@ public async Task GenerateAction_NonGeneric_Ok() { var handler = FallbackHelper.CreateHandler(_ => true, () => ((object)"secondary").AsOutcome(), false); var context = ResilienceContext.Get(); - var outcome = await handler.GetFallbackOutcomeAsync(new OutcomeArguments(context, new Outcome("primary"), new HandleFallbackArguments()))!; + var outcome = await handler.GetFallbackOutcomeAsync(new OutcomeArguments(context, new Outcome("primary"), new FallbackPredicateArguments()))!; outcome.Result.Should().Be("secondary"); } diff --git a/test/Polly.Core.Tests/Fallback/FallbackHelper.cs b/test/Polly.Core.Tests/Fallback/FallbackHelper.cs index 60a3207f4d7..2ab9383cac5 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackHelper.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackHelper.cs @@ -11,7 +11,7 @@ public static FallbackHandler CreateHandler( bool isGeneric = true) { return new FallbackHandler( - PredicateInvoker.Create(args => new ValueTask(shouldHandle(args.Outcome!)), true)!, + PredicateInvoker.Create(args => new ValueTask(shouldHandle(args.Outcome!)), true)!, _ => fallback().AsValueTask(), isGeneric); } diff --git a/test/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs b/test/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs index a084e27ab7b..8242d0b99fb 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackStrategyOptionsTests.cs @@ -12,11 +12,23 @@ public void Ctor_EnsureDefaults() var options = new FallbackStrategyOptions(); options.StrategyType.Should().Be("Fallback"); - options.ShouldHandle.Should().BeNull(); + options.ShouldHandle.Should().NotBeNull(); options.OnFallback.Should().BeNull(); options.FallbackAction.Should().BeNull(); } + [Fact] + public async Task ShouldHandle_EnsureDefaults() + { + var options = new FallbackStrategyOptions(); + var args = new FallbackPredicateArguments(); + var context = ResilienceContext.Get(); + + (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] public void Validation() { diff --git a/test/Polly.Core.Tests/Hedging/Controller/HedgingControllerTests.cs b/test/Polly.Core.Tests/Hedging/Controller/HedgingControllerTests.cs index d125f155252..77455c3b6e1 100644 --- a/test/Polly.Core.Tests/Hedging/Controller/HedgingControllerTests.cs +++ b/test/Polly.Core.Tests/Hedging/Controller/HedgingControllerTests.cs @@ -18,8 +18,8 @@ public async Task Pooling_Ok() controller.RentedContexts.Should().Be(2); controller.RentedExecutions.Should().Be(2); - context1.Complete(); - context2.Complete(); + await context1.DisposeAsync(); + await context2.DisposeAsync(); controller.RentedContexts.Should().Be(0); controller.RentedExecutions.Should().Be(0); diff --git a/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs b/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs index 65833262be4..ea78a2a7aa3 100644 --- a/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs +++ b/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs @@ -309,7 +309,7 @@ public async Task Complete_EnsureOriginalContextPreparedWithAcceptedOutcome(bool context.Tasks.First(v => v.Type == type).AcceptOutcome(); // act - context.Complete(); + await context.DisposeAsync(); // assert _resilienceContext.Properties.Should().BeSameAs(originalProps); @@ -326,12 +326,12 @@ public async Task Complete_EnsureOriginalContextPreparedWithAcceptedOutcome(bool } [Fact] - public void Complete_NoTasks_EnsureCleaned() + public async Task Complete_NoTasks_EnsureCleaned() { var props = _resilienceContext.Properties; var context = Create(); context.Initialize(_resilienceContext); - context.Complete(); + await context.DisposeAsync(); _resilienceContext.Properties.Should().BeSameAs(props); } @@ -343,7 +343,7 @@ public async Task Complete_NoAcceptedTasks_ShouldNotThrow() ConfigureSecondaryTasks(TimeSpan.Zero); await ExecuteAllTasksAsync(context, 2); - context.Invoking(c => c.Complete()).Should().NotThrow(); + context.Invoking(c => c.DisposeAsync().AsTask().Wait()).Should().NotThrow(); } [Fact] @@ -356,7 +356,7 @@ public async Task Complete_MultipleAcceptedTasks_ShouldNotThrow() context.Tasks[0].AcceptOutcome(); context.Tasks[1].AcceptOutcome(); - context.Invoking(c => c.Complete()).Should().NotThrow(); + context.Invoking(c => c.DisposeAsync().AsTask().Wait()).Should().NotThrow(); } [Fact] @@ -386,7 +386,7 @@ public async Task Complete_EnsurePendingTasksCleaned() pending.Wait(10).Should().BeFalse(); context.Tasks[0].AcceptOutcome(); - context.Complete(); + await context.DisposeAsync(); await pending; @@ -403,7 +403,7 @@ public async Task Complete_EnsureCleaned() await ExecuteAllTasksAsync(context, 2); context.Tasks[0].AcceptOutcome(); - context.Complete(); + await context.DisposeAsync(); context.LoadedTasks.Should().Be(0); context.Snapshot.Context.Should().BeNull(); diff --git a/test/Polly.Core.Tests/Hedging/HandleHedgingArgumentsTests.cs b/test/Polly.Core.Tests/Hedging/HandleHedgingArgumentsTests.cs deleted file mode 100644 index ae31745ae9f..00000000000 --- a/test/Polly.Core.Tests/Hedging/HandleHedgingArgumentsTests.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Polly.Hedging; - -namespace Polly.Core.Tests.Hedging; - -public class HandleHedgingArgumentsTests -{ - [Fact] - public void Ctor_Ok() - { - this.Invoking(_ => new HandleHedgingArguments()).Should().NotThrow(); - } -} diff --git a/test/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs b/test/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs index 45bc207478a..b24be55c7de 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs @@ -11,7 +11,7 @@ public class HedgingHandlerTests public async Task GenerateAction_Generic_Ok() { var handler = new HedgingHandler( - PredicateInvoker.Create(args => PredicateResult.True, true)!, + PredicateInvoker.Create(args => PredicateResult.True, true)!, args => () => "ok".AsOutcomeAsync(), true); @@ -27,7 +27,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)!, + PredicateInvoker.Create(args => PredicateResult.True, false)!, args => { if (nullAction) @@ -55,7 +55,7 @@ public async Task GenerateAction_NonGeneric_Ok(bool nullAction) public async Task GenerateAction_NonGeneric_FromCallback() { var handler = new HedgingHandler( - PredicateInvoker.Create(args => PredicateResult.True, false)!, + PredicateInvoker.Create(args => PredicateResult.True, false)!, args => () => args.Callback(args.ActionContext), false); diff --git a/test/Polly.Core.Tests/Hedging/HedgingHelper.cs b/test/Polly.Core.Tests/Hedging/HedgingHelper.cs index c0aa3ada40e..75950b0f965 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingHelper.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingHelper.cs @@ -11,7 +11,7 @@ public static HedgingHandler CreateHandler( Func, Func>>?> generator) { return new HedgingHandler( - PredicateInvoker.Create(args => new ValueTask(shouldHandle(args.Outcome!)), true)!, + PredicateInvoker.Create(args => new ValueTask(shouldHandle(args.Outcome!)), true)!, generator, true); } diff --git a/test/Polly.Core.Tests/Hedging/HedgingPredicateArgumentsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingPredicateArgumentsTests.cs new file mode 100644 index 00000000000..ea2b821d3c1 --- /dev/null +++ b/test/Polly.Core.Tests/Hedging/HedgingPredicateArgumentsTests.cs @@ -0,0 +1,12 @@ +using Polly.Hedging; + +namespace Polly.Core.Tests.Hedging; + +public class HedgingPredicateArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + this.Invoking(_ => new HedgingPredicateArguments()).Should().NotThrow(); + } +} diff --git a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index ce007592b16..7f854df8c5d 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -72,10 +72,12 @@ public async Task Execute_CancellationRequested_Throws() var strategy = Create(); _cts.Cancel(); + var context = ResilienceContext.Get(); + context.CancellationToken = _cts.Token; - await strategy.Invoking(s => s.ExecuteAsync(_ => new ValueTask(Success), _cts.Token).AsTask()) - .Should() - .ThrowAsync(); + var outcome = await strategy.ExecuteOutcomeAsync((_, _) => "dummy".AsOutcomeAsync(), context, "state"); + outcome.Exception.Should().BeOfType(); + outcome.Exception!.StackTrace.Should().Contain("Execute_CancellationRequested_Throws"); } [InlineData(-1)] @@ -259,7 +261,7 @@ public async Task ExecuteAsync_EnsureDiscardedResultDisposed() var strategy = Create(handler); // act - var result = await strategy.ExecuteAsync(async token => + var resultTask = strategy.ExecuteAsync(async token => { #pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods await _timeProvider.Delay(LongDelay); @@ -271,6 +273,8 @@ public async Task ExecuteAsync_EnsureDiscardedResultDisposed() _timeProvider.Advance(LongDelay); await primaryResult.WaitForDisposalAsync(); + await resultTask; + primaryResult.IsDisposed.Should().BeTrue(); secondaryResult.IsDisposed.Should().BeFalse(); } diff --git a/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs index 9b3e82611de..b8bbbe67c17 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs @@ -7,7 +7,7 @@ namespace Polly.Core.Tests.Hedging; public class HedgingStrategyOptionsTests { [Fact] - public async Task Ctor_EnsureDefaults() + public void Ctor_EnsureDefaults() { var options = new HedgingStrategyOptions(); @@ -17,8 +17,31 @@ public async Task Ctor_EnsureDefaults() options.HedgingDelay.Should().Be(TimeSpan.FromSeconds(2)); options.MaxHedgedAttempts.Should().Be(2); options.OnHedging.Should().BeNull(); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task HedgingActionGenerator_EnsureDefaults(bool synchronous) + { + var options = new HedgingStrategyOptions(); + var context = ResilienceContext.Get().Initialize(synchronous); + var threadId = Thread.CurrentThread.ManagedThreadId; + + var action = options.HedgingActionGenerator(new HedgingActionGeneratorArguments(context, context, 1, c => + { + if (synchronous) + { + Thread.CurrentThread.ManagedThreadId.Should().NotBe(threadId); + } + else + { + Thread.CurrentThread.ManagedThreadId.Should().Be(threadId); + } + + return 99.AsOutcomeAsync(); + }))!; - var action = options.HedgingActionGenerator(new HedgingActionGeneratorArguments(ResilienceContext.Get(), ResilienceContext.Get(), 1, c => 99.AsOutcomeAsync()))!; action.Should().NotBeNull(); (await action()).Result.Should().Be(99); } @@ -27,7 +50,7 @@ public async Task Ctor_EnsureDefaults() public async Task ShouldHandle_EnsureDefaults() { var options = new HedgingStrategyOptions(); - var args = new HandleHedgingArguments(); + var args = new HedgingPredicateArguments(); var context = ResilienceContext.Get(); (await options.ShouldHandle(new(context, new Outcome(0), args))).Should().Be(false); diff --git a/test/Polly.Core.Tests/Issues/IssuesTests.FlowingContext_849.cs b/test/Polly.Core.Tests/Issues/IssuesTests.FlowingContext_849.cs index 351287fbd1b..9feb9bd5c9c 100644 --- a/test/Polly.Core.Tests/Issues/IssuesTests.FlowingContext_849.cs +++ b/test/Polly.Core.Tests/Issues/IssuesTests.FlowingContext_849.cs @@ -12,7 +12,7 @@ public void FlowingContext_849() .AddRetry(new RetryStrategyOptions { // configure the predicate and use the context - ShouldRetry = args => + ShouldHandle = args => { // access the context to evaluate the retry ResilienceContext context = args.Context; diff --git a/test/Polly.Core.Tests/Issues/IssuesTests.HandleMultipleResults_898.cs b/test/Polly.Core.Tests/Issues/IssuesTests.HandleMultipleResults_898.cs index c2b203d47e7..9e571250d75 100644 --- a/test/Polly.Core.Tests/Issues/IssuesTests.HandleMultipleResults_898.cs +++ b/test/Polly.Core.Tests/Issues/IssuesTests.HandleMultipleResults_898.cs @@ -13,7 +13,7 @@ public void HandleMultipleResults_898() BackoffType = RetryBackoffType.Constant, RetryCount = 1, BaseDelay = TimeSpan.FromMilliseconds(1), - ShouldRetry = args => args switch + ShouldHandle = args => args switch { // handle string results { Result: string res } when res == "error" => PredicateResult.True, diff --git a/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs b/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs index 0b1309fd7f0..22d111065f1 100644 --- a/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs +++ b/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs @@ -372,7 +372,7 @@ public void EnableReloads_Ok() builder.AddRetry(new RetryStrategyOptions { - ShouldRetry = _ => PredicateResult.True, + ShouldHandle = _ => PredicateResult.True, RetryCount = retryCount, BaseDelay = TimeSpan.FromMilliseconds(2), }); @@ -408,7 +408,7 @@ public void EnableReloads_Generic_Ok() builder.AddRetry(new RetryStrategyOptions { - ShouldRetry = _ => PredicateResult.True, + ShouldHandle = _ => PredicateResult.True, RetryCount = retryCount, BaseDelay = TimeSpan.FromMilliseconds(2), }); diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs index bce599a018c..1721de8383e 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyBuilderExtensionsTests.cs @@ -16,7 +16,7 @@ public class RetryResilienceStrategyBuilderExtensionsTests BackoffType = RetryBackoffType.Exponential, RetryCount = 3, BaseDelay = TimeSpan.FromSeconds(2), - ShouldRetry = _ => PredicateResult.True, + ShouldHandle = _ => PredicateResult.True, }); AssertStrategy(builder, RetryBackoffType.Exponential, 3, TimeSpan.FromSeconds(2)); @@ -37,7 +37,7 @@ public class RetryResilienceStrategyBuilderExtensionsTests BackoffType = RetryBackoffType.Exponential, RetryCount = 3, BaseDelay = TimeSpan.FromSeconds(2), - ShouldRetry = _ => PredicateResult.True + ShouldHandle = _ => PredicateResult.True }); AssertStrategy(builder, RetryBackoffType.Exponential, 3, TimeSpan.FromSeconds(2)); @@ -66,7 +66,7 @@ public void AddRetry_GenericOverloads_Ok(Action> public void AddRetry_DefaultOptions_Ok() { var builder = new ResilienceStrategyBuilder(); - var options = new RetryStrategyOptions { ShouldRetry = _ => PredicateResult.True }; + var options = new RetryStrategyOptions { ShouldHandle = _ => PredicateResult.True }; builder.AddRetry(options); @@ -99,12 +99,12 @@ private static void AssertStrategy(ResilienceStrategyBuilder builder, Retr public void AddRetry_InvalidOptions_Throws() { new ResilienceStrategyBuilder() - .Invoking(b => b.AddRetry(new RetryStrategyOptions { ShouldRetry = null! })) + .Invoking(b => b.AddRetry(new RetryStrategyOptions { ShouldHandle = null! })) .Should() .Throw(); new ResilienceStrategyBuilder() - .Invoking(b => b.AddRetry(new RetryStrategyOptions { ShouldRetry = null! })) + .Invoking(b => b.AddRetry(new RetryStrategyOptions { ShouldHandle = null! })) .Should() .Throw(); } diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs index e9654f75ca8..18e14b9dbec 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs @@ -15,7 +15,7 @@ public class RetryResilienceStrategyTests public RetryResilienceStrategyTests() { _telemetry = TestUtilities.CreateResilienceTelemetry(_diagnosticSource.Object); - _options.ShouldRetry = _ => new ValueTask(false); + _options.ShouldHandle = _ => new ValueTask(false); } [Fact] @@ -49,7 +49,7 @@ public async Task ExecuteAsync_CancellationRequestedAfterCallback_EnsureNotRetri { using var cancellationToken = new CancellationTokenSource(); - _options.ShouldRetry = _ => PredicateResult.True; + _options.ShouldHandle = _ => PredicateResult.True; _options.OnRetry = _ => { cancellationToken.Cancel(); @@ -73,7 +73,7 @@ public void ExecuteAsync_MultipleRetries_EnsureDiscardedResultsDisposed() _options.RetryCount = 5; SetupNoDelay(); _timeProvider.SetupAnyDelay(); - _options.ShouldRetry = _ => PredicateResult.True; + _options.ShouldHandle = _ => PredicateResult.True; var results = new List(); var sut = CreateSut(); @@ -99,7 +99,7 @@ public void Retry_RetryCount_Respected() { int calls = 0; _options.OnRetry = _ => { calls++; return default; }; - _options.ShouldRetry = args => args.Outcome.ResultPredicateAsync(0); + _options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0); _options.RetryCount = 12; SetupNoDelay(); var sut = CreateSut(); @@ -120,7 +120,7 @@ public void RetryException_RetryCount_Respected() return default; }; - _options.ShouldRetry = args => args.Outcome.ExceptionPredicateAsync(); + _options.ShouldHandle = args => args.Outcome.ExceptionPredicateAsync(); _options.RetryCount = 3; SetupNoDelay(); var sut = CreateSut(); @@ -145,7 +145,7 @@ public void Retry_Infinite_Respected() calls++; return default; }; - _options.ShouldRetry = args => args.Outcome.ResultPredicateAsync(0); + _options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0); _options.RetryCount = RetryStrategyOptions.InfiniteRetryCount; SetupNoDelay(); var sut = CreateSut(); @@ -160,7 +160,7 @@ public void RetryDelayGenerator_Respected() { int calls = 0; _options.OnRetry = _ => { calls++; return default; }; - _options.ShouldRetry = args => args.Outcome.ResultPredicateAsync(0); + _options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0); _options.RetryCount = 3; _options.BackoffType = RetryBackoffType.Constant; _options.RetryDelayGenerator = _ => new ValueTask(TimeSpan.FromMilliseconds(123)); @@ -188,7 +188,7 @@ public void OnRetry_EnsureCorrectArguments() return default; }; - _options.ShouldRetry = args => args.Outcome.ResultPredicateAsync(0); + _options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0); _options.RetryCount = 3; _options.BackoffType = RetryBackoffType.Linear; _timeProvider.SetupAnyDelay(); @@ -215,7 +215,7 @@ public void OnRetry_EnsureTelemetry() _diagnosticSource.Setup(v => v.IsEnabled("OnRetry")).Returns(true); - _options.ShouldRetry = args => args.Outcome.ResultPredicateAsync(0); + _options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0); _options.RetryCount = 3; _options.BackoffType = RetryBackoffType.Linear; _timeProvider.SetupAnyDelay(); @@ -243,7 +243,7 @@ public void RetryDelayGenerator_EnsureCorrectArguments() return new ValueTask(TimeSpan.Zero); }; - _options.ShouldRetry = args => args.Outcome.ResultPredicateAsync(0); + _options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0); _options.RetryCount = 3; _options.BackoffType = RetryBackoffType.Linear; _timeProvider.SetupAnyDelay(); @@ -270,7 +270,7 @@ private RetryResilienceStrategy CreateSut(TimeProvider? timeProvider = null) _options.BaseDelay, _options.BackoffType, _options.RetryCount, - PredicateInvoker.Create(_options.ShouldRetry!, false)!, + PredicateInvoker.Create(_options.ShouldHandle!, false)!, EventInvoker.Create(_options.OnRetry, false), GeneratorInvoker.Create(_options.RetryDelayGenerator, TimeSpan.MinValue, false), timeProvider ?? _timeProvider.Object, diff --git a/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs b/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs index 7242cb10b62..fa958f1cb23 100644 --- a/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryStrategyOptionsTests.cs @@ -12,7 +12,7 @@ public void Ctor_Ok() var options = new RetryStrategyOptions(); options.StrategyType.Should().Be("Retry"); - options.ShouldRetry.Should().NotBeNull(); + options.ShouldHandle.Should().NotBeNull(); options.RetryDelayGenerator.Should().BeNull(); @@ -27,12 +27,12 @@ public void Ctor_Ok() public async Task ShouldHandle_EnsureDefaults() { var options = new RetryStrategyOptions(); - var args = new ShouldRetryArguments(0); + var args = new RetryPredicateArguments(0); var context = ResilienceContext.Get(); - (await options.ShouldRetry(new(context, new Outcome(0), args))).Should().Be(false); - (await options.ShouldRetry(new(context, new Outcome(new OperationCanceledException()), args))).Should().Be(false); - (await options.ShouldRetry(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] @@ -40,7 +40,7 @@ public void InvalidOptions() { var options = new RetryStrategyOptions { - ShouldRetry = null!, + ShouldHandle = null!, RetryDelayGenerator = null!, OnRetry = null!, RetryCount = -3, @@ -56,7 +56,7 @@ Invalid Options Validation Errors: The field RetryCount must be between -1 and 100. The field BaseDelay must be >= to 00:00:00. - The ShouldRetry field is required. + The ShouldHandle field is required. """); } } diff --git a/test/Polly.Core.Tests/Retry/ShouldRetryArgumentsTests.cs b/test/Polly.Core.Tests/Retry/ShouldRetryArgumentsTests.cs index 5aef531e67f..f7ea700348b 100644 --- a/test/Polly.Core.Tests/Retry/ShouldRetryArgumentsTests.cs +++ b/test/Polly.Core.Tests/Retry/ShouldRetryArgumentsTests.cs @@ -7,7 +7,7 @@ public class ShouldRetryArgumentsTests [Fact] public void Ctor_Ok() { - var args = new ShouldRetryArguments(2); + var args = new RetryPredicateArguments(2); args.Attempt.Should().Be(2); } } diff --git a/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs b/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs index 6d512af19ab..9efcece2ae1 100644 --- a/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs @@ -105,6 +105,19 @@ await sut _timeProvider.VerifyAll(); } + [Fact] + public async Task Execute_Timeout_EnsureStackTrace() + { + using var cts = new CancellationTokenSource(); + SetTimeout(TimeSpan.FromSeconds(2)); + _timeProvider.SetupCancelAfterNow(TimeSpan.FromSeconds(2)); + var sut = CreateSut(); + + var outcome = await sut.ExecuteOutcomeAsync(async (c, _) => { await Delay(c.CancellationToken); return "dummy".AsOutcome(); }, ResilienceContext.Get(), "state"); + outcome.Exception.Should().BeOfType(); + outcome.Exception!.StackTrace.Should().Contain("Execute_Timeout_EnsureStackTrace"); + } + [Fact] public async Task Execute_Cancelled_EnsureNoTimeout() { diff --git a/test/Polly.Core.Tests/Utils/ExceptionUtilitiesTests.cs b/test/Polly.Core.Tests/Utils/ExceptionUtilitiesTests.cs new file mode 100644 index 00000000000..b804f466645 --- /dev/null +++ b/test/Polly.Core.Tests/Utils/ExceptionUtilitiesTests.cs @@ -0,0 +1,33 @@ +using System; +using Polly.Utils; + +namespace Polly.Core.Tests.Utils; + +public class ExceptionUtilitiesTests +{ + [Fact] + public void TrySetStackTrace_Ok() + { + var exception = new InvalidOperationException(); + + ExceptionUtilities.TrySetStackTrace(exception); + + exception.StackTrace.Should().Contain("ExceptionUtilitiesTests"); + } + + [Fact] + public void TrySetStackTrace_AlreadySet_NotOverwritten() + { + try + { + throw new InvalidOperationException(); + + } + catch (InvalidOperationException e) + { + var oldTrace = e.StackTrace; + ExceptionUtilities.TrySetStackTrace(e); + e.StackTrace.Should().Be(oldTrace); + } + } +} diff --git a/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs b/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs index 727160aaebb..ed5983c14c5 100644 --- a/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs +++ b/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs @@ -1,3 +1,4 @@ +using System.Net.Http; using Microsoft.Extensions.Logging; using Polly.Extensions.Telemetry; @@ -58,7 +59,8 @@ public void Write_InvalidType_Nothing() public void WriteEvent_LoggingWithOutcome_Ok(bool noOutcome) { var telemetry = Create(); - ReportEvent(telemetry, noOutcome ? null : new Outcome(true)); + using var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + ReportEvent(telemetry, noOutcome ? null : new Outcome(response)); var messages = _logger.GetRecords(new EventId(0, "ResilienceEvent")).ToList(); messages.Should().HaveCount(1); @@ -69,7 +71,7 @@ public void WriteEvent_LoggingWithOutcome_Ok(bool noOutcome) } else { - messages[0].Message.Should().Be("Resilience event occurred. EventName: 'my-event', Builder Name: 'my-builder', Strategy Name: 'my-strategy', Strategy Type: 'my-strategy-type', Strategy Key: 'my-strategy-key', Result: 'True'"); + messages[0].Message.Should().Be("Resilience event occurred. EventName: 'my-event', Builder Name: 'my-builder', Strategy Name: 'my-strategy', Strategy Type: 'my-strategy-type', Strategy Key: 'my-strategy-key', Result: '200'"); } } diff --git a/test/Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj b/test/Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj index 8762aa3aa85..eee95b966cc 100644 --- a/test/Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj +++ b/test/Polly.RateLimiting.Tests/Polly.RateLimiting.Tests.csproj @@ -8,6 +8,9 @@ $(NoWarn);SA1600;SA1204 [Polly.RateLimiting]* + + + diff --git a/test/Polly.RateLimiting.Tests/RateLimiterResilienceStrategyTests.cs b/test/Polly.RateLimiting.Tests/RateLimiterResilienceStrategyTests.cs index 24f4d8a07ba..ed6bbae9900 100644 --- a/test/Polly.RateLimiting.Tests/RateLimiterResilienceStrategyTests.cs +++ b/test/Polly.RateLimiting.Tests/RateLimiterResilienceStrategyTests.cs @@ -42,7 +42,7 @@ public void Execute_HappyPath() [InlineData(true, true)] [InlineData(true, false)] [Theory] - public void Execute_LeaseRejected(bool hasEvents, bool hasRetryAfter) + public async Task Execute_LeaseRejected(bool hasEvents, bool hasRetryAfter) { _diagnosticSource.Setup(v => v.IsEnabled("OnRateLimiterRejected")).Returns(true); _diagnosticSource.Setup(v => v.Write("OnRateLimiterRejected", It.Is(obj => obj != null))); @@ -70,13 +70,17 @@ public void Execute_LeaseRejected(bool hasEvents, bool hasRetryAfter) } var strategy = Create(); + var context = ResilienceContext.Get(); + context.CancellationToken = cts.Token; + var outcome = await strategy.ExecuteOutcomeAsync((_, _) => new ValueTask>(new Outcome("dummy")), context, "state"); - var assertion = strategy - .Invoking(s => s.Execute(_ => { }, cts.Token)) + outcome.Exception .Should() - .Throw() - .And - .RetryAfter.Should().Be((TimeSpan?)metadata); + .BeOfType().Subject + .RetryAfter + .Should().Be((TimeSpan?)metadata); + + outcome.Exception!.StackTrace.Should().Contain("Execute_LeaseRejected"); _limiter.VerifyAll(); _lease.VerifyAll(); diff --git a/test/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs b/test/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs index bc3c84107da..873b4550f28 100644 --- a/test/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs +++ b/test/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs @@ -160,7 +160,7 @@ public void RetryStrategy_AsSyncPolicy_Ok() var policy = new ResilienceStrategyBuilder() .AddRetry(new RetryStrategyOptions { - ShouldRetry = _ => PredicateResult.True, + ShouldHandle = _ => PredicateResult.True, BackoffType = RetryBackoffType.Constant, RetryCount = 5, BaseDelay = TimeSpan.FromMilliseconds(1)