From 2cbe4f0cf370c5e1ab98c345cd00f0c67701ba67 Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:06:59 +0000 Subject: [PATCH] Hedging strategy also deep-copies context for primary execution (#1754) --- ...nchmarks.HedgingBenchmark-report-github.md | 18 +++--- docs/strategies/hedging.md | 3 +- .../Hedging/Controller/ContextSnapshot.cs | 3 - .../Controller/HedgingExecutionContext.cs | 27 +++------ .../Hedging/Controller/TaskExecution.cs | 36 +++--------- src/Polly.Core/ResilienceContext.cs | 6 +- src/Polly.Core/ResilienceProperties.cs | 6 +- .../HedgingExecutionContextTests.cs | 10 +--- .../Hedging/Controller/TaskExecutionTests.cs | 58 +++++++------------ .../Hedging/HedgingResilienceStrategyTests.cs | 31 +++++----- .../ResilienceContextTests.cs | 7 ++- .../ResiliencePropertiesTests.cs | 20 ++----- 12 files changed, 77 insertions(+), 148 deletions(-) delete mode 100644 src/Polly.Core/Hedging/Controller/ContextSnapshot.cs diff --git a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.HedgingBenchmark-report-github.md b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.HedgingBenchmark-report-github.md index 78f1c349209..63a3b0635f8 100644 --- a/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.HedgingBenchmark-report-github.md +++ b/bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.HedgingBenchmark-report-github.md @@ -1,17 +1,17 @@ ``` -BenchmarkDotNet v0.13.7, Windows 11 (10.0.22621.2283/22H2/2022Update/SunValley2) (Hyper-V) +BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2) (Hyper-V) Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 16 logical and 8 physical cores -.NET SDK 7.0.401 - [Host] : .NET 7.0.11 (7.0.1123.42427), X64 RyuJIT AVX2 +.NET SDK 7.0.403 + [Host] : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2 Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15 LaunchCount=2 WarmupCount=10 ``` -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | -|---------------------------- |---------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| -| Hedging_Primary | 1.432 μs | 0.0042 μs | 0.0061 μs | 1.00 | 0.00 | - | 40 B | 1.00 | -| Hedging_Secondary | 2.253 μs | 0.0051 μs | 0.0075 μs | 1.57 | 0.01 | 0.0038 | 184 B | 4.60 | -| Hedging_Primary_AsyncWork | 3.903 μs | 0.0260 μs | 0.0381 μs | 2.73 | 0.03 | 0.0610 | 1636 B | 40.90 | -| Hedging_Secondary_AsyncWork | 4.936 μs | 0.0424 μs | 0.0595 μs | 3.45 | 0.05 | 0.0687 | 1838 B | 45.95 | +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|---------------------------- |----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| +| Hedging_Primary | 1.284 μs | 0.0047 μs | 0.0066 μs | 1.00 | 0.00 | - | 40 B | 1.00 | +| Hedging_Secondary | 2.007 μs | 0.0043 μs | 0.0064 μs | 1.56 | 0.01 | 0.0038 | 184 B | 4.60 | +| Hedging_Primary_AsyncWork | 33.379 μs | 1.9491 μs | 2.9173 μs | 26.43 | 1.90 | 0.6104 | 15299 B | 382.48 | +| Hedging_Secondary_AsyncWork | 33.735 μs | 0.2915 μs | 0.4273 μs | 26.28 | 0.43 | 0.6104 | 15431 B | 385.77 | diff --git a/docs/strategies/hedging.md b/docs/strategies/hedging.md index e07b8e9edd6..fcd710cb80c 100644 --- a/docs/strategies/hedging.md +++ b/docs/strategies/hedging.md @@ -449,8 +449,7 @@ The hedging strategy supports the concurrent execution and cancellation of multi Here's the flow: -- The strategy gets the primary context and creates a snapshot for deep cloning. -- For each hedged action execution, the hedging strategy makes a deep copy of the original context. The deep copy has its own cancellation token designated for that execution. Note that the first execution (primary) uses the original resilience context, albeit with a cloned set of resilience properties. +- The strategy gets the primary context and preserves it for deep-cloning. - After the strategy has an accepted result from a hedged action, the resilience context from the action is merged back into the primary context. - All ongoing hedged actions are cancelled and discarded. The hedging strategy awaits the propagation of cancellation. diff --git a/src/Polly.Core/Hedging/Controller/ContextSnapshot.cs b/src/Polly.Core/Hedging/Controller/ContextSnapshot.cs deleted file mode 100644 index 19ee70f1bfb..00000000000 --- a/src/Polly.Core/Hedging/Controller/ContextSnapshot.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Polly.Hedging.Utils; - -internal readonly record struct ContextSnapshot(ResilienceContext Context, ResilienceProperties OriginalProperties, CancellationToken OriginalCancellationToken); diff --git a/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs b/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs index ad7487ddfe7..157ec750c9c 100644 --- a/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs +++ b/src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs @@ -16,7 +16,6 @@ internal sealed class HedgingExecutionContext : IAsyncDisposable private readonly TimeProvider _timeProvider; private readonly int _maxAttempts; private readonly Action> _onReset; - private readonly ResilienceProperties _replacedProperties = new(); public HedgingExecutionContext( ObjectPool> executionPool, @@ -30,22 +29,17 @@ public HedgingExecutionContext( _onReset = onReset; } - internal void Initialize(ResilienceContext context) - { - Snapshot = new ContextSnapshot(context, context.Properties, context.CancellationToken); - _replacedProperties.Replace(Snapshot.OriginalProperties); - Snapshot.Context.Properties = _replacedProperties; - } + internal void Initialize(ResilienceContext context) => PrimaryContext = context; public int LoadedTasks => _tasks.Count; - public ContextSnapshot Snapshot { get; private set; } + public ResilienceContext? PrimaryContext { get; private set; } - public bool IsInitialized => Snapshot.Context != null; + public bool IsInitialized => PrimaryContext != null; public IReadOnlyList> Tasks => _tasks; - private bool ContinueOnCapturedContext => Snapshot.Context.ContinueOnCapturedContext; + private bool ContinueOnCapturedContext => PrimaryContext!.ContinueOnCapturedContext; public async ValueTask> LoadExecutionAsync( Func>> primaryCallback, @@ -65,7 +59,7 @@ public async ValueTask> LoadExecutionAsync( var execution = _executionPool.Get(); - if (await execution.InitializeAsync(type, Snapshot, primaryCallback, state, LoadedTasks).ConfigureAwait(ContinueOnCapturedContext)) + if (await execution.InitializeAsync(type, PrimaryContext!, primaryCallback, state, LoadedTasks).ConfigureAwait(ContinueOnCapturedContext)) { // we were able to start a new execution, register it _tasks.Add(execution); @@ -126,7 +120,7 @@ public async ValueTask DisposeAsync() return TryRemoveExecutedTask(); } - using var delayTaskCancellation = CancellationTokenSource.CreateLinkedTokenSource(Snapshot.Context.CancellationToken); + using var delayTaskCancellation = CancellationTokenSource.CreateLinkedTokenSource(PrimaryContext!.CancellationToken); var delayTask = _timeProvider.Delay(hedgingDelay, delayTaskCancellation.Token); Task whenAnyHedgedTask = WaitForTaskCompetitionAsync(); @@ -192,10 +186,6 @@ static async Task AwaitTask(TaskExecution task, bool continueOnCaptured private void UpdateOriginalContext() { - var originalContext = Snapshot.Context; - originalContext.CancellationToken = Snapshot.OriginalCancellationToken; - originalContext.Properties = Snapshot.OriginalProperties; - if (LoadedTasks == 0) { return; @@ -205,17 +195,16 @@ private void UpdateOriginalContext() if (Tasks.FirstOrDefault(static t => t.IsAccepted) is TaskExecution acceptedExecution) { - originalContext.Properties.Replace(acceptedExecution.Properties); + PrimaryContext!.Properties.AddOrReplaceProperties(acceptedExecution.Context.Properties); } } private void Reset() { - _replacedProperties.Clear(); _tasks.Clear(); _executingTasks.Clear(); - Snapshot = default; + PrimaryContext = null; _onReset(this); } diff --git a/src/Polly.Core/Hedging/Controller/TaskExecution.cs b/src/Polly.Core/Hedging/Controller/TaskExecution.cs index c3f8d129f2e..fee82e68081 100644 --- a/src/Polly.Core/Hedging/Controller/TaskExecution.cs +++ b/src/Polly.Core/Hedging/Controller/TaskExecution.cs @@ -55,8 +55,6 @@ public TaskExecution(HedgingHandler handler, CancellationTokenSourcePool canc public bool IsAccepted { get; private set; } - public ResilienceProperties Properties { get; } = new(); - public ResilienceContext Context => _activeContext ?? throw new InvalidOperationException("TaskExecution is not initialized."); public HedgedTaskType Type { get; set; } @@ -89,7 +87,7 @@ public void Cancel() public async ValueTask InitializeAsync( HedgedTaskType type, - ContextSnapshot snapshot, + ResilienceContext primaryContext, Func>> primaryCallback, TState state, int attemptNumber) @@ -98,22 +96,21 @@ public async ValueTask InitializeAsync( Type = type; _cancellationSource = _cancellationTokenSourcePool.Get(System.Threading.Timeout.InfiniteTimeSpan); _startExecutionTimestamp = _timeProvider.GetTimestamp(); - Properties.Replace(snapshot.OriginalProperties); + _activeContext = _cachedContext; + _activeContext.InitializeFrom(primaryContext, _cancellationSource!.Token); - if (snapshot.OriginalCancellationToken.CanBeCanceled) + if (primaryContext.CancellationToken.CanBeCanceled) { - _cancellationRegistration = snapshot.OriginalCancellationToken.Register(o => ((CancellationTokenSource)o!).Cancel(), _cancellationSource); + _cancellationRegistration = primaryContext.CancellationToken.Register(o => ((CancellationTokenSource)o!).Cancel(), _cancellationSource); } - PrepareContext(ref snapshot); - if (type == HedgedTaskType.Secondary) { Func>>? action = null; try { - action = _handler.GenerateAction(CreateArguments(primaryCallback, snapshot.Context, state, attemptNumber)); + action = _handler.GenerateAction(CreateArguments(primaryCallback, primaryContext, state, attemptNumber)); if (action == null) { await ResetAsync().ConfigureAwait(false); @@ -127,7 +124,7 @@ public async ValueTask InitializeAsync( return true; } - await HandleOnHedgingAsync(snapshot.Context, attemptNumber - 1).ConfigureAwait(Context.ContinueOnCapturedContext); + await HandleOnHedgingAsync(primaryContext, attemptNumber - 1).ConfigureAwait(Context.ContinueOnCapturedContext); ExecutionTaskSafe = ExecuteSecondaryActionAsync(action); } @@ -193,7 +190,6 @@ public async ValueTask ResetAsync() IsAccepted = false; Outcome = default; IsHandled = false; - Properties.Clear(); OnReset = null; AttemptNumber = 0; _activeContext = null; @@ -246,22 +242,4 @@ private async Task UpdateOutcomeAsync(Outcome outcome) IsHandled = await _handler.ShouldHandle(args).ConfigureAwait(Context.ContinueOnCapturedContext); TelemetryUtil.ReportExecutionAttempt(_telemetry, Context, outcome, AttemptNumber, ExecutionTime, IsHandled); } - - private void PrepareContext(ref ContextSnapshot snapshot) - { - if (Type == HedgedTaskType.Primary) - { - // now just replace the properties - _activeContext = snapshot.Context; - } - else - { - // secondary hedged tasks get their own unique context - _activeContext = _cachedContext; - _activeContext.InitializeFrom(snapshot.Context); - } - - _activeContext.Properties = Properties; - _activeContext.CancellationToken = _cancellationSource!.Token; - } } diff --git a/src/Polly.Core/ResilienceContext.cs b/src/Polly.Core/ResilienceContext.cs index d386b460bb0..ebeaca78615 100644 --- a/src/Polly.Core/ResilienceContext.cs +++ b/src/Polly.Core/ResilienceContext.cs @@ -61,15 +61,17 @@ internal ResilienceContext() /// /// Gets the custom properties attached to the context. /// - public ResilienceProperties Properties { get; internal set; } = new(); + public ResilienceProperties Properties { get; } = new(); - internal void InitializeFrom(ResilienceContext context) + internal void InitializeFrom(ResilienceContext context, CancellationToken cancellationToken) { OperationKey = context.OperationKey; ResultType = context.ResultType; IsSynchronous = context.IsSynchronous; CancellationToken = context.CancellationToken; ContinueOnCapturedContext = context.ContinueOnCapturedContext; + CancellationToken = cancellationToken; + Properties.AddOrReplaceProperties(context.Properties); } [ExcludeFromCodeCoverage] diff --git a/src/Polly.Core/ResilienceProperties.cs b/src/Polly.Core/ResilienceProperties.cs index 2a593960d66..8a62029d304 100644 --- a/src/Polly.Core/ResilienceProperties.cs +++ b/src/Polly.Core/ResilienceProperties.cs @@ -58,10 +58,8 @@ public void Set(ResiliencePropertyKey key, TValue value) Options[key.Key] = value; } - internal void Replace(ResilienceProperties other) + internal void AddOrReplaceProperties(ResilienceProperties other) { - Clear(); - // try to avoid enumerator allocation if (other.Options is Dictionary otherOptions) { @@ -78,7 +76,5 @@ internal void Replace(ResilienceProperties other) } } } - - internal void Clear() => Options.Clear(); } diff --git a/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs b/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs index 8c0a2700e2d..f00b23cc56c 100644 --- a/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs +++ b/test/Polly.Core.Tests/Hedging/Controller/HedgingExecutionContextTests.cs @@ -54,7 +54,7 @@ public void Ctor_Ok() var context = Create(); context.LoadedTasks.Should().Be(0); - context.Snapshot.Context.Should().BeNull(); + context.PrimaryContext.Should().BeNull(); context.Should().NotBeNull(); } @@ -67,11 +67,7 @@ public void Initialize_Ok() context.Initialize(_resilienceContext); - context.Snapshot.Context.Should().Be(_resilienceContext); - context.Snapshot.Context.Properties.Should().NotBeSameAs(props); - context.Snapshot.OriginalProperties.Should().BeSameAs(props); - context.Snapshot.OriginalCancellationToken.Should().Be(_cts.Token); - context.Snapshot.Context.Properties.Options.Should().HaveCount(1); + context.PrimaryContext.Should().Be(_resilienceContext); context.IsInitialized.Should().BeTrue(); } @@ -409,7 +405,7 @@ public async Task Complete_EnsureCleaned() await context.DisposeAsync(); context.LoadedTasks.Should().Be(0); - context.Snapshot.Context.Should().BeNull(); + context.PrimaryContext!.Should().BeNull(); _onReset.WaitOne(AssertTimeout); _resets.Count.Should().Be(1); diff --git a/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs b/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs index fa7ec8280c8..51c549fdbee 100644 --- a/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs +++ b/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs @@ -15,7 +15,7 @@ public class TaskExecutionTests : IDisposable private readonly HedgingTimeProvider _timeProvider; private readonly ResilienceStrategyTelemetry _telemetry; private readonly List _args = new(); - private ContextSnapshot _snapshot; + private ResilienceContext _primaryContext = ResilienceContextPool.Shared.Get(); public TaskExecutionTests() { @@ -47,10 +47,10 @@ public TaskExecutionTests() public async Task Initialize_Primary_Ok(string value, bool handled) { var execution = Create(); - await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot, + await execution.InitializeAsync(HedgedTaskType.Primary, _primaryContext, (context, state) => { - AssertPrimaryContext(context, execution); + AssertContext(context); state.Should().Be("dummy-state"); return Outcome.FromResultAsValueTask(new DisposableResult { Name = value }); }, @@ -60,7 +60,7 @@ await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot, await execution.ExecutionTaskSafe!; execution.Outcome.Result!.Name.Should().Be(value); execution.IsHandled.Should().Be(handled); - AssertPrimaryContext(execution.Context, execution); + AssertContext(execution.Context); _args.Should().HaveCount(1); _args[0].Handled.Should().Be(handled); @@ -71,7 +71,7 @@ await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot, public async Task Initialize_PrimaryCallbackThrows_EnsureExceptionHandled() { var execution = Create(); - await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot, + await execution.InitializeAsync(HedgedTaskType.Primary, _primaryContext, (_, _) => throw new InvalidOperationException(), "dummy-state", 1); @@ -89,18 +89,18 @@ public async Task Initialize_Secondary_Ok(string value, bool handled) var execution = Create(); Generator = args => { - AssertSecondaryContext(args.ActionContext, execution); + AssertContext(args.ActionContext); args.AttemptNumber.Should().Be(4); return () => Outcome.FromResultAsValueTask(new DisposableResult { Name = value }); }; - (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); + (await execution.InitializeAsync(HedgedTaskType.Secondary, _primaryContext, null!, "dummy-state", 4)).Should().BeTrue(); await execution.ExecutionTaskSafe!; execution.Outcome.Result!.Name.Should().Be(value); execution.IsHandled.Should().Be(handled); - AssertSecondaryContext(execution.Context, execution); + AssertContext(execution.Context); } [Fact] @@ -109,7 +109,7 @@ public async Task Initialize_SecondaryWhenTaskGeneratorReturnsNull_Ok() var execution = Create(); Generator = args => null; - (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeFalse(); + (await execution.InitializeAsync(HedgedTaskType.Secondary, _primaryContext, null!, "dummy-state", 4)).Should().BeFalse(); execution.Invoking(e => e.Context).Should().Throw(); } @@ -146,7 +146,7 @@ public async Task Initialize_SecondaryWhenTaskGeneratorThrows_EnsureOutcome() var execution = Create(); Generator = args => throw new FormatException(); - (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); + (await execution.InitializeAsync(HedgedTaskType.Secondary, _primaryContext, null!, "dummy-state", 4)).Should().BeTrue(); await execution.ExecutionTaskSafe!; execution.Outcome.Exception.Should().BeOfType(); @@ -158,7 +158,7 @@ public async Task Initialize_ExecutionTaskDoesNotThrows() var execution = Create(); Generator = args => throw new FormatException(); - (await execution.InitializeAsync(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue(); + (await execution.InitializeAsync(HedgedTaskType.Secondary, _primaryContext, null!, "dummy-state", 4)).Should().BeTrue(); await execution.ExecutionTaskSafe!.Invoking(async t => await t).Should().NotThrowAsync(); } @@ -180,7 +180,7 @@ public async Task Initialize_Cancelled_EnsureRespected(bool primary) var execution = Create(); - await execution.InitializeAsync(primary ? HedgedTaskType.Primary : HedgedTaskType.Secondary, _snapshot, + await execution.InitializeAsync(primary ? HedgedTaskType.Primary : HedgedTaskType.Secondary, _primaryContext, async (context, _) => { try @@ -223,6 +223,7 @@ public async Task ResetAsync_Ok(bool accept) var token = default(CancellationToken); await InitializePrimaryAsync(execution, result, context => token = context.CancellationToken); await execution.ExecutionTaskSafe!; + var context = execution.Context; if (accept) { @@ -235,7 +236,7 @@ public async Task ResetAsync_Ok(bool accept) // assert execution.IsAccepted.Should().BeFalse(); execution.IsHandled.Should().BeFalse(); - execution.Properties.Options.Should().HaveCount(0); + context.Properties.Options.Should().HaveCount(0); execution.Invoking(e => e.Context).Should().Throw(); #if !NETCOREAPP token.Invoking(t => t.WaitHandle).Should().Throw(); @@ -252,34 +253,18 @@ public async Task ResetAsync_Ok(bool accept) private async Task InitializePrimaryAsync(TaskExecution execution, DisposableResult? result = null, Action? onContext = null) { - await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot, (context, _) => + await execution.InitializeAsync(HedgedTaskType.Primary, _primaryContext, (context, _) => { onContext?.Invoke(context); return Outcome.FromResultAsValueTask(result ?? new DisposableResult { Name = Handled }); }, "dummy-state", 1); } - private void AssertPrimaryContext(ResilienceContext context, TaskExecution execution) + private void AssertContext(ResilienceContext context) { - context.IsInitialized.Should().BeTrue(); - context.Should().BeSameAs(_snapshot.Context); - context.Properties.Should().NotBeSameAs(_snapshot.OriginalProperties); - context.Properties.Should().BeSameAs(execution.Properties); - context.CancellationToken.Should().NotBeSameAs(_snapshot.OriginalCancellationToken); - context.CancellationToken.CanBeCanceled.Should().BeTrue(); - - context.Properties.Options.Should().HaveCount(1); - context.Properties.TryGetValue(_myKey, out var value).Should().BeTrue(); - value.Should().Be("dummy-value"); - } - - private void AssertSecondaryContext(ResilienceContext context, TaskExecution execution) - { - context.IsInitialized.Should().BeTrue(); - context.Should().NotBeSameAs(_snapshot.Context); - context.Properties.Should().NotBeSameAs(_snapshot.OriginalProperties); - context.Properties.Should().BeSameAs(execution.Properties); - context.CancellationToken.Should().NotBeSameAs(_snapshot.OriginalCancellationToken); + context.Should().NotBeSameAs(_primaryContext); + context.Properties.Should().NotBeSameAs(_primaryContext.Properties); + context.CancellationToken.Should().NotBeSameAs(_primaryContext.CancellationToken); context.CancellationToken.CanBeCanceled.Should().BeTrue(); context.Properties.Options.Should().HaveCount(1); @@ -289,9 +274,8 @@ private void AssertSecondaryContext(ResilienceContext context, TaskExecution(isSynchronous: false), new ResilienceProperties(), token ?? _cts.Token); - _snapshot.Context.CancellationToken = _cts.Token; - _snapshot.OriginalProperties.Set(_myKey, "dummy-value"); + _primaryContext = ResilienceContextPool.Shared.Get(token ?? _cts.Token); + _primaryContext.Properties.Set(_myKey, "dummy-value"); } private Func, Func>>?> Generator { get; set; } = args => diff --git a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index ea45fe7955f..37b13d8633a 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -356,7 +356,7 @@ public async Task ExecuteAsync_EveryHedgedTaskShouldHaveDifferentContexts() context.CancellationToken.CanBeCanceled.Should().BeTrue(); tokenHashCodes.Add(context.CancellationToken.GetHashCode()); context.Properties.GetValue(beforeKey, "wrong").Should().Be("before"); - context.Should().Be(primaryContext); + context.Should().NotBe(primaryContext); contexts.Add(context); await _timeProvider.Delay(LongDelay, context.CancellationToken); return "primary"; @@ -406,50 +406,47 @@ public async Task ExecuteAsync_EnsurePropertiesConsistency(bool primaryFails) ConfigureHedging(args => { - return async () => + return () => { contexts.Add(args.ActionContext); args.ActionContext.Properties.GetValue(primaryKey, string.Empty).Should().Be("primary"); args.ActionContext.Properties.Set(secondaryKey, "secondary"); - await _timeProvider.Delay(TimeSpan.FromHours(1), args.ActionContext.CancellationToken); - return Outcome.FromResult(primaryFails ? Success : Failure); + return Outcome.FromResultAsValueTask(primaryFails ? Success : Failure); }; }); var strategy = Create(); // act - var executeTask = strategy.ExecuteAsync( - async (context, _) => + await strategy.ExecuteAsync( + (context, _) => { + context.Should().NotBe(primaryContext); contexts.Add(context); context.Properties.GetValue(primaryKey, string.Empty).Should().Be("primary"); context.Properties.Set(primaryKey2, "primary-2"); - await _timeProvider.Delay(TimeSpan.FromHours(2), context.CancellationToken); - return primaryFails ? Failure : Success; + return new ValueTask(primaryFails ? Failure : Success); }, primaryContext, - "state").AsTask(); + "state"); // assert - contexts.Should().HaveCount(2); - primaryContext.Properties.Options.Should().HaveCount(2); primaryContext.Properties.GetValue(primaryKey, string.Empty).Should().Be("primary"); + primaryContext.Properties.Options.Should().HaveCount(2); + primaryContext.Properties.Should().BeSameAs(storedProps); if (primaryFails) { - _timeProvider.Advance(TimeSpan.FromHours(1)); - await executeTask; + contexts.Should().HaveCount(2); primaryContext.Properties.GetValue(secondaryKey, string.Empty).Should().Be("secondary"); + primaryContext.Properties.GetValue(primaryKey2, string.Empty).Should().BeEmpty(); } else { - _timeProvider.Advance(TimeSpan.FromHours(2)); - await executeTask; + contexts.Should().HaveCount(1); primaryContext.Properties.GetValue(primaryKey2, string.Empty).Should().Be("primary-2"); + primaryContext.Properties.GetValue(secondaryKey, string.Empty).Should().BeEmpty(); } - - primaryContext.Properties.Should().BeSameAs(storedProps); } [Fact] diff --git a/test/Polly.Core.Tests/ResilienceContextTests.cs b/test/Polly.Core.Tests/ResilienceContextTests.cs index 1dee2e62355..31dfc6f5337 100644 --- a/test/Polly.Core.Tests/ResilienceContextTests.cs +++ b/test/Polly.Core.Tests/ResilienceContextTests.cs @@ -25,9 +25,10 @@ public void Initialize_From_Ok(bool synchronous) var context = ResilienceContextPool.Shared.Get("some-key"); context.Initialize(synchronous); context.ContinueOnCapturedContext = true; - + context.Properties.Set(new ResiliencePropertyKey("A"), "B"); + using var cancellation = new CancellationTokenSource(); var other = ResilienceContextPool.Shared.Get(); - other.InitializeFrom(context); + other.InitializeFrom(context, cancellation.Token); other.ResultType.Should().Be(typeof(bool)); other.IsVoid.Should().BeFalse(); @@ -35,6 +36,8 @@ public void Initialize_From_Ok(bool synchronous) other.IsSynchronous.Should().Be(synchronous); other.ContinueOnCapturedContext.Should().BeTrue(); other.OperationKey.Should().Be("some-key"); + other.CancellationToken.Should().Be(cancellation.Token); + other.Properties.GetValue(new ResiliencePropertyKey("A"), string.Empty).Should().Be("B"); } [InlineData(true)] diff --git a/test/Polly.Core.Tests/ResiliencePropertiesTests.cs b/test/Polly.Core.Tests/ResiliencePropertiesTests.cs index ffeb0282cbe..e4ef2c31ba0 100644 --- a/test/Polly.Core.Tests/ResiliencePropertiesTests.cs +++ b/test/Polly.Core.Tests/ResiliencePropertiesTests.cs @@ -56,23 +56,10 @@ public void TryGetValue_IncorrectType_NotFound() props.TryGetValue(key2, out var val).Should().Be(false); } - [Fact] - public void Clear_Ok() - { - var key1 = new ResiliencePropertyKey("dummy"); - var key2 = new ResiliencePropertyKey("dummy"); - - var props = new ResilienceProperties(); - props.Set(key1, 12345); - props.Clear(); - - props.Options.Should().HaveCount(0); - } - [InlineData(true)] [InlineData(false)] [Theory] - public void Replace_Ok(bool isRawDictionary) + public void AddOrReplaceProperties_Ok(bool isRawDictionary) { var key1 = new ResiliencePropertyKey("A"); var key2 = new ResiliencePropertyKey("B"); @@ -88,8 +75,9 @@ public void Replace_Ok(bool isRawDictionary) otherProps.Set(key2, "B"); - props.Replace(otherProps); - props.Options.Should().HaveCount(1); + props.AddOrReplaceProperties(otherProps); + props.Options.Should().HaveCount(2); + props.GetValue(key1, "").Should().Be("A"); props.GetValue(key2, "").Should().Be("B"); } }