diff --git a/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.BridgeBenchmark-report-github.md b/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.BridgeBenchmark-report-github.md new file mode 100644 index 00000000000..29f4bb112e8 --- /dev/null +++ b/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.BridgeBenchmark-report-github.md @@ -0,0 +1,15 @@ +``` ini + +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/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.302 + [Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2 + +Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15 +LaunchCount=2 WarmupCount=10 + +``` +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|----------------------- |----------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| +| NoOpAsync | 85.38 ns | 1.001 ns | 1.371 ns | 1.00 | 0.00 | 0.0120 | 304 B | 1.00 | +| NullResilienceStrategy | 416.38 ns | 1.924 ns | 2.820 ns | 4.87 | 0.10 | 0.0148 | 376 B | 1.24 | diff --git a/src/Polly.Core.Benchmarks/BridgeBenchmark.cs b/src/Polly.Core.Benchmarks/BridgeBenchmark.cs new file mode 100644 index 00000000000..7b104db7ff9 --- /dev/null +++ b/src/Polly.Core.Benchmarks/BridgeBenchmark.cs @@ -0,0 +1,22 @@ +using BenchmarkDotNet.Attributes; + +namespace Polly.Core.Benchmarks; + +public class BridgeBenchmark +{ + private IAsyncPolicy? _policy; + private IAsyncPolicy? _policyWrapped; + + [GlobalSetup] + public void Setup() + { + _policy = Policy.NoOpAsync(); + _policyWrapped = NullResilienceStrategy.Instance.AsAsyncPolicy(); + } + + [Benchmark(Baseline = true)] + public Task NoOpAsync() => _policy!.ExecuteAsync(() => Task.FromResult("dummy")); + + [Benchmark] + public Task NullResilienceStrategy() => _policyWrapped!.ExecuteAsync(() => Task.FromResult("dummy")); +} diff --git a/src/Polly.Core.Tests/Utils/LegacySupportTests.cs b/src/Polly.Core.Tests/Utils/LegacySupportTests.cs new file mode 100644 index 00000000000..11134cd4901 --- /dev/null +++ b/src/Polly.Core.Tests/Utils/LegacySupportTests.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Polly.Utils; + +namespace Polly.Core.Tests.Utils; + +public class LegacySupportTests +{ + [Fact] + public void SetProperties_Ok() + { + var resilienceProperties = new ResilienceProperties(); + var oldProps = resilienceProperties.Options; + var newProps = new Dictionary(); + + resilienceProperties.SetProperties(newProps, out var oldProperties2); + + resilienceProperties.Options.Should().BeSameAs(newProps); + oldProperties2.Should().BeSameAs(oldProps); + } +} diff --git a/src/Polly.Core/ResilienceProperties.cs b/src/Polly.Core/ResilienceProperties.cs index 6e04ba7e812..05e24386cd3 100644 --- a/src/Polly.Core/ResilienceProperties.cs +++ b/src/Polly.Core/ResilienceProperties.cs @@ -9,7 +9,7 @@ namespace Polly; /// public sealed class ResilienceProperties : IDictionary { - private Dictionary Options { get; } = new(); + internal IDictionary Options { get; set; } = new Dictionary(); /// /// Gets the value of a given property. @@ -87,26 +87,26 @@ internal void Replace(ResilienceProperties other) int ICollection>.Count => Options.Count; /// - bool ICollection>.IsReadOnly => ((IDictionary)Options).IsReadOnly; + bool ICollection>.IsReadOnly => Options.IsReadOnly; /// void IDictionary.Add(string key, object? value) => Options.Add(key, value); /// - void ICollection>.Add(KeyValuePair item) => ((IDictionary)Options).Add(item); + void ICollection>.Add(KeyValuePair item) => Options.Add(item); /// void ICollection>.Clear() => Options.Clear(); /// - bool ICollection>.Contains(KeyValuePair item) => ((IDictionary)Options).Contains(item); + bool ICollection>.Contains(KeyValuePair item) => Options.Contains(item); /// bool IDictionary.ContainsKey(string key) => Options.ContainsKey(key); /// void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => - ((IDictionary)Options).CopyTo(array, arrayIndex); + Options.CopyTo(array, arrayIndex); /// IEnumerator> IEnumerable>.GetEnumerator() => Options.GetEnumerator(); @@ -118,7 +118,7 @@ internal void Replace(ResilienceProperties other) bool IDictionary.Remove(string key) => Options.Remove(key); /// - bool ICollection>.Remove(KeyValuePair item) => ((IDictionary)Options).Remove(item); + bool ICollection>.Remove(KeyValuePair item) => Options.Remove(item); /// bool IDictionary.TryGetValue(string key, out object? value) => Options.TryGetValue(key, out value); diff --git a/src/Polly.Core/Utils/LegacySupport.cs b/src/Polly.Core/Utils/LegacySupport.cs new file mode 100644 index 00000000000..76e2226cffe --- /dev/null +++ b/src/Polly.Core/Utils/LegacySupport.cs @@ -0,0 +1,35 @@ +using System.ComponentModel; + +namespace Polly.Utils; + +/// +/// Legacy support for older versions of Polly. +/// +/// +/// This class is used by the legacy Polly infrastructure and should not be used directly by user code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class LegacySupport +{ + /// + /// Changes the underlying properties of a instance. + /// + /// The resilience properties. + /// The properties to use. + /// The old properties used by . + /// + /// This method is used by the legacy Polly infrastructure and should not be used directly by user code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static void SetProperties( + this ResilienceProperties resilienceProperties, + IDictionary properties, + out IDictionary oldProperties) + { + Guard.NotNull(resilienceProperties); + Guard.NotNull(properties); + + oldProperties = resilienceProperties.Options; + resilienceProperties.Options = properties; + } +} diff --git a/src/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs b/src/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs index d4228ba087b..316bd7572d7 100644 --- a/src/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs +++ b/src/Polly.Specs/ResilienceStrategyConversionExtensionsTests.cs @@ -1,5 +1,6 @@ using Polly.Strategy; using Polly.TestUtils; +using Polly.Utilities.Wrappers; namespace Polly.Specs; @@ -167,17 +168,20 @@ public void RetryStrategy_AsSyncPolicy_Ok() .Build() .AsSyncPolicy(); - var tries = 0; + var context = new Context(); + context["retry"] = 0; + policy.Execute( - () => + c => { - tries++; + c["retry"] = (int)c["retry"] + 1; return "dummy"; - }) + }, + context) .Should() .Be("dummy"); - tries.Should().Be(6); + context["retry"].Should().Be(6); } private static void AssertContext(Context context) diff --git a/src/Polly/Utilities/Wrappers/ResilienceContextFactory.cs b/src/Polly/Utilities/Wrappers/ResilienceContextFactory.cs index a22f72f1bc9..9972a6f3c48 100644 --- a/src/Polly/Utilities/Wrappers/ResilienceContextFactory.cs +++ b/src/Polly/Utilities/Wrappers/ResilienceContextFactory.cs @@ -1,42 +1,26 @@ +using Polly.Utils; + namespace Polly.Utilities.Wrappers; internal static class ResilienceContextFactory { - public static readonly ResiliencePropertyKey ContextKey = new("Polly.Legacy.Context"); - - public static ResilienceContext Create(Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) + public static ResilienceContext Create( + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext, + out IDictionary oldProperties) { var resilienceContext = ResilienceContext.Get(); resilienceContext.CancellationToken = cancellationToken; resilienceContext.ContinueOnCapturedContext = continueOnCapturedContext; - - foreach (var pair in context) - { - var props = (IDictionary)resilienceContext.Properties; - props.Add(pair.Key, pair.Value); - } - - resilienceContext.Properties.Set(ContextKey, context); + resilienceContext.Properties.SetProperties(context, out oldProperties); return resilienceContext; } - public static void Restore(ResilienceContext context) + public static void Cleanup(ResilienceContext resilienceContext, IDictionary oldProperties) { - var originalContext = context.GetContext(); - - foreach (var pair in context.Properties) - { - if (pair.Key == ContextKey.Key) - { - continue; - } - - originalContext[pair.Key] = pair.Value; - } - - ResilienceContext.Return(context); + resilienceContext.Properties.SetProperties(oldProperties, out _); + ResilienceContext.Return(resilienceContext); } - - public static Context GetContext(this ResilienceContext resilienceContext) => resilienceContext.Properties.GetValue(ContextKey, null!); } diff --git a/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.TResult.cs b/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.TResult.cs index afe761ded94..38afa49e75a 100644 --- a/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.TResult.cs +++ b/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.TResult.cs @@ -8,21 +8,25 @@ internal sealed class ResilienceStrategyAsyncPolicy : AsyncPolicy ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) { - var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, continueOnCapturedContext); + var resilienceContext = ResilienceContextFactory.Create( + context, + cancellationToken, + continueOnCapturedContext, + out var oldProperties); try { return await _strategy.ExecuteAsync( - static async (context, state) => + static async (resilienceContext, state) => { - return await state(context.GetContext(), context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + return await state.action(state.context, resilienceContext.CancellationToken).ConfigureAwait(resilienceContext.ContinueOnCapturedContext); }, resilienceContext, - action).ConfigureAwait(continueOnCapturedContext); + (action, context)).ConfigureAwait(continueOnCapturedContext); } finally { - ResilienceContextFactory.Restore(resilienceContext); + ResilienceContextFactory.Cleanup(resilienceContext, oldProperties); } } } diff --git a/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.cs b/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.cs index de385a6067a..041c5453637 100644 --- a/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.cs +++ b/src/Polly/Utilities/Wrappers/ResilienceStrategyAsyncPolicy.cs @@ -12,21 +12,25 @@ protected sealed override async Task ImplementationAsync( CancellationToken cancellationToken, bool continueOnCapturedContext) { - var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, continueOnCapturedContext); + var resilienceContext = ResilienceContextFactory.Create( + context, + cancellationToken, + continueOnCapturedContext, + out var oldProperties); try { await _strategy.ExecuteAsync( - static async (context, state) => + static async (resilienceContext, state) => { - await state(context.GetContext(), context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + await state.action(state.context, resilienceContext.CancellationToken).ConfigureAwait(resilienceContext.ContinueOnCapturedContext); }, resilienceContext, - action).ConfigureAwait(continueOnCapturedContext); + (action, context)).ConfigureAwait(continueOnCapturedContext); } finally { - ResilienceContextFactory.Restore(resilienceContext); + ResilienceContextFactory.Cleanup(resilienceContext, oldProperties); } } @@ -36,21 +40,25 @@ protected override async Task ImplementationAsync( CancellationToken cancellationToken, bool continueOnCapturedContext) { - var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, continueOnCapturedContext); + var resilienceContext = ResilienceContextFactory.Create( + context, + cancellationToken, + continueOnCapturedContext, + out var oldProperties); try { return await _strategy.ExecuteAsync( - static async (context, state) => + static async (resilienceContext, state) => { - return await state(context.GetContext(), context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + return await state.action(state.context, resilienceContext.CancellationToken).ConfigureAwait(resilienceContext.ContinueOnCapturedContext); }, resilienceContext, - action).ConfigureAwait(continueOnCapturedContext); + (action, context)).ConfigureAwait(continueOnCapturedContext); } finally { - ResilienceContextFactory.Restore(resilienceContext); + ResilienceContextFactory.Cleanup(resilienceContext, oldProperties); } } } diff --git a/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.TResult.cs b/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.TResult.cs index b77ba8a841e..e66d892e686 100644 --- a/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.TResult.cs +++ b/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.TResult.cs @@ -8,21 +8,25 @@ internal sealed class ResilienceStrategySyncPolicy : Policy protected sealed override TResult Implementation(Func action, Context context, CancellationToken cancellationToken) { - var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, true); + var resilienceContext = ResilienceContextFactory.Create( + context, + cancellationToken, + true, + out var oldProperties); try { return _strategy.Execute( static (context, state) => { - return state(context.GetContext(), context.CancellationToken); + return state.action(state.context, context.CancellationToken); }, resilienceContext, - action); + (action, context)); } finally { - ResilienceContextFactory.Restore(resilienceContext); + ResilienceContextFactory.Cleanup(resilienceContext, oldProperties); } } } diff --git a/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.cs b/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.cs index 997d2280bd7..915c977d2c9 100644 --- a/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.cs +++ b/src/Polly/Utilities/Wrappers/ResilienceStrategySyncPolicy.cs @@ -8,41 +8,49 @@ internal sealed class ResilienceStrategySyncPolicy : Policy protected override void Implementation(Action action, Context context, CancellationToken cancellationToken) { - var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, true); + var resilienceContext = ResilienceContextFactory.Create( + context, + cancellationToken, + true, + out var oldProperties); try { _strategy.Execute( static (context, state) => { - state(context.GetContext(), context.CancellationToken); + state.action(state.context, context.CancellationToken); }, resilienceContext, - action); + (action, context)); } finally { - ResilienceContextFactory.Restore(resilienceContext); + ResilienceContextFactory.Cleanup(resilienceContext, oldProperties); } } protected sealed override TResult Implementation(Func action, Context context, CancellationToken cancellationToken) { - var resilienceContext = ResilienceContextFactory.Create(context, cancellationToken, true); + var resilienceContext = ResilienceContextFactory.Create( + context, + cancellationToken, + true, + out var oldProperties); try { return _strategy.Execute( static (context, state) => { - return state(context.GetContext(), context.CancellationToken); + return state.action(state.context, context.CancellationToken); }, resilienceContext, - action); + (action, context)); } finally { - ResilienceContextFactory.Restore(resilienceContext); + ResilienceContextFactory.Cleanup(resilienceContext, oldProperties); } } }