Skip to content

Commit 36f47bc

Browse files
committed
Introduce OutcomeResilienceStrategy
1 parent 47aef87 commit 36f47bc

File tree

47 files changed

+465
-619
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+465
-619
lines changed

bench/BenchmarkDotNet.Artifacts/results/Polly.Core.Benchmarks.MultipleStrategiesBenchmark-report-github.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15
99
LaunchCount=2 WarmupCount=10
1010

1111
```
12-
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
13-
|------------------------------------- |---------:|----------:|----------:|------:|--------:|-------:|----------:|------------:|
14-
| ExecuteStrategyPipeline_V7 | 2.220 μs | 0.0164 μs | 0.0236 μs | 1.00 | 0.00 | 0.1106 | 2824 B | 1.00 |
15-
| ExecuteStrategyPipeline_V8 | 1.901 μs | 0.0089 μs | 0.0127 μs | 0.86 | 0.01 | - | 40 B | 0.01 |
16-
| ExecuteStrategyPipeline_Telemetry_V8 | 2.947 μs | 0.0077 μs | 0.0115 μs | 1.33 | 0.02 | - | 40 B | 0.01 |
12+
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
13+
|-------------------------------------- |---------:|----------:|----------:|------:|--------:|-------:|----------:|------------:|
14+
| ExecuteStrategyPipeline_V7 | 2.488 μs | 0.0316 μs | 0.0463 μs | 1.00 | 0.00 | 0.1106 | 2824 B | 1.00 |
15+
| ExecuteStrategyPipeline_V8 | 1.913 μs | 0.0066 μs | 0.0093 μs | 0.77 | 0.01 | - | 40 B | 0.01 |
16+
| ExecuteStrategyPipeline_Telemetry_V8 | 2.431 μs | 0.0088 μs | 0.0129 μs | 0.98 | 0.02 | - | 40 B | 0.01 |
17+
| ExecuteStrategyPipeline_NonGeneric_V8 | 2.207 μs | 0.0068 μs | 0.0102 μs | 0.89 | 0.01 | - | 40 B | 0.01 |
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
``` ini
22

3-
BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2)
3+
BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1848/22H2/2022Update/SunValley2)
44
Intel Core i9-10885H CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores
5-
.NET SDK=7.0.203
6-
[Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
5+
.NET SDK=7.0.304
6+
[Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2
77

88
Job=MediumRun Toolchain=InProcessEmitToolchain IterationCount=15
99
LaunchCount=2 WarmupCount=10
1010

1111
```
12-
| Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
13-
|---------------- |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:|
14-
| ExecuteRetry_V7 | 264.2 ns | 10.80 ns | 15.83 ns | 255.3 ns | 1.00 | 0.00 | 0.0658 | 552 B | 1.00 |
15-
| ExecuteRetry_V8 | 244.7 ns | 7.75 ns | 10.61 ns | 244.1 ns | 0.93 | 0.05 | - | - | 0.00 |
12+
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
13+
|---------------- |---------:|--------:|--------:|------:|--------:|-------:|----------:|------------:|
14+
| ExecuteRetry_V7 | 210.9 ns | 4.37 ns | 6.27 ns | 1.00 | 0.00 | 0.0658 | 552 B | 1.00 |
15+
| ExecuteRetry_V8 | 224.1 ns | 2.34 ns | 3.43 ns | 1.06 | 0.04 | - | - | 0.00 |

bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs

+15
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class MultipleStrategiesBenchmark
88
private object? _strategyV7;
99
private object? _strategyV8;
1010
private object? _strategyTelemetryV8;
11+
private ResilienceStrategy? _nonGeneric;
1112

1213
[GlobalSetup]
1314
public void Setup()
@@ -16,6 +17,7 @@ public void Setup()
1617
_strategyV7 = Helper.CreateStrategyPipeline(PollyVersion.V7, false);
1718
_strategyV8 = Helper.CreateStrategyPipeline(PollyVersion.V8, false);
1819
_strategyTelemetryV8 = Helper.CreateStrategyPipeline(PollyVersion.V8, true);
20+
_nonGeneric = Helper.CreateNonGenericStrategyPipeline();
1921
}
2022

2123
[GlobalCleanup]
@@ -29,4 +31,17 @@ public void Setup()
2931

3032
[Benchmark]
3133
public ValueTask ExecuteStrategyPipeline_Telemetry_V8() => _strategyTelemetryV8!.ExecuteAsync(PollyVersion.V8);
34+
35+
[Benchmark]
36+
public async ValueTask ExecuteStrategyPipeline_NonGeneric_V8()
37+
{
38+
var context = ResilienceContext.Get();
39+
40+
await _nonGeneric!.ExecuteOutcomeAsync(
41+
static (_, _) => new ValueTask<Outcome<string>>(new Outcome<string>("dummy")),
42+
context,
43+
string.Empty).ConfigureAwait(false);
44+
45+
ResilienceContext.Return(context);
46+
}
3247
}

bench/Polly.Core.Benchmarks/Utils/Helper.MultipleStrategies.cs

+50-5
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@ internal static partial class Helper
2222
PermitLimit = 10
2323
})
2424
.AddTimeout(TimeSpan.FromSeconds(10))
25-
.AddRetry(
26-
predicate => predicate.Handle<InvalidOperationException>().HandleResult(Failure),
27-
RetryBackoffType.Constant,
28-
3,
29-
TimeSpan.FromSeconds(1))
25+
.AddRetry(new()
26+
{
27+
BackoffType = RetryBackoffType.Constant,
28+
RetryCount = 3,
29+
BaseDelay = TimeSpan.FromSeconds(1),
30+
ShouldHandle = args => args switch
31+
{
32+
{ Exception: InvalidOperationException } => PredicateResult.True,
33+
{ Result: var result } when result == Failure => PredicateResult.True,
34+
_ => PredicateResult.False
35+
}
36+
})
3037
.AddTimeout(TimeSpan.FromSeconds(1))
3138
.AddAdvancedCircuitBreaker(new()
3239
{
@@ -49,4 +56,42 @@ internal static partial class Helper
4956
}),
5057
_ => throw new NotSupportedException()
5158
};
59+
60+
public static ResilienceStrategy CreateNonGenericStrategyPipeline()
61+
{
62+
return new ResilienceStrategyBuilder()
63+
.AddConcurrencyLimiter(new ConcurrencyLimiterOptions
64+
{
65+
QueueLimit = 10,
66+
PermitLimit = 10
67+
})
68+
.AddTimeout(TimeSpan.FromSeconds(10))
69+
.AddRetry(new()
70+
{
71+
BackoffType = RetryBackoffType.Constant,
72+
RetryCount = 3,
73+
BaseDelay = TimeSpan.FromSeconds(1),
74+
ShouldHandle = args => args switch
75+
{
76+
{ Exception: InvalidOperationException } => PredicateResult.True,
77+
{ Result: string result } when result == Failure => PredicateResult.True,
78+
_ => PredicateResult.False
79+
}
80+
})
81+
.AddTimeout(TimeSpan.FromSeconds(1))
82+
.AddAdvancedCircuitBreaker(new()
83+
{
84+
FailureThreshold = 0.5,
85+
SamplingDuration = TimeSpan.FromSeconds(30),
86+
MinimumThroughput = 10,
87+
BreakDuration = TimeSpan.FromSeconds(5),
88+
ShouldHandle = args => args switch
89+
{
90+
{ Exception: InvalidOperationException } => PredicateResult.True,
91+
{ Result: string result } when result == Failure => PredicateResult.True,
92+
_ => PredicateResult.False
93+
}
94+
})
95+
.Build();
96+
}
5297
}

src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategy.cs

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
namespace Polly.CircuitBreaker;
22

3-
internal sealed class CircuitBreakerResilienceStrategy : ResilienceStrategy
3+
internal sealed class CircuitBreakerResilienceStrategy<T> : OutcomeResilienceStrategy<T>
44
{
5-
private readonly PredicateInvoker<CircuitBreakerPredicateArguments> _handler;
6-
private readonly CircuitStateController _controller;
5+
private readonly Func<OutcomeArguments<T, CircuitBreakerPredicateArguments>, ValueTask<bool>> _handler;
6+
private readonly CircuitStateController<T> _controller;
77

88
public CircuitBreakerResilienceStrategy(
9-
PredicateInvoker<CircuitBreakerPredicateArguments> handler,
10-
CircuitStateController controller,
9+
Func<OutcomeArguments<T, CircuitBreakerPredicateArguments>, ValueTask<bool>> handler,
10+
CircuitStateController<T> controller,
1111
CircuitBreakerStateProvider? stateProvider,
12-
CircuitBreakerManualControl? manualControl)
12+
CircuitBreakerManualControl? manualControl,
13+
bool isGeneric)
14+
: base(isGeneric)
1315
{
1416
_handler = handler;
1517
_controller = controller;
@@ -21,20 +23,17 @@ public CircuitBreakerResilienceStrategy(
2123
_controller.Dispose);
2224
}
2325

24-
protected internal override async ValueTask<Outcome<TResult>> ExecuteCoreAsync<TResult, TState>(
25-
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
26-
ResilienceContext context,
27-
TState state)
26+
protected override async ValueTask<Outcome<T>> ExecuteCallbackAsync<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback, ResilienceContext context, TState state)
2827
{
29-
if (await _controller.OnActionPreExecuteAsync<TResult>(context).ConfigureAwait(context.ContinueOnCapturedContext) is Outcome<TResult> outcome)
28+
if (await _controller.OnActionPreExecuteAsync(context).ConfigureAwait(context.ContinueOnCapturedContext) is Outcome<T> outcome)
3029
{
3130
return outcome;
3231
}
3332

3433
outcome = await ExecuteCallbackSafeAsync(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext);
3534

36-
var args = new OutcomeArguments<TResult, CircuitBreakerPredicateArguments>(context, outcome, new CircuitBreakerPredicateArguments());
37-
if (await _handler.HandleAsync(args).ConfigureAwait(context.ContinueOnCapturedContext))
35+
var args = new OutcomeArguments<T, CircuitBreakerPredicateArguments>(context, outcome, new CircuitBreakerPredicateArguments());
36+
if (await _handler(args).ConfigureAwait(context.ContinueOnCapturedContext))
3837
{
3938
await _controller.OnActionFailureAsync(outcome, context).ConfigureAwait(context.ContinueOnCapturedContext);
4039
}

src/Polly.Core/CircuitBreaker/CircuitBreakerResilienceStrategyBuilderExtensions.cs

+11-7
Original file line numberDiff line numberDiff line change
@@ -121,22 +121,26 @@ private static TBuilder AddSimpleCircuitBreakerCore<TBuilder, TResult>(this TBui
121121
return builder.AddStrategy(context => CreateStrategy(context, options, new ConsecutiveFailuresCircuitBehavior(options.FailureThreshold)), options);
122122
}
123123

124-
internal static CircuitBreakerResilienceStrategy CreateStrategy<TResult>(ResilienceStrategyBuilderContext context, CircuitBreakerStrategyOptions<TResult> options, CircuitBehavior behavior)
124+
internal static CircuitBreakerResilienceStrategy<TResult> CreateStrategy<TResult>(
125+
ResilienceStrategyBuilderContext context,
126+
CircuitBreakerStrategyOptions<TResult> options,
127+
CircuitBehavior behavior)
125128
{
126-
var controller = new CircuitStateController(
129+
var controller = new CircuitStateController<TResult>(
127130
options.BreakDuration,
128-
context.CreateInvoker(options.OnOpened),
129-
context.CreateInvoker(options.OnClosed),
131+
options.OnOpened,
132+
options.OnClosed,
130133
options.OnHalfOpened,
131134
behavior,
132135
context.TimeProvider,
133136
context.Telemetry);
134137

135-
return new CircuitBreakerResilienceStrategy(
136-
context.CreateInvoker(options.ShouldHandle)!,
138+
return new CircuitBreakerResilienceStrategy<TResult>(
139+
options.ShouldHandle!,
137140
controller,
138141
options.StateProvider,
139-
options.ManualControl);
142+
options.ManualControl,
143+
context.IsGenericBuilder);
140144
}
141145
}
142146

src/Polly.Core/CircuitBreaker/Controller/CircuitStateController.cs

+19-19
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ namespace Polly.CircuitBreaker;
55
/// <summary>
66
/// Thread-safe controller that holds and manages the circuit breaker state transitions.
77
/// </summary>
8-
internal sealed class CircuitStateController : IDisposable
8+
internal sealed class CircuitStateController<T> : IDisposable
99
{
1010
private readonly object _lock = new();
1111
private readonly ScheduledTaskExecutor _executor = new();
12-
private readonly EventInvoker<OnCircuitOpenedArguments>? _onOpened;
13-
private readonly EventInvoker<OnCircuitClosedArguments>? _onClosed;
12+
private readonly Func<OutcomeArguments<T, OnCircuitOpenedArguments>, ValueTask>? _onOpened;
13+
private readonly Func<OutcomeArguments<T, OnCircuitClosedArguments>, ValueTask>? _onClosed;
1414
private readonly Func<OnCircuitHalfOpenedArguments, ValueTask>? _onHalfOpen;
1515
private readonly TimeProvider _timeProvider;
1616
private readonly ResilienceStrategyTelemetry _telemetry;
@@ -24,8 +24,8 @@ internal sealed class CircuitStateController : IDisposable
2424

2525
public CircuitStateController(
2626
TimeSpan breakDuration,
27-
EventInvoker<OnCircuitOpenedArguments>? onOpened,
28-
EventInvoker<OnCircuitClosedArguments>? onClosed,
27+
Func<OutcomeArguments<T, OnCircuitOpenedArguments>, ValueTask>? onOpened,
28+
Func<OutcomeArguments<T, OnCircuitClosedArguments>, ValueTask>? onClosed,
2929
Func<OnCircuitHalfOpenedArguments, ValueTask>? onHalfOpen,
3030
CircuitBehavior behavior,
3131
TimeProvider timeProvider,
@@ -90,7 +90,7 @@ public ValueTask IsolateCircuitAsync(ResilienceContext context)
9090
lock (_lock)
9191
{
9292
SetLastHandledOutcome_NeedsLock(new Outcome<VoidResult>(new IsolatedCircuitException()));
93-
OpenCircuitFor_NeedsLock(new Outcome<VoidResult>(VoidResult.Instance), TimeSpan.MaxValue, manual: true, context, out task);
93+
OpenCircuitFor_NeedsLock(new Outcome<T>(default(T)), TimeSpan.MaxValue, manual: true, context, out task);
9494
_circuitState = CircuitState.Isolated;
9595
}
9696

@@ -107,13 +107,13 @@ public ValueTask CloseCircuitAsync(ResilienceContext context)
107107

108108
lock (_lock)
109109
{
110-
CloseCircuit_NeedsLock(new Outcome<VoidResult>(VoidResult.Instance), manual: true, context, out task);
110+
CloseCircuit_NeedsLock(new Outcome<T>(default(T)), manual: true, context, out task);
111111
}
112112

113113
return ExecuteScheduledTaskAsync(task, context);
114114
}
115115

116-
public async ValueTask<Outcome<TResult>?> OnActionPreExecuteAsync<TResult>(ResilienceContext context)
116+
public async ValueTask<Outcome<T>?> OnActionPreExecuteAsync(ResilienceContext context)
117117
{
118118
EnsureNotDisposed();
119119

@@ -150,13 +150,13 @@ public ValueTask CloseCircuitAsync(ResilienceContext context)
150150

151151
if (exception is not null)
152152
{
153-
return new Outcome<TResult>(exception);
153+
return new Outcome<T>(exception);
154154
}
155155

156156
return null;
157157
}
158158

159-
public ValueTask OnActionSuccessAsync<TResult>(Outcome<TResult> outcome, ResilienceContext context)
159+
public ValueTask OnActionSuccessAsync(Outcome<T> outcome, ResilienceContext context)
160160
{
161161
EnsureNotDisposed();
162162

@@ -182,7 +182,7 @@ public ValueTask OnActionSuccessAsync<TResult>(Outcome<TResult> outcome, Resilie
182182
return ExecuteScheduledTaskAsync(task, context);
183183
}
184184

185-
public ValueTask OnActionFailureAsync<TResult>(Outcome<TResult> outcome, ResilienceContext context)
185+
public ValueTask OnActionFailureAsync(Outcome<T> outcome, ResilienceContext context)
186186
{
187187
EnsureNotDisposed();
188188

@@ -251,11 +251,11 @@ private void EnsureNotDisposed()
251251
{
252252
if (_disposed)
253253
{
254-
throw new ObjectDisposedException(nameof(CircuitStateController));
254+
throw new ObjectDisposedException(nameof(CircuitStateController<T>));
255255
}
256256
}
257257

258-
private void CloseCircuit_NeedsLock<TResult>(Outcome<TResult> outcome, bool manual, ResilienceContext context, out Task? scheduledTask)
258+
private void CloseCircuit_NeedsLock(Outcome<T> outcome, bool manual, ResilienceContext context, out Task? scheduledTask)
259259
{
260260
scheduledTask = null;
261261

@@ -269,12 +269,12 @@ private void CloseCircuit_NeedsLock<TResult>(Outcome<TResult> outcome, bool manu
269269

270270
if (priorState != CircuitState.Closed)
271271
{
272-
var args = new OutcomeArguments<TResult, OnCircuitClosedArguments>(context, outcome, new OnCircuitClosedArguments(manual));
272+
var args = new OutcomeArguments<T, OnCircuitClosedArguments>(context, outcome, new OnCircuitClosedArguments(manual));
273273
_telemetry.Report(CircuitBreakerConstants.OnCircuitClosed, args);
274274

275275
if (_onClosed is not null)
276276
{
277-
_executor.ScheduleTask(() => _onClosed.HandleAsync(args).AsTask(), context, out scheduledTask);
277+
_executor.ScheduleTask(() => _onClosed(args).AsTask(), context, out scheduledTask);
278278
}
279279
}
280280
}
@@ -310,12 +310,12 @@ private void SetLastHandledOutcome_NeedsLock<TResult>(Outcome<TResult> outcome)
310310

311311
private BrokenCircuitException GetBreakingException_NeedsLock() => _breakingException ?? new BrokenCircuitException();
312312

313-
private void OpenCircuit_NeedsLock<TResult>(Outcome<TResult> outcome, bool manual, ResilienceContext context, out Task? scheduledTask)
313+
private void OpenCircuit_NeedsLock(Outcome<T> outcome, bool manual, ResilienceContext context, out Task? scheduledTask)
314314
{
315315
OpenCircuitFor_NeedsLock(outcome, _breakDuration, manual, context, out scheduledTask);
316316
}
317317

318-
private void OpenCircuitFor_NeedsLock<TResult>(Outcome<TResult> outcome, TimeSpan breakDuration, bool manual, ResilienceContext context, out Task? scheduledTask)
318+
private void OpenCircuitFor_NeedsLock(Outcome<T> outcome, TimeSpan breakDuration, bool manual, ResilienceContext context, out Task? scheduledTask)
319319
{
320320
scheduledTask = null;
321321
var utcNow = _timeProvider.UtcNow;
@@ -325,12 +325,12 @@ private void OpenCircuitFor_NeedsLock<TResult>(Outcome<TResult> outcome, TimeSpa
325325
var transitionedState = _circuitState;
326326
_circuitState = CircuitState.Open;
327327

328-
var args = new OutcomeArguments<TResult, OnCircuitOpenedArguments>(context, outcome, new OnCircuitOpenedArguments(breakDuration, manual));
328+
var args = new OutcomeArguments<T, OnCircuitOpenedArguments>(context, outcome, new OnCircuitOpenedArguments(breakDuration, manual));
329329
_telemetry.Report(CircuitBreakerConstants.OnCircuitOpened, args);
330330

331331
if (_onOpened is not null)
332332
{
333-
_executor.ScheduleTask(() => _onOpened.HandleAsync(args).AsTask(), context, out scheduledTask);
333+
_executor.ScheduleTask(() => _onOpened(args).AsTask(), context, out scheduledTask);
334334
}
335335
}
336336
}

src/Polly.Core/CircuitBreaker/Health/HealthMetrics.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace Polly.CircuitBreaker.Health;
22

33
/// <summary>
44
/// The health metrics for advanced circuit breaker.
5-
/// All operations here are executed from <see cref="CircuitStateController"/> under a lock and are thread safe.
5+
/// All operations here are executed from <see cref="CircuitStateController{T}"/> under a lock and are thread safe.
66
/// </summary>
77
internal abstract class HealthMetrics
88
{
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace Polly.CircuitBreaker;
22

33
/// <inheritdoc/>
4-
public class SimpleCircuitBreakerStrategyOptions : SimpleCircuitBreakerStrategyOptions<object>
4+
public class SimpleCircuitBreakerStrategyOptions : SimpleCircuitBreakerStrategyOptions<int>
55
{
66
}

src/Polly.Core/Fallback/FallbackHandler.cs

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
namespace Polly.Fallback;
22

33
internal sealed record class FallbackHandler<T>(
4-
PredicateInvoker<FallbackPredicateArguments> ShouldHandle,
4+
Func<OutcomeArguments<T, FallbackPredicateArguments>, ValueTask<bool>> ShouldHandle,
55
Func<OutcomeArguments<T, FallbackPredicateArguments>, ValueTask<Outcome<T>>> ActionGenerator,
66
bool IsGeneric)
77
{
8-
public bool HandlesFallback<TResult>() => IsGeneric switch
9-
{
10-
true => typeof(TResult) == typeof(T),
11-
false => true
12-
};
13-
148
public async ValueTask<Outcome<TResult>> GetFallbackOutcomeAsync<TResult>(OutcomeArguments<TResult, FallbackPredicateArguments> args)
159
{
1610
var copiedArgs = new OutcomeArguments<T, FallbackPredicateArguments>(

0 commit comments

Comments
 (0)