Skip to content

Commit bc67c08

Browse files
authored
Merge pull request #67 from route4me/yurii-bart-feat-add-retryable-policy-01
Yurii bart feat add retryable policy 01
2 parents 87777c1 + 7cebf05 commit bc67c08

File tree

8 files changed

+864
-79
lines changed

8 files changed

+864
-79
lines changed

route4me-csharp-sdk/Route4MeSDKLibrary/Managers/Route4MeManagerBase.cs

Lines changed: 158 additions & 74 deletions
Large diffs are not rendered by default.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
6+
using Polly;
7+
using Polly.Extensions.Http;
8+
9+
namespace Route4MeSDKLibrary.Resilience
10+
{
11+
internal static class HttpResiliencePolicy
12+
{
13+
private static readonly Lazy<IAsyncPolicy<HttpResponseMessage>> _asyncPolicy
14+
= new Lazy<IAsyncPolicy<HttpResponseMessage>>(CreateAsyncPolicy);
15+
16+
private static readonly Lazy<ISyncPolicy<HttpResponseMessage>> _syncPolicy
17+
= new Lazy<ISyncPolicy<HttpResponseMessage>>(CreateSyncPolicy);
18+
19+
private static readonly Random _jitterer = new Random();
20+
21+
public static IAsyncPolicy<HttpResponseMessage> GetAsyncPolicy()
22+
{
23+
return Route4MeConfig.RetryCount > 0
24+
? _asyncPolicy.Value
25+
: Policy.NoOpAsync<HttpResponseMessage>();
26+
}
27+
28+
public static ISyncPolicy<HttpResponseMessage> GetSyncPolicy()
29+
{
30+
return Route4MeConfig.RetryCount > 0
31+
? _syncPolicy.Value
32+
: Policy.NoOp<HttpResponseMessage>();
33+
}
34+
35+
private static IAsyncPolicy<HttpResponseMessage> CreateAsyncPolicy()
36+
{
37+
var retryPolicy = CreateAsyncRetryPolicy();
38+
39+
if (Route4MeConfig.EnableCircuitBreaker)
40+
{
41+
var circuitBreakerPolicy = CreateAsyncCircuitBreakerPolicy();
42+
return Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);
43+
}
44+
45+
return retryPolicy;
46+
}
47+
48+
private static ISyncPolicy<HttpResponseMessage> CreateSyncPolicy()
49+
{
50+
var retryPolicy = CreateSyncRetryPolicy();
51+
52+
if (Route4MeConfig.EnableCircuitBreaker)
53+
{
54+
var circuitBreakerPolicy = CreateSyncCircuitBreakerPolicy();
55+
return Policy.Wrap(retryPolicy, circuitBreakerPolicy);
56+
}
57+
58+
return retryPolicy;
59+
}
60+
61+
private static IAsyncPolicy<HttpResponseMessage> CreateAsyncRetryPolicy()
62+
{
63+
return HttpPolicyExtensions
64+
.HandleTransientHttpError()
65+
.Or<TaskCanceledException>()
66+
.OrResult(r => r.StatusCode == (HttpStatusCode)429)
67+
.WaitAndRetryAsync(
68+
retryCount: Route4MeConfig.RetryCount,
69+
sleepDurationProvider: (retryAttempt) =>
70+
{
71+
var exponentialDelay = Route4MeConfig.RetryInitialDelay
72+
.TotalMilliseconds * Math.Pow(2, retryAttempt - 1);
73+
74+
var jitter = (_jitterer.NextDouble() * 0.4) - 0.2;
75+
var delayWithJitter = exponentialDelay * (1 + jitter);
76+
77+
return TimeSpan.FromMilliseconds(delayWithJitter);
78+
},
79+
onRetry: (outcome, timespan, retryCount, context) =>
80+
{
81+
Route4MeConfig.OnRetry?.Invoke(
82+
outcome.Exception,
83+
timespan,
84+
retryCount,
85+
context
86+
);
87+
}
88+
);
89+
}
90+
91+
private static ISyncPolicy<HttpResponseMessage> CreateSyncRetryPolicy()
92+
{
93+
return HttpPolicyExtensions
94+
.HandleTransientHttpError()
95+
.Or<TaskCanceledException>()
96+
.OrResult(r => r.StatusCode == (HttpStatusCode)429)
97+
.WaitAndRetry(
98+
retryCount: Route4MeConfig.RetryCount,
99+
sleepDurationProvider: (retryAttempt) =>
100+
{
101+
var exponentialDelay = Route4MeConfig.RetryInitialDelay
102+
.TotalMilliseconds * Math.Pow(2, retryAttempt - 1);
103+
var jitter = (_jitterer.NextDouble() * 0.4) - 0.2;
104+
var delayWithJitter = exponentialDelay * (1 + jitter);
105+
return TimeSpan.FromMilliseconds(delayWithJitter);
106+
},
107+
onRetry: (outcome, timespan, retryCount, context) =>
108+
{
109+
Route4MeConfig.OnRetry?.Invoke(
110+
outcome.Exception,
111+
timespan,
112+
retryCount,
113+
context
114+
);
115+
}
116+
);
117+
}
118+
119+
private static IAsyncPolicy<HttpResponseMessage> CreateAsyncCircuitBreakerPolicy()
120+
{
121+
return HttpPolicyExtensions
122+
.HandleTransientHttpError()
123+
.Or<TaskCanceledException>()
124+
.OrResult(r => r.StatusCode == (HttpStatusCode)429)
125+
.CircuitBreakerAsync(
126+
handledEventsAllowedBeforeBreaking: Route4MeConfig.CircuitBreakerFailureThreshold,
127+
durationOfBreak: Route4MeConfig.CircuitBreakerDuration,
128+
onBreak: (outcome, duration) =>
129+
{
130+
Route4MeConfig.OnCircuitBreakerOpen?.Invoke(
131+
outcome.Exception,
132+
duration
133+
);
134+
},
135+
onReset: () => { },
136+
onHalfOpen: () => { }
137+
);
138+
}
139+
140+
private static ISyncPolicy<HttpResponseMessage> CreateSyncCircuitBreakerPolicy()
141+
{
142+
return HttpPolicyExtensions
143+
.HandleTransientHttpError()
144+
.Or<TaskCanceledException>()
145+
.OrResult(r => r.StatusCode == (HttpStatusCode)429)
146+
.CircuitBreaker(
147+
handledEventsAllowedBeforeBreaking: Route4MeConfig.CircuitBreakerFailureThreshold,
148+
durationOfBreak: Route4MeConfig.CircuitBreakerDuration,
149+
onBreak: (outcome, duration) =>
150+
{
151+
Route4MeConfig.OnCircuitBreakerOpen?.Invoke(
152+
outcome.Exception,
153+
duration
154+
);
155+
},
156+
onReset: () => { },
157+
onHalfOpen: () => { }
158+
);
159+
}
160+
161+
internal static bool IsTransientError(HttpStatusCode statusCode)
162+
{
163+
return statusCode == HttpStatusCode.RequestTimeout
164+
|| statusCode == (HttpStatusCode)429
165+
|| statusCode == HttpStatusCode.ServiceUnavailable
166+
|| statusCode == HttpStatusCode.GatewayTimeout
167+
|| ((int)statusCode >= 500 && (int)statusCode < 600);
168+
}
169+
}
170+
}

route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeConfig.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22

3+
using Polly;
4+
35
namespace Route4MeSDKLibrary
46
{
57
/// <summary>
@@ -23,5 +25,67 @@ public static TimeSpan HttpTimeout
2325
get => HttpClientHolderManager.RequestsTimeout;
2426
set => HttpClientHolderManager.RequestsTimeout = value;
2527
}
28+
29+
/// <summary>
30+
/// Gets or sets the maximum number of retry attempts for transient HTTP failures.
31+
/// Default is 0 (retry disabled for backward compatibility).
32+
/// Set to a value between 1-10 to enable retry logic.
33+
/// </summary>
34+
/// <example>
35+
/// // Enable 3 retry attempts with exponential backoff
36+
/// Route4MeConfig.RetryCount = 3;
37+
/// </example>
38+
public static int RetryCount { get; set; } = 0;
39+
40+
/// <summary>
41+
/// Gets or sets the initial delay for exponential backoff retry strategy.
42+
/// Default is 200ms. Each subsequent retry doubles this value with jitter.
43+
/// </summary>
44+
/// <example>
45+
/// // Set initial delay to 500ms
46+
/// Route4MeConfig.RetryInitialDelay = TimeSpan.FromMilliseconds(500);
47+
/// </example>
48+
public static TimeSpan RetryInitialDelay { get; set; } = TimeSpan.FromMilliseconds(200);
49+
50+
/// <summary>
51+
/// Gets or sets whether to enable circuit breaker pattern.
52+
/// Default is false. When enabled, prevents cascading failures by temporarily
53+
/// halting requests after consecutive failures.
54+
/// </summary>
55+
/// <example>
56+
/// // Enable circuit breaker
57+
/// Route4MeConfig.EnableCircuitBreaker = true;
58+
/// </example>
59+
public static bool EnableCircuitBreaker { get; set; } = false;
60+
61+
/// <summary>
62+
/// Gets or sets the number of consecutive failures before circuit breaker opens.
63+
/// Default is 5. Only applies when EnableCircuitBreaker is true.
64+
/// </summary>
65+
public static int CircuitBreakerFailureThreshold { get; set; } = 5;
66+
67+
/// <summary>
68+
/// Gets or sets the duration the circuit breaker remains open before allowing retry.
69+
/// Default is 30 seconds. Only applies when EnableCircuitBreaker is true.
70+
/// </summary>
71+
public static TimeSpan CircuitBreakerDuration { get; set; } = TimeSpan.FromSeconds(30);
72+
73+
/// <summary>
74+
/// Gets or sets the callback action invoked when a retry attempt occurs.
75+
/// Useful for logging and monitoring retry behavior.
76+
/// </summary>
77+
/// <example>
78+
/// Route4MeConfig.OnRetry = (exception, timeSpan, retryCount, context) =>
79+
/// {
80+
/// Console.WriteLine($"Retry {retryCount} after {timeSpan.TotalMilliseconds}ms due to {exception.Message}");
81+
/// };
82+
/// </example>
83+
public static Action<Exception, TimeSpan, int, Context> OnRetry { get; set; }
84+
85+
/// <summary>
86+
/// Gets or sets the callback action invoked when circuit breaker opens.
87+
/// Useful for alerting and monitoring.
88+
/// </summary>
89+
public static Action<Exception, TimeSpan> OnCircuitBreakerOpen { get; set; }
2690
}
2791
}

route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeSDKLibrary.csproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ The service is typically used by organizations who must route many drivers to ma
3636
<ItemGroup>
3737
<PackageReference Include="CsvHelper" Version="2.16.3" />
3838
<PackageReference Include="fastJSON" Version="2.4.0.4" />
39-
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.2" />
40-
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
41-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
39+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
40+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
41+
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.11" />
42+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
4243
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
44+
<PackageReference Include="Polly" Version="8.4.2" />
45+
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
4346
<PackageReference Include="SocketIoClientDotNet.Standard" Version="0.0.5" />
4447
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
4548
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

0 commit comments

Comments
 (0)