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

Make the ReactiveResilienceStrategy type-safe #1462

Merged
merged 3 commits into from
Aug 8, 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
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,17 @@ public static class CircuitBreakerCompositeStrategyBuilderExtensions
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All options members preserved.")]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CircuitBreakerStrategyOptions))]
public static CompositeStrategyBuilder AddCircuitBreaker(this CompositeStrategyBuilder builder, CircuitBreakerStrategyOptions options)
{
Guard.NotNull(builder);
Guard.NotNull(options);

return builder.AddCircuitBreakerCore(options);
return builder.AddStrategy(context => CreateStrategy(context, options), options);
}

/// <summary>
Expand All @@ -47,39 +52,29 @@ public static CompositeStrategyBuilder AddCircuitBreaker(this CompositeStrategyB
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
public static CompositeStrategyBuilder<TResult> AddCircuitBreaker<TResult>(this CompositeStrategyBuilder<TResult> builder, CircuitBreakerStrategyOptions<TResult> options)
{
Guard.NotNull(builder);
Guard.NotNull(options);

return builder.AddCircuitBreakerCore(options);
}

[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All options members preserved.")]
private static TBuilder AddCircuitBreakerCore<TBuilder, TResult>(this TBuilder builder, CircuitBreakerStrategyOptions<TResult> options)
where TBuilder : CompositeStrategyBuilderBase
public static CompositeStrategyBuilder<TResult> AddCircuitBreaker<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>(
this CompositeStrategyBuilder<TResult> builder,
CircuitBreakerStrategyOptions<TResult> options)
{
return builder.AddStrategy(
context =>
{
var behavior = new AdvancedCircuitBehavior(
options.FailureRatio,
options.MinimumThroughput,
HealthMetrics.Create(options.SamplingDuration, context.TimeProvider));
Guard.NotNull(builder);
Guard.NotNull(options);

return CreateStrategy<TResult, CircuitBreakerStrategyOptions<TResult>>(context, options, behavior);
},
options);
return builder.AddStrategy(context => CreateStrategy(context, options), options);
}

internal static CircuitBreakerResilienceStrategy<TResult> CreateStrategy<TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(
internal static CircuitBreakerResilienceStrategy<TResult> CreateStrategy<TResult>(
StrategyBuilderContext context,
CircuitBreakerStrategyOptions<TResult> options,
CircuitBehavior behavior)
CircuitBreakerStrategyOptions<TResult> options)
{
var behavior = new AdvancedCircuitBehavior(
options.FailureRatio,
options.MinimumThroughput,
HealthMetrics.Create(options.SamplingDuration, context.TimeProvider));

var controller = new CircuitStateController<TResult>(
options.BreakDuration,
options.OnOpened,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public CircuitBreakerResilienceStrategy(
_controller.Dispose);
}

protected override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
{
if (await _controller.OnActionPreExecuteAsync(context).ConfigureAwait(context.ContinueOnCapturedContext) is Outcome<T> outcome)
{
Expand Down
47 changes: 47 additions & 0 deletions src/Polly.Core/CompositeStrategyBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,53 @@ public static TBuilder AddStrategy<TBuilder>(this TBuilder builder, Func<Strateg
return builder;
}

/// <summary>
/// Adds a reactive strategy to the builder.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="factory">The factory that creates a resilience strategy.</param>
/// <param name="options">The options associated with the strategy. If none are provided the default instance of <see cref="ResilienceStrategyOptions"/> is created.</param>
/// <returns>The same builder instance.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="factory"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a strategy. The builder cannot be modified after it has been used.</exception>
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> is invalid.</exception>
[RequiresUnreferencedCode(Constants.OptionsValidation)]
public static CompositeStrategyBuilder AddStrategy(
this CompositeStrategyBuilder builder, Func<StrategyBuilderContext, ReactiveResilienceStrategy<object>> factory,
ResilienceStrategyOptions options)
{
Guard.NotNull(builder);
Guard.NotNull(factory);
Guard.NotNull(options);

builder.AddStrategyCore(context => new ReactiveResilienceStrategyBridge<object>(factory(context)), options);
return builder;
}

/// <summary>
/// Adds a reactive strategy to the builder.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <param name="factory">The factory that creates a resilience strategy.</param>
/// <param name="options">The options associated with the strategy. If none are provided the default instance of <see cref="ResilienceStrategyOptions"/> is created.</param>
/// <returns>The same builder instance.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/>, <paramref name="factory"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when this builder was already used to create a strategy. The builder cannot be modified after it has been used.</exception>
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> is invalid.</exception>
[RequiresUnreferencedCode(Constants.OptionsValidation)]
public static CompositeStrategyBuilder<TResult> AddStrategy<TResult>(
this CompositeStrategyBuilder<TResult> builder, Func<StrategyBuilderContext, ReactiveResilienceStrategy<TResult>> factory,
ResilienceStrategyOptions options)
{
Guard.NotNull(builder);
Guard.NotNull(factory);
Guard.NotNull(options);

builder.AddStrategyCore(context => new ReactiveResilienceStrategyBridge<TResult>(factory(context)), options);
return builder;
}

internal sealed class EmptyOptions : ResilienceStrategyOptions
{
public static readonly EmptyOptions Instance = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ public static class FallbackCompositeStrategyBuilderExtensions
/// <returns>The builder instance with the fallback strategy added.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
public static CompositeStrategyBuilder<TResult> AddFallback<TResult>(this CompositeStrategyBuilder<TResult> builder, FallbackStrategyOptions<TResult> options)
[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All options members preserved.")]
public static CompositeStrategyBuilder<TResult> AddFallback<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>(
this CompositeStrategyBuilder<TResult> builder,
FallbackStrategyOptions<TResult> options)
{
Guard.NotNull(builder);
Guard.NotNull(options);

builder.AddFallbackCore<TResult, FallbackStrategyOptions<TResult>>(options);
return builder;
return builder.AddStrategy(context => CreateFallback(context, options), options);
}

/// <summary>
Expand All @@ -35,34 +40,30 @@ public static CompositeStrategyBuilder<TResult> AddFallback<TResult>(this Compos
/// <returns>The builder instance with the fallback strategy added.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All options members preserved.")]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FallbackStrategyOptions))]
internal static CompositeStrategyBuilder AddFallback(this CompositeStrategyBuilder builder, FallbackStrategyOptions options)
{
Guard.NotNull(builder);
Guard.NotNull(options);

builder.AddFallbackCore<object, FallbackStrategyOptions>(options);
return builder;
return builder.AddStrategy(context => CreateFallback(context, options), options);
}

[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All options members preserved.")]
internal static void AddFallbackCore<TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TOptions>(
this CompositeStrategyBuilderBase builder,
private static ReactiveResilienceStrategy<TResult> CreateFallback<TResult>(
StrategyBuilderContext context,
FallbackStrategyOptions<TResult> options)
{
builder.AddStrategy(context =>
{
var handler = new FallbackHandler<TResult>(
options.ShouldHandle!,
options.FallbackAction!);
var handler = new FallbackHandler<TResult>(
options.ShouldHandle!,
options.FallbackAction!);

return new FallbackResilienceStrategy<TResult>(
handler,
options.OnFallback,
context.Telemetry);
},
options);
return new FallbackResilienceStrategy<TResult>(
handler,
options.OnFallback,
context.Telemetry);
}
}
2 changes: 1 addition & 1 deletion src/Polly.Core/Fallback/FallbackResilienceStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public FallbackResilienceStrategy(FallbackHandler<T> handler, Func<OutcomeArgume
_telemetry = telemetry;
}

protected override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
{
var outcome = await ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext);
var handleFallbackArgs = new OutcomeArguments<T, FallbackPredicateArguments>(context, outcome, default);
Expand Down
58 changes: 30 additions & 28 deletions src/Polly.Core/Hedging/HedgingCompositeStrategyBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@ public static class HedgingCompositeStrategyBuilderExtensions
/// <returns>The builder instance with the hedging strategy added.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
public static CompositeStrategyBuilder<TResult> AddHedging<TResult>(this CompositeStrategyBuilder<TResult> builder, HedgingStrategyOptions<TResult> options)
[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All options members preserved.")]
public static CompositeStrategyBuilder<TResult> AddHedging<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TResult>(
this CompositeStrategyBuilder<TResult> builder,
HedgingStrategyOptions<TResult> options)
{
Guard.NotNull(builder);
Guard.NotNull(options);

builder.AddHedgingCore<TResult, HedgingStrategyOptions<TResult>>(options);
return builder;
return builder.AddStrategy(context => CreateHedgingStrategy(context, options, isGeneric: true), options);
}

/// <summary>
Expand All @@ -36,39 +41,36 @@ public static CompositeStrategyBuilder<TResult> AddHedging<TResult>(this Composi
/// <returns>The builder instance with the hedging strategy added.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ValidationException">Thrown when <paramref name="options"/> are invalid.</exception>
[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All options members preserved.")]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(HedgingStrategyOptions))]
internal static CompositeStrategyBuilder AddHedging(this CompositeStrategyBuilder builder, HedgingStrategyOptions options)
{
Guard.NotNull(builder);
Guard.NotNull(options);

builder.AddHedgingCore<object, HedgingStrategyOptions>(options);
return builder;
return builder.AddStrategy(context => CreateHedgingStrategy(context, options, isGeneric: false), options);
}

[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "All options members preserved.")]
internal static void AddHedgingCore<TResult, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(
this CompositeStrategyBuilderBase builder,
HedgingStrategyOptions<TResult> options)
private static HedgingResilienceStrategy<TResult> CreateHedgingStrategy<TResult>(
StrategyBuilderContext context,
HedgingStrategyOptions<TResult> options,
bool isGeneric)
{
builder.AddStrategy(context =>
{
var handler = new HedgingHandler<TResult>(
options.ShouldHandle!,
options.HedgingActionGenerator,
IsGeneric: builder is not CompositeStrategyBuilder);
var handler = new HedgingHandler<TResult>(
options.ShouldHandle!,
options.HedgingActionGenerator,
IsGeneric: isGeneric);

return new HedgingResilienceStrategy<TResult>(
options.HedgingDelay,
options.MaxHedgedAttempts,
handler,
options.OnHedging,
options.HedgingDelayGenerator,
context.TimeProvider,
context.Telemetry);
},
options);
return new HedgingResilienceStrategy<TResult>(
options.HedgingDelay,
options.MaxHedgedAttempts,
handler,
options.OnHedging,
options.HedgingDelayGenerator,
context.TimeProvider,
context.Telemetry);
}
}
2 changes: 1 addition & 1 deletion src/Polly.Core/Hedging/HedgingResilienceStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public HedgingResilienceStrategy(
public Func<OutcomeArguments<T, OnHedgingArguments>, ValueTask>? OnHedging { get; }

[ExcludeFromCodeCoverage] // coverlet issue
protected override async ValueTask<Outcome<T>> ExecuteCore<TState>(
protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback,
ResilienceContext context,
TState state)
Expand Down
Loading