diff --git a/src/Polly.Core/Hedging/Controller/TaskExecution.cs b/src/Polly.Core/Hedging/Controller/TaskExecution.cs index a87798351db..354b7db27bd 100644 --- a/src/Polly.Core/Hedging/Controller/TaskExecution.cs +++ b/src/Polly.Core/Hedging/Controller/TaskExecution.cs @@ -239,7 +239,7 @@ private async Task ExecutePrimaryActionAsync(Func outcome) { - var args = new HedgingPredicateArguments(Context, outcome); + var args = new HedgingPredicateArguments(Context, outcome, AttemptNumber); Outcome = outcome; IsHandled = await _handler.ShouldHandle(args).ConfigureAwait(Context.ContinueOnCapturedContext); TelemetryUtil.ReportExecutionAttempt(_telemetry, Context, outcome, AttemptNumber, ExecutionTime, IsHandled); diff --git a/src/Polly.Core/Hedging/HedgingPredicateArguments.cs b/src/Polly.Core/Hedging/HedgingPredicateArguments.cs index e8f8a5ef489..52b3b26dd22 100644 --- a/src/Polly.Core/Hedging/HedgingPredicateArguments.cs +++ b/src/Polly.Core/Hedging/HedgingPredicateArguments.cs @@ -22,6 +22,15 @@ public HedgingPredicateArguments(ResilienceContext context, Outcome out Outcome = outcome; } + /// + /// Initializes a new instance of the struct. + /// + /// The context in which the resilience operation or event occurred. + /// The outcome of the resilience operation or event. + /// The zero-based attempt number. + public HedgingPredicateArguments(ResilienceContext context, Outcome outcome, int attemptNumber) + : this(context, outcome) => AttemptNumber = attemptNumber; + /// /// Gets the outcome of the user-specified callback. /// @@ -31,4 +40,9 @@ public HedgingPredicateArguments(ResilienceContext context, Outcome out /// Gets the context of this event. /// public ResilienceContext Context { get; } + + /// + /// Gets the zero-based attempt number. + /// + public int? AttemptNumber { get; } = null; } diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index ab058de62d4..ab781236285 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Polly.Hedging.HedgingPredicateArguments.AttemptNumber.get -> int? +Polly.Hedging.HedgingPredicateArguments.HedgingPredicateArguments(Polly.ResilienceContext! context, Polly.Outcome outcome, int attemptNumber) -> void diff --git a/test/Polly.Core.Tests/Hedging/HedgingPredicateArgumentsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingPredicateArgumentsTests.cs index 5c4ef399025..eb054df83d2 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingPredicateArgumentsTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingPredicateArgumentsTests.cs @@ -16,5 +16,21 @@ public static void Ctor_Ok() // Assert args.Context.ShouldBe(context); args.Outcome.Result.ShouldBe(1); + args.AttemptNumber.ShouldBeNull(); + } + + [Fact] + public static void Ctor_With_AttemptNumber_Ok() + { + // Arrange + var context = ResilienceContextPool.Shared.Get(); + + // Act + var args = new HedgingPredicateArguments(context, Outcome.FromResult(1), 10); + + // Assert + args.Context.ShouldBe(context); + args.Outcome.Result.ShouldBe(1); + args.AttemptNumber.ShouldBe(10); } } diff --git a/test/Polly.Core.Tests/Hedging/HedgingResiliencePipelineBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResiliencePipelineBuilderExtensionsTests.cs index d30f6dfa6f4..0774a6dd4be 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResiliencePipelineBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResiliencePipelineBuilderExtensionsTests.cs @@ -91,4 +91,40 @@ public async Task AddHedging_IntegrationTestWithRealDelay() var result = await strategy.ExecuteAsync(token => new ValueTask("error")); result.ShouldBe("success"); } + + [Fact] + public async Task AddHedging_AttemptNumbers_Are_Incremented() + { + const string Error = "error"; + const string Success = "success"; + int shouldHandleAttemptNumber = 0; + int actionGeneratorAttemptNumber = 1; + + var strategy = _builder.AddHedging(new() + { + MaxHedgedAttempts = 4, + ShouldHandle = args => + { + args.AttemptNumber.ShouldBe(shouldHandleAttemptNumber++); + return args.Outcome.Result switch + { + Error => PredicateResult.True(), + _ => PredicateResult.False() + }; + }, + ActionGenerator = args => + { + args.AttemptNumber.ShouldBe(actionGeneratorAttemptNumber++); + return async () => + { + await Task.Delay(20); + return Outcome.FromResult(args.AttemptNumber == 3 ? Success : Error); + }; + } + }) + .Build(); + + var result = await strategy.ExecuteAsync(token => new ValueTask(Error)); + result.ShouldBe(Success); + } } diff --git a/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs index ff8ed080d84..5ad6fe5ae04 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTests.cs @@ -57,9 +57,9 @@ public async Task ShouldHandle_EnsureDefaults() var options = new HedgingStrategyOptions(); var context = ResilienceContextPool.Shared.Get(); - (await options.ShouldHandle(new HedgingPredicateArguments(context, Outcome.FromResult(0)))).ShouldBe(false); - (await options.ShouldHandle(new HedgingPredicateArguments(context, Outcome.FromException(new OperationCanceledException())))).ShouldBe(false); - (await options.ShouldHandle(new HedgingPredicateArguments(context, Outcome.FromException(new InvalidOperationException())))).ShouldBe(true); + (await options.ShouldHandle(new HedgingPredicateArguments(context, Outcome.FromResult(0), 0))).ShouldBe(false); + (await options.ShouldHandle(new HedgingPredicateArguments(context, Outcome.FromException(new OperationCanceledException()), 1))).ShouldBe(false); + (await options.ShouldHandle(new HedgingPredicateArguments(context, Outcome.FromException(new InvalidOperationException()), 2))).ShouldBe(true); } [Fact] diff --git a/test/Polly.Core.Tests/PredicateBuilderTests.cs b/test/Polly.Core.Tests/PredicateBuilderTests.cs index d12f4098916..edb6baf14d4 100644 --- a/test/Polly.Core.Tests/PredicateBuilderTests.cs +++ b/test/Polly.Core.Tests/PredicateBuilderTests.cs @@ -117,7 +117,7 @@ public async Task Operator_HedgingStrategyOptions_Ok() ShouldHandle = new PredicateBuilder().HandleResult("error") }; - var handled = await options.ShouldHandle(new(context, CreateOutcome("error"))); + var handled = await options.ShouldHandle(new(context, CreateOutcome("error"), 0)); handled.ShouldBeTrue(); }