Skip to content

Commit

Permalink
ResilienceStrategyRegistry API improvements (#1388)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Jul 4, 2023
1 parent 20598ad commit 66c5af2
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 39 deletions.
28 changes: 16 additions & 12 deletions src/Polly.Core/Registry/ResilienceStrategyRegistry.TResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,29 @@ public bool TryGet(TKey key, [NotNullWhen(true)] out ResilienceStrategy<TResult>

if (_builders.TryGetValue(key, out var configure))
{
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));

#if NETCOREAPP3_0_OR_GREATER
strategy = _strategies.GetOrAdd(key, static (_, factory) =>
{
return new ResilienceStrategy<TResult>(CreateStrategy(factory.instance._activator, factory.context, factory.configure));
},
(instance: this, context, configure));
#else
strategy = _strategies.GetOrAdd(key, _ => new ResilienceStrategy<TResult>(CreateStrategy(_activator, context, configure)));
#endif

strategy = GetOrAdd(key, configure);
return true;
}

strategy = null;
return false;
}

public ResilienceStrategy<TResult> GetOrAdd(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure)
{
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));

#if NETCOREAPP3_0_OR_GREATER
return _strategies.GetOrAdd(key, static (_, factory) =>
{
return new ResilienceStrategy<TResult>(CreateStrategy(factory.instance._activator, factory.context, factory.configure));
},
(instance: this, context, configure));
#else
return _strategies.GetOrAdd(key, _ => new ResilienceStrategy<TResult>(CreateStrategy(_activator, context, configure)));
#endif
}

public bool TryAddBuilder(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure) => _builders.TryAdd(key, configure);

public bool RemoveBuilder(TKey key) => _builders.TryRemove(key, out _);
Expand Down
81 changes: 70 additions & 11 deletions src/Polly.Core/Registry/ResilienceStrategyRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,24 +118,83 @@ public override bool TryGetStrategy(TKey key, [NotNullWhen(true)] out Resilience

if (_builders.TryGetValue(key, out var configure))
{
var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));

#if NETCOREAPP3_0_OR_GREATER
strategy = _strategies.GetOrAdd(key, static (_, factory) =>
{
return CreateStrategy(factory.instance._activator, factory.context, factory.configure);
},
(instance: this, context, configure));
#else
strategy = _strategies.GetOrAdd(key, _ => CreateStrategy(_activator, context, configure));
#endif
strategy = GetOrAddStrategy(key, configure);
return true;
}

strategy = null;
return false;
}

/// <summary>
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
/// </summary>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="configure">The callback that configures the strategy builder.</param>
/// <returns>An instance of strategy.</returns>
public ResilienceStrategy GetOrAddStrategy(TKey key, Action<ResilienceStrategyBuilder> configure)
{
Guard.NotNull(configure);

return GetOrAddStrategy(key, (builder, _) => configure(builder));
}

/// <summary>
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
/// </summary>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="configure">The callback that configures the strategy builder.</param>
/// <returns>An instance of strategy.</returns>
public ResilienceStrategy GetOrAddStrategy(TKey key, Action<ResilienceStrategyBuilder, ConfigureBuilderContext<TKey>> configure)
{
Guard.NotNull(configure);

if (_strategies.TryGetValue(key, out var strategy))
{
return strategy;
}

var context = new ConfigureBuilderContext<TKey>(key, _builderNameFormatter(key), _strategyKeyFormatter(key));

#if NETCOREAPP3_0_OR_GREATER
return _strategies.GetOrAdd(key, static (_, factory) =>
{
return CreateStrategy(factory.instance._activator, factory.context, factory.configure);
},
(instance: this, context, configure));
#else
return _strategies.GetOrAdd(key, _ => CreateStrategy(_activator, context, configure));
#endif
}

/// <summary>
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
/// </summary>
/// <typeparam name="TResult">The type of result that the resilience strategy handles.</typeparam>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="configure">The callback that configures the strategy builder.</param>
/// <returns>An instance of strategy.</returns>
public ResilienceStrategy<TResult> GetOrAddStrategy<TResult>(TKey key, Action<ResilienceStrategyBuilder<TResult>> configure)
{
Guard.NotNull(configure);

return GetOrAddStrategy<TResult>(key, (builder, _) => configure(builder));
}

/// <summary>
/// Gets existing strategy or creates a new one using the <paramref name="configure"/> callback.
/// </summary>
/// <typeparam name="TResult">The type of result that the resilience strategy handles.</typeparam>
/// <param name="key">The key used to identify the resilience strategy.</param>
/// <param name="configure">The callback that configures the strategy builder.</param>
/// <returns>An instance of strategy.</returns>
public ResilienceStrategy<TResult> GetOrAddStrategy<TResult>(TKey key, Action<ResilienceStrategyBuilder<TResult>, ConfigureBuilderContext<TKey>> configure)
{
Guard.NotNull(configure);

return GetGenericRegistry<TResult>().GetOrAdd(key, configure);
}

/// <summary>
/// Tries to add a resilience strategy builder to the registry.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Polly.Extensions.Utils;
using Polly.Extensions.Registry;
using Polly.Registry;

namespace Polly.Extensions.DependencyInjection;
Expand Down Expand Up @@ -53,12 +53,7 @@ internal AddResilienceStrategyContext(ConfigureBuilderContext<TKey> registryCont
/// You can listen for changes only for single options. If you call this method multiple times, the preceding calls are ignored and only the last one wins.
/// </para>
/// </remarks>
public void EnableReloads<TOptions>(string? name = null)
{
var monitor = ServiceProvider.GetRequiredService<IOptionsMonitor<TOptions>>();

RegistryContext.EnableReloads(() => new OptionsReloadHelper<TOptions>(monitor, name ?? Options.DefaultName).GetCancellationToken);
}
public void EnableReloads<TOptions>(string? name = null) => RegistryContext.EnableReloads(ServiceProvider.GetRequiredService<IOptionsMonitor<TOptions>>(), name);

/// <summary>
/// Gets the options identified by <paramref name="name"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ public static class PollyServiceCollectionExtensions
/// <param name="configure">An action that configures the resilience strategy.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the registered resilience strategy.</returns>
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
/// <remarks>
/// You can retrieve the registered strategy by resolving the <see cref="ResilienceStrategyProvider{TKey}"/> class from the dependency injection container.
/// <para>
/// This call enables the telemetry for the registered resilience strategy.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
this IServiceCollection services,
TKey key,
Expand All @@ -54,13 +54,13 @@ public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
/// <param name="configure">An action that configures the resilience strategy.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the registered resilience strategy.</returns>
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
/// <remarks>
/// You can retrieve the registered strategy by resolving the <see cref="ResilienceStrategyProvider{TKey}"/> class from the dependency injection container.
/// <para>
/// This call enables the telemetry for the registered resilience strategy.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
this IServiceCollection services,
TKey key,
Expand All @@ -85,7 +85,7 @@ public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
});
});

return AddResilienceStrategyRegistry<TKey>(services);
return AddResilienceStrategy<TKey>(services);
}

/// <summary>
Expand All @@ -97,13 +97,13 @@ public static IServiceCollection AddResilienceStrategy<TKey, TResult>(
/// <param name="configure">An action that configures the resilience strategy.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the registered resilience strategy.</returns>
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
/// <remarks>
/// You can retrieve the registered strategy by resolving the <see cref="ResilienceStrategyProvider{TKey}"/> class from the dependency injection container.
/// <para>
/// This call enables the telemetry for the registered resilience strategy.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddResilienceStrategy<TKey>(
this IServiceCollection services,
TKey key,
Expand All @@ -125,13 +125,13 @@ public static IServiceCollection AddResilienceStrategy<TKey>(
/// <param name="configure">An action that configures the resilience strategy.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with the registered resilience strategy.</returns>
/// <exception cref="InvalidOperationException">Thrown if the resilience strategy builder with the provided key has already been added to the registry.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
/// <remarks>
/// You can retrieve the registered strategy by resolving the <see cref="ResilienceStrategyProvider{TKey}"/> class from the dependency injection container.
/// <para>
/// This call enables the telemetry for the registered resilience strategy.
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.</exception>
public static IServiceCollection AddResilienceStrategy<TKey>(
this IServiceCollection services,
TKey key,
Expand All @@ -156,12 +156,28 @@ public static IServiceCollection AddResilienceStrategy<TKey>(
});
});

return AddResilienceStrategyRegistry<TKey>(services);
return AddResilienceStrategy<TKey>(services);
}

private static IServiceCollection AddResilienceStrategyRegistry<TKey>(this IServiceCollection services)
/// <summary>
/// Adds the infrastructure that allows configuring and retrieving resilience strategies using the <typeparamref name="TKey"/> key.
/// </summary>
/// <typeparam name="TKey">The type of the key used to identify the resilience strategy.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the resilience strategy to.</param>
/// <returns>The updated <see cref="IServiceCollection"/> with additional services added.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> is <see langword="null"/>.</exception>
/// <remarks>
/// You can retrieve the strategy registry by resolving the <see cref="ResilienceStrategyProvider{TKey}"/>
/// or <see cref="ResilienceStrategyRegistry{TKey}"/> class from the dependency injection container.
/// <para>
/// This call enables telemetry for all resilience strategies created using <see cref="ResilienceStrategyRegistry{TKey}"/>.
/// </para>
/// </remarks>
public static IServiceCollection AddResilienceStrategy<TKey>(this IServiceCollection services)
where TKey : notnull
{
Guard.NotNull(services);

// check marker to ensure the APIs bellow are called only once for each TKey type
// this prevents polluting the service collection with unnecessary Configure calls
if (services.Contains(RegistryMarker<TKey>.ServiceDescriptor))
Expand All @@ -172,7 +188,7 @@ private static IServiceCollection AddResilienceStrategyRegistry<TKey>(this IServ
services.AddOptions();
services.Add(RegistryMarker<TKey>.ServiceDescriptor);
services.AddResilienceStrategyBuilder();
services.AddResilienceStrategyRegistry<TKey>();
services.AddResilienceStrategy<TKey>();

services.TryAddSingleton(serviceProvider =>
{
Expand Down
2 changes: 1 addition & 1 deletion src/Polly.Extensions/Polly.Extensions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<Reference Include="System.Net.Http" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'"/>
<Reference Include="System.Net.Http" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />
</ItemGroup>
</Project>
36 changes: 36 additions & 0 deletions src/Polly.Extensions/Registry/ConfigureBuilderContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.Extensions.Options;
using Polly.Extensions.Utils;
using Polly.Registry;
using Polly.Utils;

namespace Polly.Extensions.Registry;

/// <summary>
/// Extensions for <see cref="ConfigureBuilderContext{TKey}"/>.
/// </summary>
public static class ConfigureBuilderContextExtensions
{
/// <summary>
/// Enables dynamic reloading of the resilience strategy whenever the <typeparamref name="TOptions"/> options are changed.
/// </summary>
/// <typeparam name="TKey">The type of the key used to identify the resilience strategy.</typeparam>
/// <typeparam name="TOptions">The options type to listen to.</typeparam>
/// <param name="context">The builder context.</param>
/// <param name="optionsMonitor">The options monitor.</param>
/// <param name="name">The named options, if any.</param>
/// <remarks>
/// You can decide based on the <paramref name="name"/> to listen for changes in global options or named options.
/// If <paramref name="name"/> is <see langword="null"/> then the global options are listened to.
/// <para>
/// You can listen for changes only for single options. If you call this method multiple times, the preceding calls are ignored and only the last one wins.
/// </para>
/// </remarks>
public static void EnableReloads<TKey, TOptions>(this ConfigureBuilderContext<TKey> context, IOptionsMonitor<TOptions> optionsMonitor, string? name = null)
where TKey : notnull
{
Guard.NotNull(context);
Guard.NotNull(optionsMonitor);

context.EnableReloads(() => new OptionsReloadHelper<TOptions>(optionsMonitor, name ?? Options.DefaultName).GetCancellationToken);
}
}
30 changes: 30 additions & 0 deletions test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Polly.Registry;
using Polly.Retry;
using Polly.Telemetry;
using Polly.Timeout;

namespace Polly.Core.Tests.Registry;

Expand Down Expand Up @@ -429,5 +430,34 @@ public void EnableReloads_Generic_Ok()
tries.Should().Be(retryCount + 1);
}

[Fact]
public void GetOrAddStrategy_Ok()
{
var id = new StrategyId(typeof(string), "A");
var called = 0;

var registry = CreateRegistry();
var strategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });
var otherStrategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });

strategy.Should().BeOfType<TimeoutResilienceStrategy>();
strategy.Should().BeSameAs(otherStrategy);
called.Should().Be(1);
}

[Fact]
public void GetOrAddStrategy_Generic_Ok()
{
var id = new StrategyId(typeof(string), "A");
var called = 0;

var registry = CreateRegistry();
var strategy = registry.GetOrAddStrategy<string>(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });
var otherStrategy = registry.GetOrAddStrategy<string>(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; });

strategy.Strategy.Should().BeOfType<TimeoutResilienceStrategy>();
strategy.Should().BeSameAs(otherStrategy);
}

private ResilienceStrategyRegistry<StrategyId> CreateRegistry() => new(_options);
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ public void AddResilienceStrategy_Multiple_Ok()
.HaveCount(30);
}

[Fact]
public void AddResilienceStrategyInfra_Ok()
{
var provider = new ServiceCollection().AddResilienceStrategy<string>().BuildServiceProvider();

provider.GetRequiredService<ResilienceStrategyRegistry<string>>().Should().NotBeNull();
provider.GetRequiredService<ResilienceStrategyProvider<string>>().Should().NotBeNull();
provider.GetRequiredService<ResilienceStrategyBuilder>().DiagnosticSource.Should().NotBeNull();
}

private void AddResilienceStrategy(string key, Action<ResilienceStrategyBuilderContext>? onBuilding = null)
{
_services.AddResilienceStrategy(key, builder =>
Expand Down

0 comments on commit 66c5af2

Please sign in to comment.