From 572558a23e0170914ca90bac12b4f8ee10102a1f Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Fri, 14 Jul 2023 16:08:08 +0200 Subject: [PATCH] Demonstrate how to create dynamic strategies with complex keys (#1366) --- .../IssuesTests.StrategiesPerEndpoint_1365.cs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 test/Polly.Extensions.Tests/Issues/IssuesTests.StrategiesPerEndpoint_1365.cs diff --git a/test/Polly.Extensions.Tests/Issues/IssuesTests.StrategiesPerEndpoint_1365.cs b/test/Polly.Extensions.Tests/Issues/IssuesTests.StrategiesPerEndpoint_1365.cs new file mode 100644 index 00000000000..13e4445e70d --- /dev/null +++ b/test/Polly.Extensions.Tests/Issues/IssuesTests.StrategiesPerEndpoint_1365.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.RateLimiting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Polly.Extensions.Registry; +using Polly.Registry; +using Polly.Retry; +using Polly.Timeout; + +namespace Polly.Extensions.Tests.Issues; + +public partial class IssuesTests +{ + [Fact] + public void StrategiesPerEndpoint_1365() + { + var events = new List(); + using var listener = TestUtilities.EnablePollyMetering(events); + var services = new ServiceCollection(); + + services.AddResilienceStrategyRegistry(); + services.AddOptions(); + + // add resilience strategy, keyed by EndpointKey that only defines the builder name + services.AddResilienceStrategy(new EndpointKey("endpoint-pipeline", string.Empty, string.Empty), (builder, context) => + { + var serviceProvider = context.ServiceProvider; + var endpointOptions = context.GetOptions().Endpoints[context.StrategyKey.EndpointName]; + var registry = context.ServiceProvider.GetRequiredService>(); + + // we want this pipeline to react to changes to the options + context.EnableReloads(); + + // we want to limit the number of concurrent requests per endpoint and not include the resource. + // using a registry we can create and cache the shared resilience strategy + var rateLimiterStrategy = registry.GetOrAddStrategy($"rate-limiter/{context.StrategyKey.EndpointName}", (builder, context) => + { + // let's also enable reloads for the rate limiter + context.EnableReloads(serviceProvider.GetRequiredService>()); + + builder.AddConcurrencyLimiter(new ConcurrencyLimiterOptions { PermitLimit = endpointOptions.MaxParallelization }); + }); + + builder.AddStrategy(rateLimiterStrategy); + + // apply retries optionally per-endpoint + if (endpointOptions.Retries > 0) + { + builder.AddRetry(new() + { + BackoffType = RetryBackoffType.ExponentialWithJitter, + RetryCount = endpointOptions.Retries, + StrategyName = $"{context.StrategyKey.EndpointName}-Retry", + }); + } + + // apply circuit breaker + builder.AddAdvancedCircuitBreaker(new() + { + BreakDuration = endpointOptions.BreakDuration, + StrategyName = $"{context.StrategyKey.EndpointName}-{context.StrategyKey.Resource}-CircuitBreaker" + }); + + // apply timeout + builder.AddTimeout(new TimeoutStrategyOptions + { + StrategyName = $"{context.StrategyKey.EndpointName}-Timeout", + Timeout = endpointOptions.Timeout.Add(TimeSpan.FromSeconds(1)), + }); + }); + + // configure the registry to allow multi-dimensional keys + services.AddResilienceStrategyRegistry(options => + { + options.BuilderComparer = new EndpointKey.BuilderComparer(); + options.StrategyComparer = new EndpointKey.StrategyComparer(); + + // format the key for telemetry + options.InstanceNameFormatter = key => $"{key.EndpointName}/{key.Resource}"; + + // format the builder name for telemetry + options.BuilderNameFormatter = key => key.BuilderName; + }); + + // create the strategy provider + var provider = services.BuildServiceProvider().GetRequiredService>(); + + // define a key for each resource/endpoint combination + var resource1Key = new EndpointKey("endpoint-pipeline", "Endpoint 1", "Resource 1"); + var resource2Key = new EndpointKey("endpoint-pipeline", "Endpoint 1", "Resource 2"); + + var strategy1 = provider.GetStrategy(resource1Key); + var strategy2 = provider.GetStrategy(resource2Key); + + strategy1.Should().NotBe(strategy2); + provider.GetStrategy(resource1Key).Should().BeSameAs(strategy1); + provider.GetStrategy(resource2Key).Should().BeSameAs(strategy2); + + strategy1.Execute(() => { }); + events.Should().HaveCount(3); + events[0].Tags["builder-name"].Should().Be("endpoint-pipeline"); + events[0].Tags["builder-instance"].Should().Be("Endpoint 1/Resource 1"); + } + + public class EndpointOptions + { + public int Retries { get; set; } = 3; + + public TimeSpan BreakDuration { get; set; } = TimeSpan.FromSeconds(10); + + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(4); + + public int MaxParallelization { get; set; } = 10; + } + + public class EndpointsOptions + { + public Dictionary Endpoints { get; set; } = new() + { + ["Endpoint 1"] = new EndpointOptions { Retries = 2 }, + ["Endpoint 2"] = new EndpointOptions { Retries = 3 }, + }; + } + + public record EndpointKey(string BuilderName, string EndpointName, string Resource) + { + public class BuilderComparer : IEqualityComparer + { + public bool Equals(EndpointKey? x, EndpointKey? y) => StringComparer.Ordinal.Equals(x?.BuilderName, y?.BuilderName); + + public int GetHashCode([DisallowNull] EndpointKey obj) => StringComparer.Ordinal.GetHashCode(obj.BuilderName); + } + + public class StrategyComparer : IEqualityComparer + { + public bool Equals(EndpointKey? x, EndpointKey? y) => (x?.BuilderName, x?.EndpointName, x?.Resource) == (y?.BuilderName, y?.EndpointName, y?.Resource); + + public int GetHashCode([DisallowNull] EndpointKey obj) => (obj.BuilderName, obj.EndpointName, obj.Resource).GetHashCode(); + } + } +}