-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHttpRetryHandler.cs
More file actions
83 lines (73 loc) · 3.21 KB
/
HttpRetryHandler.cs
File metadata and controls
83 lines (73 loc) · 3.21 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
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Polly;
using Polly.Retry;
namespace ZibStack.NET.Aop;
/// <summary>
/// Built-in handler for <see cref="HttpRetryAttribute"/>. Uses Polly to retry on transient
/// HTTP errors — same logic as <c>Microsoft.Extensions.Http.Resilience</c>.
///
/// <para>
/// Handles:
/// <list type="bullet">
/// <item><see cref="HttpRequestException"/> with transient status codes (408, 429, 5xx)</item>
/// <item><see cref="HttpRequestException"/> without a status code (network-level failure)</item>
/// <item><see cref="TaskCanceledException"/> (HTTP client timeout)</item>
/// </list>
/// </para>
/// </summary>
public sealed class PollyHttpRetryHandler : IAroundAspectHandler, IAsyncAroundAspectHandler
{
private static readonly ConcurrentDictionary<string, ResiliencePipeline> PipelineCache = new();
/// <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 static ResiliencePipeline GetPipeline(AspectContext context)
{
var maxRetry = 3;
var delayMs = 200;
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;
var cacheKey = $"http:{maxRetry}:{delayMs}";
return PipelineCache.GetOrAdd(cacheKey, _ =>
new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = maxRetry,
Delay = TimeSpan.FromMilliseconds(delayMs),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = args => new ValueTask<bool>(IsTransientHttpError(args.Outcome.Exception)),
})
.Build());
}
private static bool IsTransientHttpError(Exception? exception) => exception switch
{
HttpRequestException { StatusCode: { } status } => IsTransientStatusCode(status),
HttpRequestException => true, // no status code = network failure
TaskCanceledException => true, // HTTP client timeout
_ => false,
};
private static bool IsTransientStatusCode(HttpStatusCode status) => status is
HttpStatusCode.RequestTimeout or // 408
HttpStatusCode.TooManyRequests or // 429
HttpStatusCode.InternalServerError or // 500
HttpStatusCode.BadGateway or // 502
HttpStatusCode.ServiceUnavailable or // 503
HttpStatusCode.GatewayTimeout; // 504
}