Skip to content

Commit

Permalink
Introduce ResilienceStrategyRegistry (#1085)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk committed Mar 28, 2023
1 parent 94b309d commit 2f84eab
Show file tree
Hide file tree
Showing 6 changed files with 440 additions and 0 deletions.
36 changes: 36 additions & 0 deletions src/Polly.Core.Tests/Registry/ResilienceStrategyProviderTests.cs
Original file line number Diff line number Diff line change
@@ -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<KeyNotFoundException>()
.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<string>
{
public ResilienceStrategy? Strategy { get; set; }

public override bool TryGet(string key, [NotNullWhen(true)] out ResilienceStrategy? strategy)
{
strategy = Strategy;
return Strategy != null;
}
}
}
172 changes: 172 additions & 0 deletions src/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs
Original file line number Diff line number Diff line change
@@ -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<ResilienceStrategyBuilder> _callback = _ => { };

[Fact]
public void Ctor_Default_Ok()
{
this.Invoking(_ => new ResilienceStrategyRegistry<string>()).Should().NotThrow();
}

[Fact]
public void Ctor_InvalidOptions_Throws()
{
this.Invoking(_ => new ResilienceStrategyRegistry<string>(new ResilienceStrategyRegistryOptions<string> { BuilderFactory = null! }))
.Should()
.Throw<ValidationException>().WithMessage("The resilience strategy registry options are invalid.*");
}

[Fact]
public void Clear_Ok()
{
var registry = new ResilienceStrategyRegistry<string>();

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<string>();

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<string>();
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<ResilienceStrategy>();
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<StrategyId> CreateRegistry()
{
return new ResilienceStrategyRegistry<StrategyId>(new ResilienceStrategyRegistryOptions<StrategyId>
{
BuilderFactory = () =>
{
var builder = new ResilienceStrategyBuilder();
_callback(builder);
return builder;
},
StrategyComparer = StrategyId.Comparer,
BuilderComparer = StrategyId.BuilderComparer
});
}
}
25 changes: 25 additions & 0 deletions src/Polly.Core.Tests/Registry/StrategyId.cs
Original file line number Diff line number Diff line change
@@ -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<StrategyId> ResilienceKey = new("Polly.StrategyId");

public static StrategyId Create<T>(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<StrategyId> Comparer = EqualityComparer<StrategyId>.Default;

public static readonly IEqualityComparer<StrategyId> BuilderComparer = new BuilderResilienceKeyComparer();

private sealed class BuilderResilienceKeyComparer : IEqualityComparer<StrategyId>
{
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();
}
}
38 changes: 38 additions & 0 deletions src/Polly.Core/Registry/ResilienceStrategyProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Diagnostics.CodeAnalysis;

namespace Polly.Registry;

#pragma warning disable CA1716 // Identifiers should not match keywords

/// <summary>
/// Represents a provider for resilience strategies that are accessible by <typeparamref name="TKey"/>.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
public abstract class ResilienceStrategyProvider<TKey>
where TKey : notnull
{
/// <summary>
/// Retrieves a resilience strategy from the provider using the specified key.
/// </summary>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <returns>The resilience strategy associated with the specified key.</returns>
/// <exception cref="KeyNotFoundException">Thrown when no resilience strategy is found for the specified key.</exception>
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.");
}

/// <summary>
/// Tries to get a resilience strategy from the provider using the specified key.
/// </summary>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="strategy">The output resilience strategy if found, null otherwise.</param>
/// <returns>true if the strategy was found, false otherwise.</returns>
public abstract bool TryGet(TKey key, [NotNullWhen(true)] out ResilienceStrategy? strategy);
}
Loading

0 comments on commit 2f84eab

Please sign in to comment.