From bfc7c35c4ed360671ccc24496dbfeec80bafa742 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Thu, 8 Jun 2023 15:48:01 +0200 Subject: [PATCH 1/3] Specify default action generator for hedging --- .../HedgingExecutionContextTests.cs | 9 ++- .../Hedging/Controller/TaskExecutionTests.cs | 9 ++- .../Hedging/HedgingActions.cs | 23 +++--- .../Hedging/HedgingHandlerTests.cs | 24 +++---- ...esilienceStrategyBuilderExtensionsTests.cs | 6 +- .../Hedging/HedgingResilienceStrategyTests.cs | 57 +++++++-------- .../HedgingStrategyOptionsTResultTests.cs | 3 +- .../ResilienceStrategyBuilderTests.cs | 71 +++++++++++++++++++ .../Hedging/Controller/TaskExecution.cs | 9 ++- ...HedgingActionGeneratorArguments.TResult.cs | 6 +- .../Hedging/HedgingHandler.Handler.cs | 4 +- .../Hedging/HedgingHandler.TResult.cs | 2 +- src/Polly.Core/Hedging/HedgingHandler.cs | 8 +-- .../Hedging/HedgingStrategyOptions.TResult.cs | 10 ++- src/Polly.Core/Hedging/VoidHedgingHandler.cs | 2 +- src/Polly.Core/Outcome.cs | 1 + src/Polly.TestUtils/Outcome.cs | 12 ++++ .../TestResilienceStrategyOptions.cs | 2 +- 18 files changed, 180 insertions(+), 78 deletions(-) create mode 100644 src/Polly.TestUtils/Outcome.cs diff --git a/src/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs b/src/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs index 287cf584c72..6b3eefacceb 100644 --- a/src/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs +++ b/src/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs @@ -198,7 +198,7 @@ public async Task TryWaitForCompletedExecutionAsync_TwiceWhenSecondaryGeneratorR await LoadExecutionAsync(context); await LoadExecutionAsync(context); - Generator = args => () => Task.FromResult(new DisposableResult { Name = "secondary" }); + Generator = args => () => new DisposableResult { Name = "secondary" }.AsOutcomeAsync(); var task = await context.TryWaitForCompletedExecutionAsync(TimeSpan.Zero); task!.Type.Should().Be(HedgedTaskType.Primary); @@ -463,12 +463,15 @@ private void ConfigureSecondaryTasks(params TimeSpan[] delays) { args.Context.Properties.Set(new ResiliencePropertyKey(attempt.ToString(CultureInfo.InvariantCulture)), attempt); await _timeProvider.Delay(delays[attempt], args.Context.CancellationToken); - return new DisposableResult(delays[attempt].ToString()); + return new DisposableResult(delays[attempt].ToString()).AsOutcome(); }; }; } - private Func, Func>?> Generator { get; set; } = args => () => Task.FromResult(new DisposableResult { Name = Handled }); + private Func, Func>>?> Generator { get; set; } = args => + { + return () => new DisposableResult { Name = Handled }.AsOutcomeAsync(); + }; private HedgingExecutionContext Create() { diff --git a/src/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs b/src/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs index 1d1b78ec206..154e82b4a24 100644 --- a/src/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs +++ b/src/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs @@ -68,7 +68,7 @@ public async Task Initialize_Secondary_Ok(string value, bool handled) { AssertSecondaryContext(args.Context, execution); args.Attempt.Should().Be(4); - return () => Task.FromResult(new DisposableResult { Name = value }); + return () => new DisposableResult { Name = value }.AsOutcomeAsync(); }; (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); @@ -151,7 +151,7 @@ public async Task Initialize_Cancelled_EnsureRespected(bool primary) return async () => { await _timeProvider.Delay(TimeSpan.FromDays(1), args.Context.CancellationToken); - return new DisposableResult { Name = Handled }; + return new DisposableResult { Name = Handled }.AsOutcome(); }; }; @@ -271,7 +271,10 @@ private void CreateSnapshot(CancellationToken? token = null) _snapshot.OriginalProperties.Set(_myKey, "dummy-value"); } - private Func, Func>?> Generator { get; set; } = args => () => Task.FromResult(new DisposableResult { Name = Handled }); + private Func, Func>>?> Generator { get; set; } = args => + { + return () => new DisposableResult { Name = Handled }.AsOutcomeAsync(); + }; private TaskExecution Create() => new(_hedgingHandler.CreateHandler()!, CancellationTokenSourcePool.Create(TimeProvider.System)); } diff --git a/src/Polly.Core.Tests/Hedging/HedgingActions.cs b/src/Polly.Core.Tests/Hedging/HedgingActions.cs index 7fa78bb562a..0367152c579 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingActions.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingActions.cs @@ -32,31 +32,34 @@ public HedgingActions(TimeProvider timeProvider) }; } - public Func, Func>?> Generator { get; } + public Func, Func>>?> Generator { get; } - public Func, Func>?> EmptyFunctionsProvider { get; } = args => null; + public Func, Func>>?> EmptyFunctionsProvider { get; } = args => null; - public List>> Functions { get; } + public List>>> Functions { get; } - private async Task GetApples(ResilienceContext context) + private async ValueTask> GetApples(ResilienceContext context) { await _timeProvider.Delay(TimeSpan.FromSeconds(10), context.CancellationToken); - return "Apples"; + return "Apples".AsOutcome(); } - private async Task GetPears(ResilienceContext context) + private async ValueTask> GetPears(ResilienceContext context) { await _timeProvider.Delay(TimeSpan.FromSeconds(3), context.CancellationToken); - return "Pears"; + return "Pears".AsOutcome(); } - private async Task GetOranges(ResilienceContext context) + private async ValueTask> GetOranges(ResilienceContext context) { await _timeProvider.Delay(TimeSpan.FromSeconds(2), context.CancellationToken); - return "Oranges"; + return "Oranges".AsOutcome(); } - public static Func, Func>?> GetGenerator(Func> task) => args => () => task(args.Context); + public static Func, Func>>?> GetGenerator(Func>> task) + { + return args => () => task(args.Context); + } public int MaxHedgedTasks => Functions.Count + 1; } diff --git a/src/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs b/src/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs index ff553b50259..e4fca297026 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingHandlerTests.cs @@ -56,12 +56,12 @@ public void SetHedging_Empty_Discarded() .SetHedging(handler => { handler.ShouldHandle = _ => PredicateResult.True; - handler.HedgingActionGenerator = args => () => Task.FromResult(10); + handler.HedgingActionGenerator = args => () => 10.AsOutcomeAsync(); }) .SetVoidHedging(handler => { handler.ShouldHandle = _ => PredicateResult.True; - handler.HedgingActionGenerator = args => () => Task.CompletedTask; + handler.HedgingActionGenerator = args => () => default; }); handler.IsEmpty.Should().BeFalse(); @@ -75,7 +75,7 @@ public async Task SetHedging_Ok() var handler = new HedgingHandler() .SetHedging(handler => { - handler.HedgingActionGenerator = args => () => Task.FromResult(0); + handler.HedgingActionGenerator = args => () => 0.AsOutcomeAsync(); handler.ShouldHandle = args => new ValueTask(args.Result == -1); }) .CreateHandler(); @@ -87,11 +87,11 @@ public async Task SetHedging_Ok() handler.HandlesHedging().Should().BeTrue(); - var action = handler.TryCreateHedgedAction(context, 0); + var action = handler.TryCreateHedgedAction(context, 0, context => 0.AsOutcomeAsync()); action.Should().NotBeNull(); - (await (action!()!)).Should().Be(0); + (await (action!()!)).Result.Should().Be(0); - handler.TryCreateHedgedAction(context, 0).Should().BeNull(); + handler.TryCreateHedgedAction(context, 0, context => 0.0.AsOutcomeAsync()); } [InlineData(true)] @@ -111,7 +111,7 @@ public async Task SetVoidHedging_Ok(bool returnsNullAction) return null; } - return () => Task.CompletedTask; + return () => default; }; handler.ShouldHandle = args => new ValueTask(args.Exception is InvalidOperationException); }) @@ -124,7 +124,7 @@ public async Task SetVoidHedging_Ok(bool returnsNullAction) handler.HandlesHedging().Should().BeTrue(); - var action = handler.TryCreateHedgedAction(ResilienceContext.Get(), 0); + var action = handler.TryCreateHedgedAction(ResilienceContext.Get(), 0, _ => VoidResult.Instance.AsOutcomeAsync()); if (returnsNullAction) { action.Should().BeNull(); @@ -132,7 +132,7 @@ public async Task SetVoidHedging_Ok(bool returnsNullAction) else { action.Should().NotBeNull(); - (await (action!()!)).Should().Be(VoidResult.Instance); + (await (action!()!)).Result.Should().Be(VoidResult.Instance); } } @@ -143,7 +143,7 @@ public async Task ShouldHandleAsync_UnknownResultType_Null() var handler = new HedgingHandler() .SetHedging(handler => { - handler.HedgingActionGenerator = args => () => Task.FromResult(0); + handler.HedgingActionGenerator = args => () => new Outcome(0).AsValueTask(); handler.ShouldHandle = args => new ValueTask(args.Exception is InvalidOperationException); }) .SetHedging(handler => @@ -151,7 +151,7 @@ public async Task ShouldHandleAsync_UnknownResultType_Null() handler.HedgingActionGenerator = args => { args.Context.Should().NotBeNull(); - return () => Task.FromResult("dummy"); + return () => "dummy".AsOutcomeAsync(); }; handler.ShouldHandle = args => new ValueTask(args.Exception is InvalidOperationException); @@ -161,6 +161,6 @@ public async Task ShouldHandleAsync_UnknownResultType_Null() var args = new HandleHedgingArguments(); (await handler!.ShouldHandleAsync(new(context, new Outcome(new InvalidOperationException()), args))).Should().BeFalse(); handler.HandlesHedging().Should().BeFalse(); - handler.TryCreateHedgedAction(ResilienceContext.Get(), 0).Should().BeNull(); + handler.TryCreateHedgedAction(ResilienceContext.Get(), 0, _ => 0.0.AsOutcomeAsync()).Should().BeNull(); } } diff --git a/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyBuilderExtensionsTests.cs b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyBuilderExtensionsTests.cs index 1c1aa82016c..5e05bf9cb01 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyBuilderExtensionsTests.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyBuilderExtensionsTests.cs @@ -20,7 +20,7 @@ public void AddHedging_Generic_Ok() { _genericBuilder.AddHedging(new HedgingStrategyOptions { - HedgingActionGenerator = args => () => Task.FromResult("dummy"), + HedgingActionGenerator = args => () => "dummy".AsOutcomeAsync(), ShouldHandle = _ => PredicateResult.True }); _genericBuilder.Build().Strategy.Should().BeOfType(); @@ -73,10 +73,10 @@ public async Task AddHedging_IntegrationTest() if (args.Attempt == 3) { - return "success"; + return "success".AsOutcome(); } - return "error"; + return "error".AsOutcome(); }; }; }), diff --git a/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index 9ce4e7dfe71..d88891b7f20 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -1,5 +1,6 @@ using Polly.Hedging; using Polly.Telemetry; +using Polly.TestUtils; using Polly.Utils; using Xunit.Abstractions; @@ -143,7 +144,7 @@ public async void ExecuteAsync_EnsureHedgedTasksCancelled_Ok() } #pragma warning restore CA1031 // Do not catch general exception types - return Failure; + return Failure.AsOutcome(); }); var strategy = Create(); @@ -174,7 +175,7 @@ public async Task ExecuteAsync_EnsurePrimaryTaskCancelled_Ok() ConfigureHedging(async context => { await _timeProvider.Delay(TimeSpan.FromHours(1), context.CancellationToken); - return Success; + return Success.AsOutcome(); }); var strategy = Create(); @@ -213,7 +214,7 @@ public async Task ExecuteAsync_EnsureDiscardedResultDisposed() { return () => { - return Task.FromResult(secondaryResult); + return secondaryResult.AsOutcomeAsync(); }; }; @@ -264,7 +265,7 @@ public async Task ExecuteAsync_EveryHedgedTaskShouldHaveDifferentContexts() contexts.Add(args.Context); await Task.Yield(); args.Context.Properties.Set(afterKey, "after"); - return "secondary"; + return "secondary".AsOutcome(); }; }; @@ -336,7 +337,7 @@ public async Task ExecuteAsync_EnsurePropertiesConsistency(bool primaryFails) args.Context.Properties.GetValue(primaryKey, string.Empty).Should().Be("primary"); args.Context.Properties.Set(secondaryKey, "secondary"); await _timeProvider.Delay(TimeSpan.FromHours(1), args.Context.CancellationToken); - return primaryFails ? Success : Failure; + return (primaryFails ? Success : Failure).AsOutcome(); }; }); var strategy = Create(); @@ -423,7 +424,7 @@ public async Task ExecuteAsync_Secondary_CustomPropertiesAvailable() args.Context.Properties.TryGetValue(key2, out var val).Should().BeTrue(); val.Should().Be("my-value-2"); args.Context.Properties.Set(key, "my-value"); - return Task.FromResult(Success); + return Success.AsOutcomeAsync(); }; }); var strategy = Create(); @@ -442,7 +443,7 @@ public async Task ExecuteAsync_Secondary_CustomPropertiesAvailable() public async Task ExecuteAsync_OnHedgingEventThrows_EnsureExceptionRethrown() { // arrange - ConfigureHedging(args => () => Task.FromResult(Success)); + ConfigureHedging(args => () => Success.AsOutcomeAsync()); _options.OnHedging = _ => throw new InvalidOperationException("my-exception"); var strategy = Create(); @@ -471,13 +472,13 @@ public async Task ExecuteAsync_CancellationLinking_Ok() { await _timeProvider.Delay(TimeSpan.FromDays(0.5), context.CancellationToken); } - catch (OperationCanceledException) + catch (OperationCanceledException e) { secondaryCancelled.Set(); - throw; + return e.AsOutcome(); } - return Success; + return Success.AsOutcome(); }); var strategy = Create(); @@ -540,11 +541,11 @@ async ValueTask SlowTask(CancellationToken cancellationToken) return Success; } - Task BackgroundWork(ResilienceContext resilienceContext) + ValueTask> BackgroundWork(ResilienceContext resilienceContext) { var delay = Task.Delay(TimeSpan.FromDays(24), resilienceContext.CancellationToken); backgroundTasks.Add(delay); - return Task.FromResult(Success); + return Success.AsOutcomeAsync(); } } @@ -554,18 +555,18 @@ public async void ExecuteAsync_ZeroHedgingDelay_EnsureAllTasksSpawnedAtOnce() // arrange int executions = 0; using var allExecutionsReached = new ManualResetEvent(false); - ConfigureHedging(context => Execute(context.CancellationToken).AsTask()); + ConfigureHedging(context => Execute(context.CancellationToken)); _options.HedgingDelay = TimeSpan.Zero; // act - var task = Create().ExecuteAsync(Execute); + var task = Create().ExecuteAsync(async c => (await Execute(c)).Result, default); // assert Assert.True(allExecutionsReached.WaitOne(AssertTimeout)); _timeProvider.Advance(LongDelay); await task; - async ValueTask Execute(CancellationToken token) + async ValueTask> Execute(CancellationToken token) { if (Interlocked.Increment(ref executions) == _options.MaxHedgedAttempts) { @@ -573,7 +574,7 @@ async ValueTask Execute(CancellationToken token) } await _timeProvider.Delay(LongDelay, token); - return Success; + return Success.AsOutcome(); } } @@ -584,7 +585,7 @@ public void ExecuteAsync_InfiniteHedgingDelay_EnsureNoConcurrentExecutions() bool executing = false; int executions = 0; using var allExecutions = new ManualResetEvent(true); - ConfigureHedging(context => Execute(context.CancellationToken).AsTask()); + ConfigureHedging(context => Execute(context.CancellationToken)); // act var pending = Create().ExecuteAsync(Execute, _cts.Token); @@ -592,11 +593,11 @@ public void ExecuteAsync_InfiniteHedgingDelay_EnsureNoConcurrentExecutions() // assert Assert.True(allExecutions.WaitOne(AssertTimeout)); - async ValueTask Execute(CancellationToken token) + async ValueTask> Execute(CancellationToken token) { if (executing) { - throw new InvalidOperationException("Concurrent execution detected!"); + return new Outcome(new InvalidOperationException("Concurrent execution detected!")); } executing = true; @@ -609,7 +610,7 @@ async ValueTask Execute(CancellationToken token) await _timeProvider.Delay(LongDelay, token); - return "dummy"; + return "dummy".AsOutcome(); } finally { @@ -744,10 +745,10 @@ public async Task ExecuteAsync_ExceptionsHandled_ShouldReturnLastResult() { if (exception != null) { - throw exception; + return exception.AsOutcomeAsync(); } - return Task.FromResult(Success); + return Success.AsOutcomeAsync(); }; }; }); @@ -765,7 +766,7 @@ public async Task ExecuteAsync_EnsureHedgingDelayGeneratorRespected() ConfigureHedging(handler => { - handler.HedgingActionGenerator = args => () => Task.FromResult(Success); + handler.HedgingActionGenerator = args => () => Success.AsOutcomeAsync(); handler.ShouldHandle = _ => PredicateResult.False; }); @@ -814,7 +815,7 @@ public async Task ExecuteAsync_EnsureOnHedgingCalled() ConfigureHedging(handler => { handler.ShouldHandle = args => new ValueTask(args.Result == Failure); - handler.HedgingActionGenerator = args => () => Task.FromResult(Failure); + handler.HedgingActionGenerator = args => () => Failure.AsOutcomeAsync(); }); var strategy = Create(); @@ -833,7 +834,7 @@ public async Task ExecuteAsync_EnsureOnHedgingTelemetry() ConfigureHedging(handler => { handler.ShouldHandle = args => new ValueTask(args.Result == Failure); - handler.HedgingActionGenerator = args => () => Task.FromResult(Failure); + handler.HedgingActionGenerator = args => () => Failure.AsOutcomeAsync(); }); var strategy = Create(); @@ -862,12 +863,12 @@ private void ConfigureHedging() }); } - private void ConfigureHedging(Func> background) + private void ConfigureHedging(Func>> background) { ConfigureHedging(args => () => background(args.Context)); } - private void ConfigureHedging(Func, Func>?> generator) + private void ConfigureHedging(Func, Func>>?> generator) { ConfigureHedging(handler => { @@ -881,7 +882,7 @@ private void ConfigureHedging(Func, Func private void ConfigureHedging(TimeSpan delay) => ConfigureHedging(args => async () => { await Task.Delay(delay); - return "secondary"; + return "secondary".AsOutcome(); }); private HedgingResilienceStrategy Create() => new( diff --git a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs index 6598703ace2..4d9c637cc7a 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs @@ -13,7 +13,7 @@ public void Ctor_EnsureDefaults() options.StrategyType.Should().Be("Hedging"); options.ShouldHandle.Should().BeNull(); - options.HedgingActionGenerator.Should().BeNull(); + options.HedgingActionGenerator.Should().NotBeNull(); options.HedgingDelay.Should().Be(TimeSpan.FromSeconds(2)); options.MaxHedgedAttempts.Should().Be(2); options.OnHedging.Should().BeNull(); @@ -28,6 +28,7 @@ public void Validation() ShouldHandle = null!, MaxHedgedAttempts = -1, OnHedging = null!, + HedgingActionGenerator = null! }; options diff --git a/src/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs b/src/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs index af0e4231119..41f753022c9 100644 --- a/src/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs +++ b/src/Polly.Core.Tests/ResilienceStrategyBuilderTests.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Linq; using Polly.Utils; namespace Polly.Core.Tests; @@ -317,6 +318,52 @@ public void EmptyOptions_Ok() ResilienceStrategyBuilderExtensions.EmptyOptions.Instance.StrategyType.Should().Be("Empty"); } + [Fact] + public void ExecuteAsync_EnsureReceivedCallbackExecutesNextStrategy() + { + // arrange + var executions = new List(); + var first = new TestResilienceStrategy + { + Before = (_, _) => executions.Add("first-start"), + After = (_, _) => executions.Add("first-end"), + }; + + var second = new ExecuteCallbackTwiceStrategy + { + Before = () => executions.Add("second-start"), + After = () => executions.Add("second-end"), + }; + + var third = new TestResilienceStrategy + { + Before = (_, _) => executions.Add("third-start"), + After = (_, _) => executions.Add("third-end"), + }; + + var strategy = new ResilienceStrategyBuilder().AddStrategy(first).AddStrategy(second).AddStrategy(third).Build(); + + // act + strategy.Execute(_ => executions.Add("execute")); + + // assert + executions.SequenceEqual(new[] + { + "first-start", + "second-start", + "third-start", + "execute", + "third-end", + "third-start", + "execute", + "third-end", + "second-end", + "first-end", + }) + .Should() + .BeTrue(); + } + private class Strategy : ResilienceStrategy { public Action? Before { get; set; } @@ -340,6 +387,30 @@ protected internal override async ValueTask> ExecuteCoreAsync> ExecuteCoreAsync( + Func>> callback, + ResilienceContext context, + TState state) + { + try + { + Before?.Invoke(); + await callback(context, state); + return await callback(context, state); + } + finally + { + After?.Invoke(); + } + } + } + private class InvalidResilienceStrategyOptions : ResilienceStrategyOptions { [Required] diff --git a/src/Polly.Core/Hedging/Controller/TaskExecution.cs b/src/Polly.Core/Hedging/Controller/TaskExecution.cs index a809a989895..710c5aa3046 100644 --- a/src/Polly.Core/Hedging/Controller/TaskExecution.cs +++ b/src/Polly.Core/Hedging/Controller/TaskExecution.cs @@ -96,11 +96,11 @@ public async ValueTask InitializeAsync( if (type == HedgedTaskType.Secondary) { - Func>? action = null; + Func>>? action = null; try { - action = _handler.TryCreateHedgedAction(Context, attempt); + action = _handler.TryCreateHedgedAction(Context, attempt, (context) => primaryCallback(context, state)); if (action == null) { await ResetAsync().ConfigureAwait(false); @@ -163,14 +163,13 @@ public async ValueTask ResetAsync() _cancellationSource = null!; } - private async Task ExecuteSecondaryActionAsync(Func> action) + private async Task ExecuteSecondaryActionAsync(Func>> action) { Outcome outcome; try { - var result = await action().ConfigureAwait(Context.ContinueOnCapturedContext); - outcome = new Outcome(result); + outcome = await action().ConfigureAwait(Context.ContinueOnCapturedContext); } catch (Exception e) { diff --git a/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs b/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs index 3ed8708ff84..aa2f4487322 100644 --- a/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs @@ -1,9 +1,11 @@ namespace Polly.Hedging; /// -/// Represents arguments used in . +/// Represents arguments used in the hedging resilience strategy. /// /// The type of the result. /// The context associated with the execution of a user-provided callback. /// The zero-based hedging attempt number. -public readonly record struct HedgingActionGeneratorArguments(ResilienceContext Context, int Attempt); +/// The callback passed to hedging strategy. +/// +public readonly record struct HedgingActionGeneratorArguments(ResilienceContext Context, int Attempt, Func>> Callback); diff --git a/src/Polly.Core/Hedging/HedgingHandler.Handler.cs b/src/Polly.Core/Hedging/HedgingHandler.Handler.cs index 0441f1d61d6..df2eb7b9932 100644 --- a/src/Polly.Core/Hedging/HedgingHandler.Handler.cs +++ b/src/Polly.Core/Hedging/HedgingHandler.Handler.cs @@ -33,14 +33,14 @@ public ValueTask ShouldHandleAsync(OutcomeArguments>? TryCreateHedgedAction(ResilienceContext context, int attempt) + public Func>>? TryCreateHedgedAction(ResilienceContext context, int attempt, Func>> callback) { if (!_generators.TryGetValue(typeof(TResult), out var generator)) { return null; } - return ((Func, Func>?>)generator)(new HedgingActionGeneratorArguments(context, attempt)); + return ((Func, Func>>?>)generator)(new HedgingActionGeneratorArguments(context, attempt, callback)); } } } diff --git a/src/Polly.Core/Hedging/HedgingHandler.TResult.cs b/src/Polly.Core/Hedging/HedgingHandler.TResult.cs index 9613a981cbe..a5f8488692e 100644 --- a/src/Polly.Core/Hedging/HedgingHandler.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingHandler.TResult.cs @@ -33,5 +33,5 @@ internal sealed class HedgingHandler /// /// [Required] - public Func, Func>?>? HedgingActionGenerator { get; set; } = null; + public Func, Func>>?>? HedgingActionGenerator { get; set; } = null; } diff --git a/src/Polly.Core/Hedging/HedgingHandler.cs b/src/Polly.Core/Hedging/HedgingHandler.cs index 15e943f46e9..9ab776a7bd6 100644 --- a/src/Polly.Core/Hedging/HedgingHandler.cs +++ b/src/Polly.Core/Hedging/HedgingHandler.cs @@ -67,12 +67,12 @@ internal Handler CreateHandler() _actions.ToDictionary(pair => pair.Key, pair => pair.Value)); } - private static Func, Func>?> CreateGenericGenerator( - Func?> generator) + private static Func, Func>>?> CreateGenericGenerator( + Func?> generator) { return (args) => { - Func? action = generator(new HedgingActionGeneratorArguments(args.Context, args.Attempt)); + Func? action = generator(new HedgingActionGeneratorArguments(args.Context, args.Attempt)); if (action == null) { return null; @@ -81,7 +81,7 @@ internal Handler CreateHandler() return async () => { await action().ConfigureAwait(args.Context.ContinueOnCapturedContext); - return VoidResult.Instance; + return new Outcome(VoidResult.Instance); }; }; } diff --git a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs index 3df5902fcf3..44ec334f2b6 100644 --- a/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingStrategyOptions.TResult.cs @@ -54,10 +54,16 @@ public class HedgingStrategyOptions : ResilienceStrategyOptions /// Gets or sets the hedging action generator that creates hedged actions. /// /// - /// This property is required. Defaults to . + /// This property is required. The default delegate executes the original callback that was passed to the hedging resilience strategy. /// [Required] - public Func, Func>?>? HedgingActionGenerator { get; set; } = null; + public Func, Func>>?> HedgingActionGenerator { get; set; } = args => + { + return async () => + { + return await args.Callback(args.Context).ConfigureAwait(args.Context.ContinueOnCapturedContext); + }; + }; /// /// Gets or sets the generator that generates hedging delays for each hedging attempt. diff --git a/src/Polly.Core/Hedging/VoidHedgingHandler.cs b/src/Polly.Core/Hedging/VoidHedgingHandler.cs index 68222076fca..7695b0dc15a 100644 --- a/src/Polly.Core/Hedging/VoidHedgingHandler.cs +++ b/src/Polly.Core/Hedging/VoidHedgingHandler.cs @@ -32,5 +32,5 @@ internal sealed class VoidHedgingHandler /// /// [Required] - public Func?>? HedgingActionGenerator { get; set; } = null; + public Func?>? HedgingActionGenerator { get; set; } = null; } diff --git a/src/Polly.Core/Outcome.cs b/src/Polly.Core/Outcome.cs index a0dd40d5996..657d2102ad8 100644 --- a/src/Polly.Core/Outcome.cs +++ b/src/Polly.Core/Outcome.cs @@ -1,5 +1,6 @@ #pragma warning disable CA1815 // Override equals and operator equals on value types +using System; using System.Runtime.ExceptionServices; namespace Polly; diff --git a/src/Polly.TestUtils/Outcome.cs b/src/Polly.TestUtils/Outcome.cs new file mode 100644 index 00000000000..a28374f2c5b --- /dev/null +++ b/src/Polly.TestUtils/Outcome.cs @@ -0,0 +1,12 @@ +namespace Polly.TestUtils; + +public static class Outcome +{ + public static Outcome AsOutcome(this TResult value) => new(value); + + public static Outcome AsOutcome(this Exception error) => new(error); + + public static ValueTask> AsOutcomeAsync(this TResult value) => AsOutcome(value).AsValueTask(); + + public static ValueTask> AsOutcomeAsync(this Exception error) => AsOutcome(error).AsValueTask(); +} diff --git a/src/Polly.TestUtils/TestResilienceStrategyOptions.cs b/src/Polly.TestUtils/TestResilienceStrategyOptions.cs index 77d6d46a9ae..4e280c71ee3 100644 --- a/src/Polly.TestUtils/TestResilienceStrategyOptions.cs +++ b/src/Polly.TestUtils/TestResilienceStrategyOptions.cs @@ -1,4 +1,4 @@ -namespace Polly.TestUtils; +namespace Polly.TestUtils; public sealed class TestResilienceStrategyOptions : ResilienceStrategyOptions { From 659693eeb0707b79fa37ebbb5eb75168049f1982 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Thu, 8 Jun 2023 15:57:32 +0200 Subject: [PATCH 2/3] build fix --- src/Polly.Core.Benchmarks/Utils/Helper.Hedging.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Polly.Core.Benchmarks/Utils/Helper.Hedging.cs b/src/Polly.Core.Benchmarks/Utils/Helper.Hedging.cs index 19cb1bda322..20ef51d4313 100644 --- a/src/Polly.Core.Benchmarks/Utils/Helper.Hedging.cs +++ b/src/Polly.Core.Benchmarks/Utils/Helper.Hedging.cs @@ -13,7 +13,7 @@ public static ResilienceStrategy CreateHedging() builder.AddHedging(new HedgingStrategyOptions { ShouldHandle = args => new ValueTask(args.Result == Failure), - HedgingActionGenerator = args => () => Task.FromResult("hedged response"), + HedgingActionGenerator = args => () => new ValueTask>(new Outcome("hedged response")), }); }); } From d43f75504df0ea6bb505ed75d1481e5c7858b337 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Thu, 8 Jun 2023 16:23:08 +0200 Subject: [PATCH 3/3] fixes --- .../Hedging/HedgingStrategyOptionsTResultTests.cs | 6 +++++- .../Hedging/HedgingActionGeneratorArguments.TResult.cs | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs index 4d9c637cc7a..f731e36cd9e 100644 --- a/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs +++ b/src/Polly.Core.Tests/Hedging/HedgingStrategyOptionsTResultTests.cs @@ -7,7 +7,7 @@ namespace Polly.Core.Tests.Hedging; public class HedgingStrategyOptionsTResultTests { [Fact] - public void Ctor_EnsureDefaults() + public async Task Ctor_EnsureDefaults() { var options = new HedgingStrategyOptions(); @@ -17,6 +17,10 @@ public void Ctor_EnsureDefaults() options.HedgingDelay.Should().Be(TimeSpan.FromSeconds(2)); options.MaxHedgedAttempts.Should().Be(2); options.OnHedging.Should().BeNull(); + + var action = options.HedgingActionGenerator(new HedgingActionGeneratorArguments(ResilienceContext.Get(), 1, c => 99.AsOutcomeAsync()))!; + action.Should().NotBeNull(); + (await action()).Result.Should().Be(99); } [Fact] diff --git a/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs b/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs index aa2f4487322..09c2616ff22 100644 --- a/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs +++ b/src/Polly.Core/Hedging/HedgingActionGeneratorArguments.TResult.cs @@ -7,5 +7,4 @@ namespace Polly.Hedging; /// The context associated with the execution of a user-provided callback. /// The zero-based hedging attempt number. /// The callback passed to hedging strategy. -/// public readonly record struct HedgingActionGeneratorArguments(ResilienceContext Context, int Attempt, Func>> Callback);