Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce ResilienceStrategyRegistry #1085

Merged
merged 4 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
martincostello marked this conversation as resolved.
Show resolved Hide resolved

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;
martincostello marked this conversation as resolved.
Show resolved Hide resolved

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;
martincostello marked this conversation as resolved.
Show resolved Hide resolved

public record StrategyId(Type Type, string BuilderName, string InstanceName = "")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@geeknoid We can consider moving this to the core API as we would propably have to copy-paste this piece to our codebase. This struct allows:

  • Isolation of strategies by the type of the result they are handling.
  • Multi-instance support.

cc @martincostello

{
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