-
Notifications
You must be signed in to change notification settings - Fork 128
/
Copy pathDemo14_FallbackHedging-RetryWithFallback.cs
128 lines (113 loc) · 5.57 KB
/
Demo14_FallbackHedging-RetryWithFallback.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
117
118
119
120
121
122
123
124
125
126
127
128
using System.Net;
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 retries could help.<br/>
/// Hedging can be used to issue the same request as a retry or craft a hedged request.
/// </para>
/// <para>
/// Observations:
/// <list type="bullet">
/// <item>Same as in the previous demo.</item>
/// <item>But this time hedging will act as a combined retry and fallback strategy.</item>
/// <item>When hedging runs out of attempts, it returns a static fallback response.</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>"Response : Fallback response was provided": The last hedged request succeeded.</item>
/// </list>
/// </para>
/// Take a look at the logs for PollyTestWebApi's requests to see the duplicates.
/// </summary>
public class Demo14_FallbackHedging_RetryWithFallback : DemoBase
{
private const int MaxRetries = 2;
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 will either return a success response or a fallback.";
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),
MaxHedgedAttempts = MaxRetries,
Delay = TimeSpan.FromMilliseconds(-1),
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;
},
// Alter the default behavior to perform retries until it runs out of attempts,
// then it should return a fallback value
ActionGenerator = args =>
{
if(args.AttemptNumber != MaxRetries)
{
// Issue the original request again as the new hedged request
return () => args.Callback(args.ActionContext);
}
// Return a static fallback value
var fallbackResponse = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Fallback response was provided")};
return () => Outcome.FromResultAsValueTask(fallbackResponse);
}
}).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);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
progress.Report(ProgressWithMessage($"Response : {responseBody}", Color.Green));
EventualSuccesses++;
}
catch (Exception e)
{
progress.Report(ProgressWithMessage($"Request {TotalRequests} 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),
};
}