Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 158 additions & 74 deletions route4me-csharp-sdk/Route4MeSDKLibrary/Managers/Route4MeManagerBase.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

using Polly;
using Polly.Extensions.Http;

namespace Route4MeSDKLibrary.Resilience
{
internal static class HttpResiliencePolicy
{
private static readonly Lazy<IAsyncPolicy<HttpResponseMessage>> _asyncPolicy
= new Lazy<IAsyncPolicy<HttpResponseMessage>>(CreateAsyncPolicy);

private static readonly Lazy<ISyncPolicy<HttpResponseMessage>> _syncPolicy
= new Lazy<ISyncPolicy<HttpResponseMessage>>(CreateSyncPolicy);

private static readonly Random _jitterer = new Random();

public static IAsyncPolicy<HttpResponseMessage> GetAsyncPolicy()
{
return Route4MeConfig.RetryCount > 0
? _asyncPolicy.Value
: Policy.NoOpAsync<HttpResponseMessage>();
}

public static ISyncPolicy<HttpResponseMessage> GetSyncPolicy()
{
return Route4MeConfig.RetryCount > 0
? _syncPolicy.Value
: Policy.NoOp<HttpResponseMessage>();
}

private static IAsyncPolicy<HttpResponseMessage> CreateAsyncPolicy()
{
var retryPolicy = CreateAsyncRetryPolicy();

if (Route4MeConfig.EnableCircuitBreaker)
{
var circuitBreakerPolicy = CreateAsyncCircuitBreakerPolicy();
return Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);
}

return retryPolicy;
}

private static ISyncPolicy<HttpResponseMessage> CreateSyncPolicy()
{
var retryPolicy = CreateSyncRetryPolicy();

if (Route4MeConfig.EnableCircuitBreaker)
{
var circuitBreakerPolicy = CreateSyncCircuitBreakerPolicy();
return Policy.Wrap(retryPolicy, circuitBreakerPolicy);
}

return retryPolicy;
}

private static IAsyncPolicy<HttpResponseMessage> CreateAsyncRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TaskCanceledException>()
.OrResult(r => r.StatusCode == (HttpStatusCode)429)
.WaitAndRetryAsync(
retryCount: Route4MeConfig.RetryCount,
sleepDurationProvider: (retryAttempt) =>
{
var exponentialDelay = Route4MeConfig.RetryInitialDelay
.TotalMilliseconds * Math.Pow(2, retryAttempt - 1);

var jitter = (_jitterer.NextDouble() * 0.4) - 0.2;
var delayWithJitter = exponentialDelay * (1 + jitter);

return TimeSpan.FromMilliseconds(delayWithJitter);
},
onRetry: (outcome, timespan, retryCount, context) =>
{
Route4MeConfig.OnRetry?.Invoke(
outcome.Exception,
timespan,
retryCount,
context
);
}
);
}

private static ISyncPolicy<HttpResponseMessage> CreateSyncRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TaskCanceledException>()
.OrResult(r => r.StatusCode == (HttpStatusCode)429)
.WaitAndRetry(
retryCount: Route4MeConfig.RetryCount,
sleepDurationProvider: (retryAttempt) =>
{
var exponentialDelay = Route4MeConfig.RetryInitialDelay
.TotalMilliseconds * Math.Pow(2, retryAttempt - 1);
var jitter = (_jitterer.NextDouble() * 0.4) - 0.2;
var delayWithJitter = exponentialDelay * (1 + jitter);
return TimeSpan.FromMilliseconds(delayWithJitter);
},
onRetry: (outcome, timespan, retryCount, context) =>
{
Route4MeConfig.OnRetry?.Invoke(
outcome.Exception,
timespan,
retryCount,
context
);
}
);
}

private static IAsyncPolicy<HttpResponseMessage> CreateAsyncCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TaskCanceledException>()
.OrResult(r => r.StatusCode == (HttpStatusCode)429)
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: Route4MeConfig.CircuitBreakerFailureThreshold,
durationOfBreak: Route4MeConfig.CircuitBreakerDuration,
onBreak: (outcome, duration) =>
{
Route4MeConfig.OnCircuitBreakerOpen?.Invoke(
outcome.Exception,
duration
);
},
onReset: () => { },
onHalfOpen: () => { }
);
}

private static ISyncPolicy<HttpResponseMessage> CreateSyncCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TaskCanceledException>()
.OrResult(r => r.StatusCode == (HttpStatusCode)429)
.CircuitBreaker(
handledEventsAllowedBeforeBreaking: Route4MeConfig.CircuitBreakerFailureThreshold,
durationOfBreak: Route4MeConfig.CircuitBreakerDuration,
onBreak: (outcome, duration) =>
{
Route4MeConfig.OnCircuitBreakerOpen?.Invoke(
outcome.Exception,
duration
);
},
onReset: () => { },
onHalfOpen: () => { }
);
}

internal static bool IsTransientError(HttpStatusCode statusCode)
{
return statusCode == HttpStatusCode.RequestTimeout
|| statusCode == (HttpStatusCode)429
|| statusCode == HttpStatusCode.ServiceUnavailable
|| statusCode == HttpStatusCode.GatewayTimeout
|| ((int)statusCode >= 500 && (int)statusCode < 600);
}
}
}
64 changes: 64 additions & 0 deletions route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeConfig.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;

using Polly;

namespace Route4MeSDKLibrary
{
/// <summary>
Expand All @@ -23,5 +25,67 @@ public static TimeSpan HttpTimeout
get => HttpClientHolderManager.RequestsTimeout;
set => HttpClientHolderManager.RequestsTimeout = value;
}

/// <summary>
/// Gets or sets the maximum number of retry attempts for transient HTTP failures.
/// Default is 0 (retry disabled for backward compatibility).
/// Set to a value between 1-10 to enable retry logic.
/// </summary>
/// <example>
/// // Enable 3 retry attempts with exponential backoff
/// Route4MeConfig.RetryCount = 3;
/// </example>
public static int RetryCount { get; set; } = 0;

/// <summary>
/// Gets or sets the initial delay for exponential backoff retry strategy.
/// Default is 200ms. Each subsequent retry doubles this value with jitter.
/// </summary>
/// <example>
/// // Set initial delay to 500ms
/// Route4MeConfig.RetryInitialDelay = TimeSpan.FromMilliseconds(500);
/// </example>
public static TimeSpan RetryInitialDelay { get; set; } = TimeSpan.FromMilliseconds(200);

/// <summary>
/// Gets or sets whether to enable circuit breaker pattern.
/// Default is false. When enabled, prevents cascading failures by temporarily
/// halting requests after consecutive failures.
/// </summary>
/// <example>
/// // Enable circuit breaker
/// Route4MeConfig.EnableCircuitBreaker = true;
/// </example>
public static bool EnableCircuitBreaker { get; set; } = false;

/// <summary>
/// Gets or sets the number of consecutive failures before circuit breaker opens.
/// Default is 5. Only applies when EnableCircuitBreaker is true.
/// </summary>
public static int CircuitBreakerFailureThreshold { get; set; } = 5;

/// <summary>
/// Gets or sets the duration the circuit breaker remains open before allowing retry.
/// Default is 30 seconds. Only applies when EnableCircuitBreaker is true.
/// </summary>
public static TimeSpan CircuitBreakerDuration { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// Gets or sets the callback action invoked when a retry attempt occurs.
/// Useful for logging and monitoring retry behavior.
/// </summary>
/// <example>
/// Route4MeConfig.OnRetry = (exception, timeSpan, retryCount, context) =>
/// {
/// Console.WriteLine($"Retry {retryCount} after {timeSpan.TotalMilliseconds}ms due to {exception.Message}");
/// };
/// </example>
public static Action<Exception, TimeSpan, int, Context> OnRetry { get; set; }

/// <summary>
/// Gets or sets the callback action invoked when circuit breaker opens.
/// Useful for alerting and monitoring.
/// </summary>
public static Action<Exception, TimeSpan> OnCircuitBreakerOpen { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ The service is typically used by organizations who must route many drivers to ma
<ItemGroup>
<PackageReference Include="CsvHelper" Version="2.16.3" />
<PackageReference Include="fastJSON" Version="2.4.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.11" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Polly" Version="8.4.2" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="SocketIoClientDotNet.Standard" Version="0.0.5" />
<PackageReference Include="System.Collections.Immutable" Version="7.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
Expand Down
Loading