diff --git a/src/Polly.Core.Tests/Registry/ResilienceStrategyProviderTests.cs b/src/Polly.Core.Tests/Registry/ResilienceStrategyProviderTests.cs new file mode 100644 index 00000000000..a759683608b --- /dev/null +++ b/src/Polly.Core.Tests/Registry/ResilienceStrategyProviderTests.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; +using Polly.Registry; + +namespace Polly.Core.Tests.Registry; + +public class ResilienceStrategyProviderTests +{ + [Fact] + public void Get_DoesNotExist_Throws() + { + new Provider() + .Invoking(o => o.Get("not-exists")) + .Should() + .Throw() + .WithMessage("Unable to find a resilience strategy associated with the key 'not-exists'. Please ensure that either the resilience strategy or the builder is registered."); + } + + [Fact] + public void Get_Exist_Ok() + { + var provider = new Provider { Strategy = new TestResilienceStrategy() }; + + provider.Get("exists").Should().Be(provider.Strategy); + } + + private class Provider : ResilienceStrategyProvider + { + public ResilienceStrategy? Strategy { get; set; } + + public override bool TryGet(string key, [NotNullWhen(true)] out ResilienceStrategy? strategy) + { + strategy = Strategy; + return Strategy != null; + } + } +} diff --git a/src/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs b/src/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs new file mode 100644 index 00000000000..754956a6305 --- /dev/null +++ b/src/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs @@ -0,0 +1,172 @@ +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using Polly.Builder; +using Polly.Registry; + +namespace Polly.Core.Tests.Registry; + +public class ResilienceStrategyRegistryTests +{ + private Action _callback = _ => { }; + + [Fact] + public void Ctor_Default_Ok() + { + this.Invoking(_ => new ResilienceStrategyRegistry()).Should().NotThrow(); + } + + [Fact] + public void Ctor_InvalidOptions_Throws() + { + this.Invoking(_ => new ResilienceStrategyRegistry(new ResilienceStrategyRegistryOptions { BuilderFactory = null! })) + .Should() + .Throw().WithMessage("The resilience strategy registry options are invalid.*"); + } + + [Fact] + public void Clear_Ok() + { + var registry = new ResilienceStrategyRegistry(); + + registry.TryAddBuilder("C", (_, b) => b.AddStrategy(new TestResilienceStrategy())); + + registry.TryAdd("A", new TestResilienceStrategy()); + registry.TryAdd("B", new TestResilienceStrategy()); + registry.TryAdd("C", new TestResilienceStrategy()); + + registry.Clear(); + + registry.TryGet("A", out _).Should().BeFalse(); + registry.TryGet("B", out _).Should().BeFalse(); + registry.TryGet("C", out _).Should().BeTrue(); + } + + [Fact] + public void Remove_Ok() + { + var registry = new ResilienceStrategyRegistry(); + + registry.TryAdd("A", new TestResilienceStrategy()); + registry.TryAdd("B", new TestResilienceStrategy()); + + registry.Remove("A").Should().BeTrue(); + registry.Remove("A").Should().BeFalse(); + + registry.TryGet("A", out _).Should().BeFalse(); + registry.TryGet("B", out _).Should().BeTrue(); + } + + [Fact] + public void RemoveBuilder_Ok() + { + var registry = new ResilienceStrategyRegistry(); + registry.TryAddBuilder("A", (_, b) => b.AddStrategy(new TestResilienceStrategy())); + + registry.RemoveBuilder("A").Should().BeTrue(); + registry.RemoveBuilder("A").Should().BeFalse(); + + registry.TryGet("A", out _).Should().BeFalse(); + } + + [Fact] + public void GetStrategy_BuilderMultiInstance_EnsureMultipleInstances() + { + var builderName = "A"; + var registry = CreateRegistry(); + var strategies = new HashSet(); + registry.TryAddBuilder(StrategyId.Create(builderName), (_, builder) => builder.AddStrategy(new TestResilienceStrategy())); + + for (int i = 0; i < 100; i++) + { + var key = StrategyId.Create(builderName, i.ToString(CultureInfo.InvariantCulture)); + + strategies.Add(registry.Get(key)); + + // call again, the strategy should be already cached + strategies.Add(registry.Get(key)); + } + + strategies.Should().HaveCount(100); + } + + [Fact] + public void AddBuilder_GetStrategy_EnsureCalled() + { + var activatorCalls = 0; + _callback = _ => activatorCalls++; + var registry = CreateRegistry(); + var called = 0; + registry.TryAddBuilder(StrategyId.Create("A"), (key, builder) => + { + builder.AddStrategy(new TestResilienceStrategy()); + builder.Options.Properties.Set(StrategyId.ResilienceKey, key); + called++; + }); + + var key1 = StrategyId.Create("A"); + var key2 = StrategyId.Create("A", "Instance1"); + var key3 = StrategyId.Create("A", "Instance2"); + var keys = new[] { key1, key2, key3 }; + var strategies = keys.ToDictionary(k => k, registry.Get); + foreach (var key in keys) + { + registry.Get(key); + } + + called.Should().Be(3); + activatorCalls.Should().Be(3); + strategies.Keys.Should().HaveCount(3); + } + + [Fact] + public void TryGet_NoBuilder_Null() + { + var registry = CreateRegistry(); + var key = StrategyId.Create("A"); + + registry.TryGet(key, out var strategy).Should().BeFalse(); + strategy.Should().BeNull(); + } + + [Fact] + public void TryGet_ExplicitStrategyAdded_Ok() + { + var expectedStrategy = new TestResilienceStrategy(); + var registry = CreateRegistry(); + var key = StrategyId.Create("A", "Instance"); + registry.TryAdd(key, expectedStrategy).Should().BeTrue(); + + registry.TryGet(key, out var strategy).Should().BeTrue(); + + strategy.Should().BeSameAs(expectedStrategy); + } + + [Fact] + public void TryAdd_Twice_SecondNotAdded() + { + var expectedStrategy = new TestResilienceStrategy(); + var registry = CreateRegistry(); + var key = StrategyId.Create("A", "Instance"); + registry.TryAdd(key, expectedStrategy); + + registry.TryAdd(key, new TestResilienceStrategy()).Should().BeFalse(); + + registry.TryGet(key, out var strategy).Should().BeTrue(); + strategy.Should().BeSameAs(expectedStrategy); + } + + private ResilienceStrategyRegistry CreateRegistry() + { + return new ResilienceStrategyRegistry(new ResilienceStrategyRegistryOptions + { + BuilderFactory = () => + { + var builder = new ResilienceStrategyBuilder(); + _callback(builder); + return builder; + }, + StrategyComparer = StrategyId.Comparer, + BuilderComparer = StrategyId.BuilderComparer + }); + } +} diff --git a/src/Polly.Core.Tests/Registry/StrategyId.cs b/src/Polly.Core.Tests/Registry/StrategyId.cs new file mode 100644 index 00000000000..738f52cd906 --- /dev/null +++ b/src/Polly.Core.Tests/Registry/StrategyId.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Polly.Core.Tests.Registry; + +public record StrategyId(Type Type, string BuilderName, string InstanceName = "") +{ + public static readonly ResiliencePropertyKey ResilienceKey = new("Polly.StrategyId"); + + public static StrategyId Create(string builderName, string instanceName = "") + => new(typeof(T), builderName, instanceName); + public static StrategyId Create(string builderName, string instanceName = "") + => new(typeof(StrategyId), builderName, instanceName); + + public static readonly IEqualityComparer Comparer = EqualityComparer.Default; + + public static readonly IEqualityComparer BuilderComparer = new BuilderResilienceKeyComparer(); + + private sealed class BuilderResilienceKeyComparer : IEqualityComparer + { + public bool Equals(StrategyId? x, StrategyId? y) => x?.Type == y?.Type && x?.BuilderName == y?.BuilderName; + + public int GetHashCode(StrategyId obj) => (obj.Type, obj.BuilderName).GetHashCode(); + } +} diff --git a/src/Polly.Core/Registry/ResilienceStrategyProvider.cs b/src/Polly.Core/Registry/ResilienceStrategyProvider.cs new file mode 100644 index 00000000000..68b9649d431 --- /dev/null +++ b/src/Polly.Core/Registry/ResilienceStrategyProvider.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Polly.Registry; + +#pragma warning disable CA1716 // Identifiers should not match keywords + +/// +/// Represents a provider for resilience strategies that are accessible by . +/// +/// The type of the key. +public abstract class ResilienceStrategyProvider + where TKey : notnull +{ + /// + /// Retrieves a resilience strategy from the provider using the specified key. + /// + /// The key used to identify the resilience strategy. + /// The resilience strategy associated with the specified key. + /// Thrown when no resilience strategy is found for the specified key. + public virtual ResilienceStrategy Get(TKey key) + { + if (TryGet(key, out var strategy)) + { + return strategy; + } + + throw new KeyNotFoundException($"Unable to find a resilience strategy associated with the key '{key}'. " + + $"Please ensure that either the resilience strategy or the builder is registered."); + } + + /// + /// Tries to get a resilience strategy from the provider using the specified key. + /// + /// The key used to identify the resilience strategy. + /// The output resilience strategy if found, null otherwise. + /// true if the strategy was found, false otherwise. + public abstract bool TryGet(TKey key, [NotNullWhen(true)] out ResilienceStrategy? strategy); +} diff --git a/src/Polly.Core/Registry/ResilienceStrategyRegistry.cs b/src/Polly.Core/Registry/ResilienceStrategyRegistry.cs new file mode 100644 index 00000000000..07747c9079e --- /dev/null +++ b/src/Polly.Core/Registry/ResilienceStrategyRegistry.cs @@ -0,0 +1,131 @@ +using System.Diagnostics.CodeAnalysis; +using Polly.Builder; + +namespace Polly.Registry; + +/// +/// Represents a registry of resilience strategies and builders that are accessible by . +/// +/// The type of the key. +/// +/// This class provides a way to organize and manage multiple resilience strategies +/// using keys of type . +/// +/// Additionally, it allows registration of callbacks that configure the strategy using . +/// These callbacks are called when the resilience strategy is not yet cached and it's retrieved for the first time. +/// +/// +public sealed class ResilienceStrategyRegistry : ResilienceStrategyProvider + where TKey : notnull +{ + private readonly Func _activator; + private readonly ConcurrentDictionary> _builders; + private readonly ConcurrentDictionary _strategies; + + /// + /// Initializes a new instance of the class with the default comparer. + /// + public ResilienceStrategyRegistry() + : this(new ResilienceStrategyRegistryOptions()) + { + } + + /// + /// Initializes a new instance of the class with a custom builder factory and comparer. + /// + /// The registry options. + public ResilienceStrategyRegistry(ResilienceStrategyRegistryOptions options) + { + Guard.NotNull(options); + + ValidationHelper.ValidateObject(options, "The resilience strategy registry options are invalid."); + + _activator = options.BuilderFactory; + _builders = new ConcurrentDictionary>(options.BuilderComparer); + _strategies = new ConcurrentDictionary(options.StrategyComparer); + } + + /// + /// Tries to add an existing resilience strategy to the registry. + /// + /// The key used to identify the resilience strategy. + /// The resilience strategy instance. + /// true if the strategy was added successfully, false otherwise. + public bool TryAdd(TKey key, ResilienceStrategy strategy) + { + Guard.NotNull(strategy); + + return _strategies.TryAdd(key, strategy); + } + + /// + /// Removes a resilience strategy from the registry. + /// + /// The key used to identify the resilience strategy. + /// true if the strategy was removed successfully, false otherwise. + public bool Remove(TKey key) => _strategies.TryRemove(key, out _); + + /// + /// Tries to get a resilience strategy from the registry. + /// + /// The key used to identify the resilience strategy. + /// The output resilience strategy if found, null otherwise. + /// true if the strategy was found, false otherwise. + /// + /// Tries to get a strategy using the given key. If not found, it looks for a builder with the key, builds the strategy, + /// adds it to the registry, and returns true. If neither the strategy nor the builder is found, the method returns false. + /// + public override bool TryGet(TKey key, [NotNullWhen(true)] out ResilienceStrategy? strategy) + { + if (_strategies.TryGetValue(key, out strategy)) + { + return true; + } + + if (_builders.TryGetValue(key, out var configure)) + { + strategy = _strategies.GetOrAdd(key, key => + { + var builder = _activator(); + configure(key, builder); + return builder.Build(); + }); + + return true; + } + + strategy = null; + return false; + } + + /// + /// Tries to add a resilience strategy builder to the registry. + /// + /// The key used to identify the strategy builder. + /// The action that configures the resilience strategy builder. + /// True if the builder was added successfully, false otherwise. + /// + /// Use this method when you want to create the strategy on-demand when it's first accessed. + /// + public bool TryAddBuilder(TKey key, Action configure) + { + Guard.NotNull(configure); + + return _builders.TryAdd(key, configure); + } + + /// + /// Removes a resilience strategy builder from the registry. + /// + /// The key used to identify the resilience strategy builder. + /// true if the builder was removed successfully, false otherwise. + public bool RemoveBuilder(TKey key) => _builders.TryRemove(key, out _); + + /// + /// Clears all cached strategies. + /// + /// + /// This method only clears the cached strategies, the registered builders are kept unchanged. + /// + public void Clear() => _strategies.Clear(); +} diff --git a/src/Polly.Core/Registry/ResilienceStrategyRegistryOptions.cs b/src/Polly.Core/Registry/ResilienceStrategyRegistryOptions.cs new file mode 100644 index 00000000000..a6d8103c5c6 --- /dev/null +++ b/src/Polly.Core/Registry/ResilienceStrategyRegistryOptions.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; +using Polly.Builder; + +namespace Polly.Registry; + +/// +/// An options class used by . +/// +/// The type of the key used by the registry. +public class ResilienceStrategyRegistryOptions +{ + /// + /// Gets or sets the factory method that creates instances of . + /// + /// + /// By default, the factory method creates a new instance of using the default constructor. + /// + [Required] + public Func BuilderFactory { get; set; } = static () => new ResilienceStrategyBuilder(); + + /// + /// Gets or sets the comparer that is used by the registry to retrieve the resilience strategies. + /// + /// + /// By default, the comparer uses the default equality comparer for . + /// + [Required] + public IEqualityComparer StrategyComparer { get; set; } = EqualityComparer.Default; + + /// + /// Gets or sets the comparer that is used by the registry to retrieve the resilience strategy builders. + /// + /// + /// By default, the comparer uses the default equality comparer for . + /// + [Required] + public IEqualityComparer BuilderComparer { get; set; } = EqualityComparer.Default; +}