-
Notifications
You must be signed in to change notification settings - Fork 128
/
Copy pathDemo13_FallbackHedging-RetryOnly.cs
116 lines (102 loc) · 5.38 KB
/
Demo13_FallbackHedging-RetryOnly.cs
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
using PollyDemos.Helpers;
using PollyDemos.OutputHelpers;
namespace PollyDemos;
/// <summary>
/// <para>
/// Imagine a microservice with an endpoint of varying response status codes for the same requests.<br/>
/// Most of the time it responds with success, but other times it sends a failure response.
/// </para>
/// <para>
/// If we can assume that this is just a transient failure then retry could help.<br/>
/// Hedging can be used in a fallback mode to issue hedged requests in case of failure.
/// </para>
/// <para>
/// Observations:
/// <list type="bullet">
/// <item>When the original response indicates failure then a new hedged request is issued.</item>
/// <item>In this demo the hedging will act as a simple retry strategy.</item>
/// <item>In the next demo hedging will act as a combined retry and fallback strategy.</item>
/// </list>
/// </para>
/// <para>
/// How to read the demo logs:
/// <list type="bullet">
/// <item>"Success ... to request #N-0": The original request succeeded.</item>
/// <item>"Success ... to request #N-1": The first hedged request succeeded.</item>
/// <item>"Success ... to request #N-2": The last hedged request succeeded.</item>
/// <item>"Request N eventually failed with: ...": All requests failed.</item>
/// </list>
/// </para>
/// Take a look at the logs for PollyTestWebApi's requests to see the duplicates.
/// </summary>
public class Demo13_FallbackHedging_RetryOnly : DemoBase
{
private readonly ResiliencePropertyKey<int> requestIdKey = new("RequestId");
private readonly ResiliencePropertyKey<int> attemptNumberKey = new("AttemptNumber");
public override string Description =>
"Demonstrates a mitigation action for failed responses. If the response indicates failure then it will issue a new request. The hedging strategy waits for the first successful response or it runs out of retry attempts.";
public override async Task ExecuteAsync(CancellationToken cancellationToken, IProgress<DemoProgress> progress)
{
EventualSuccesses = 0;
Retries = 0;
EventualFailures = 0;
TotalRequests = 0;
PrintHeader(progress);
var strategy = new ResiliencePipelineBuilder<HttpResponseMessage>().AddHedging(new()
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>().HandleResult(res => !res.IsSuccessStatusCode), // Handle unsuccessful responses
MaxHedgedAttempts = 2, // Issue at most two extra hedged requests
Delay = TimeSpan.FromMilliseconds(-1), // Issue a hedged request if response does not indicate success (fallback mode)
OnHedging = args =>
{
var requestId = $"{args.ActionContext.Properties.GetValue(requestIdKey, 0)}-{args.AttemptNumber}";
var hedgedRequestNumber = args.AttemptNumber + 1;
args.ActionContext.Properties.Set(attemptNumberKey, hedgedRequestNumber);
progress.Report(ProgressWithMessage($"Strategy logging: Failed response for request #{requestId} detected. Preparing to execute hedged action {hedgedRequestNumber}.", Color.Yellow));
Retries++;
return default;
}
}).Build();
var client = new HttpClient();
var internalCancel = false;
while (!(internalCancel || cancellationToken.IsCancellationRequested))
{
TotalRequests++;
ResilienceContext context = ResilienceContextPool.Shared.Get();
try
{
context.Properties.Set(requestIdKey, TotalRequests);
var response = await strategy.ExecuteAsync(async ctx =>
{
var requestId = $"{TotalRequests}-{ctx.Properties.GetValue(attemptNumberKey, 0)}";
return await client.GetAsync($"{Configuration.WEB_API_ROOT}/api/VaryingResponseStatus/{requestId}", cancellationToken);
},context);
// If all requests failed then hedging will return the first request's response.
// To handle that failure correctly, we call EnsureSuccessStatusCode to throw an HttpRequestException
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
progress.Report(ProgressWithMessage($"Response : {responseBody}", Color.Green));
EventualSuccesses++;
}
catch (Exception e)
{
var requestId = $"{TotalRequests}-{context.Properties.GetValue(attemptNumberKey, 0)}";
progress.Report(ProgressWithMessage($"Request {requestId} eventually failed with: {e.Message}", Color.Red));
EventualFailures++;
}
finally
{
ResilienceContextPool.Shared.Return(context);
}
await Task.Delay(TimeSpan.FromSeconds(0.5), cancellationToken);
internalCancel = ShouldTerminateByKeyPress();
}
}
public override Statistic[] LatestStatistics => new Statistic[]
{
new("Total requests made", TotalRequests),
new("Requests which eventually succeeded", EventualSuccesses, Color.Green),
new("Hedged action made to help achieve success", Retries, Color.Yellow),
new("Requests which eventually failed", EventualFailures, Color.Red),
};
}