-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPollyRetryHandler.cs
More file actions
136 lines (120 loc) · 5.5 KB
/
PollyRetryHandler.cs
File metadata and controls
136 lines (120 loc) · 5.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Polly;
using Polly.Registry;
using Polly.Retry;
namespace ZibStack.NET.Aop;
/// <summary>
/// Built-in handler for <see cref="PollyRetryAttribute"/>. Uses Polly's <see cref="ResiliencePipeline"/>
/// for retry logic with exponential/linear/constant backoff and optional named pipeline references.
///
/// <para>
/// Implements both <see cref="IAroundAspectHandler"/> and <see cref="IAsyncAroundAspectHandler"/>
/// so it works on sync and async methods.
/// </para>
///
/// <para>
/// When <see cref="PollyRetryAttribute.PipelineName"/> is set, the handler resolves the pipeline
/// from <see cref="ResiliencePipelineProvider{TKey}"/> in DI (requires <c>Microsoft.Extensions.Resilience</c>).
/// Otherwise, it builds an inline pipeline from the attribute properties and caches it.
/// </para>
/// </summary>
public sealed class PollyRetryHandler : IAroundAspectHandler, IAsyncAroundAspectHandler
{
private readonly ResiliencePipelineProvider<string>? _pipelineProvider;
private static readonly ConcurrentDictionary<string, ResiliencePipeline> PipelineCache = new();
/// <summary>
/// Creates a handler with a pipeline provider for named pipeline resolution.
/// </summary>
public PollyRetryHandler(ResiliencePipelineProvider<string> pipelineProvider) =>
_pipelineProvider = pipelineProvider;
/// <summary>
/// Creates a handler without a pipeline provider (inline pipelines only).
/// </summary>
public PollyRetryHandler() => _pipelineProvider = null;
/// <inheritdoc />
public object? Around(AspectContext context, Func<object?> proceed)
{
var pipeline = GetPipeline(context);
return pipeline.Execute(_ => proceed(), CancellationToken.None);
}
/// <inheritdoc />
public async ValueTask<object?> AroundAsync(AspectContext context, Func<ValueTask<object?>> proceed)
{
var pipeline = GetPipeline(context);
return await pipeline.ExecuteAsync(async ct => await proceed().ConfigureAwait(false),
CancellationToken.None).ConfigureAwait(false);
}
private ResiliencePipeline GetPipeline(AspectContext context)
{
// Named pipeline — resolve from DI
if (context.Properties.TryGetValue("PipelineName", out var pn) && pn is string name && name.Length > 0)
{
if (_pipelineProvider is null)
throw new InvalidOperationException(
$"PollyRetryAttribute.PipelineName '{name}' requires ResiliencePipelineProvider<string> in DI. " +
$"Add 'builder.Services.AddResiliencePipeline(\"{name}\", ...)' at startup.");
return _pipelineProvider.GetPipeline(name);
}
// Inline pipeline — build from attribute properties and cache
var maxRetry = 3;
var delayMs = 200;
var backoffType = RetryBackoffType.Exponential;
Type[]? handleTypes = null;
Type[]? ignoreTypes = null;
if (context.Properties.TryGetValue("MaxRetryAttempts", out var mr) && mr is int m) maxRetry = m;
if (context.Properties.TryGetValue("DelayMs", out var dm) && dm is int d) delayMs = d;
if (context.Properties.TryGetValue("BackoffType", out var bt) && bt is int b) backoffType = (RetryBackoffType)b;
if (context.Properties.TryGetValue("Handle", out var hf) && hf is Type[] h) handleTypes = h;
if (context.Properties.TryGetValue("Ignore", out var ig) && ig is Type[] i) ignoreTypes = i;
var handleKey = handleTypes is not null ? string.Join(",", handleTypes.Select(t => t.FullName)) : "";
var ignoreKey = ignoreTypes is not null ? string.Join(",", ignoreTypes.Select(t => t.FullName)) : "";
var cacheKey = $"{maxRetry}:{delayMs}:{(int)backoffType}:{handleKey}:{ignoreKey}";
return PipelineCache.GetOrAdd(cacheKey, _ =>
{
var pollyBackoff = backoffType switch
{
RetryBackoffType.Constant => DelayBackoffType.Constant,
RetryBackoffType.Linear => DelayBackoffType.Linear,
RetryBackoffType.Exponential => DelayBackoffType.Exponential,
_ => DelayBackoffType.Exponential,
};
var options = new RetryStrategyOptions
{
MaxRetryAttempts = maxRetry,
Delay = TimeSpan.FromMilliseconds(delayMs),
BackoffType = pollyBackoff,
UseJitter = backoffType == RetryBackoffType.Exponential,
};
// Exception filtering
if (handleTypes is not null)
{
var captured = handleTypes;
options.ShouldHandle = args => new ValueTask<bool>(
args.Outcome.Exception is not null && MatchesAny(args.Outcome.Exception, captured));
}
else if (ignoreTypes is not null)
{
var captured = ignoreTypes;
options.ShouldHandle = args => new ValueTask<bool>(
args.Outcome.Exception is not null && !MatchesAny(args.Outcome.Exception, captured));
}
return new ResiliencePipelineBuilder()
.AddRetry(options)
.Build();
});
}
private static bool MatchesAny(Exception ex, Type[] types)
{
var exType = ex.GetType();
foreach (var t in types)
{
if (t.IsAssignableFrom(exType))
return true;
}
return false;
}
}