diff --git a/route4me-csharp-sdk/Route4MeSDKLibrary/Managers/Route4MeManagerBase.cs b/route4me-csharp-sdk/Route4MeSDKLibrary/Managers/Route4MeManagerBase.cs index 82be0a1..68d415b 100644 --- a/route4me-csharp-sdk/Route4MeSDKLibrary/Managers/Route4MeManagerBase.cs +++ b/route4me-csharp-sdk/Route4MeSDKLibrary/Managers/Route4MeManagerBase.cs @@ -12,10 +12,14 @@ using Newtonsoft.Json; +using Polly.CircuitBreaker; + using Route4MeSDK; using Route4MeSDK.DataTypes.V5; using Route4MeSDK.QueryTypes; +using Route4MeSDKLibrary.Resilience; + namespace Route4MeSDKLibrary.Managers { public abstract class Route4MeManagerBase @@ -167,7 +171,14 @@ protected async Task> GetJsonObjectFromAPIAsync { case HttpMethodType.Get: { - var response = await httpClientHolder.HttpClient.GetStreamAsync(uri.PathAndQuery).ConfigureAwait(false); + var httpResponse = await HttpResiliencePolicy.GetAsyncPolicy() + .ExecuteAsync(async () => + await httpClientHolder.HttpClient.GetAsync(uri.PathAndQuery) + .ConfigureAwait(false)) + .ConfigureAwait(false); + + httpResponse.EnsureSuccessStatusCode(); + var response = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); if (isString) { @@ -203,31 +214,35 @@ protected async Task> GetJsonObjectFromAPIAsync content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } - HttpResponseMessage response; - if (isPut) - { - response = await httpClientHolder.HttpClient.PutAsync(uri.PathAndQuery, content).ConfigureAwait(false); - } - else if (isPatch) - { - content.Headers.ContentType = new MediaTypeHeaderValue("application/json-patch+json"); - response = await httpClientHolder.HttpClient.PatchAsync(uri.PathAndQuery, content).ConfigureAwait(false); - } - else if (isDelete) - { - var request = new HttpRequestMessage + HttpResponseMessage response = await HttpResiliencePolicy.GetAsyncPolicy() + .ExecuteAsync(async () => { - Content = content, - Method = HttpMethod.Delete, - RequestUri = new Uri(uri.PathAndQuery, UriKind.Relative) - }; - response = await httpClientHolder.HttpClient.SendAsync(request).ConfigureAwait(false); - } - else - { - response = await httpClientHolder.HttpClient.PostAsync(uri.PathAndQuery, content) - .ConfigureAwait(false); - } + if (isPut) + { + return await httpClientHolder.HttpClient.PutAsync(uri.PathAndQuery, content).ConfigureAwait(false); + } + else if (isPatch) + { + content.Headers.ContentType = new MediaTypeHeaderValue("application/json-patch+json"); + return await httpClientHolder.HttpClient.PatchAsync(uri.PathAndQuery, content).ConfigureAwait(false); + } + else if (isDelete) + { + var request = new HttpRequestMessage + { + Content = content, + Method = HttpMethod.Delete, + RequestUri = new Uri(uri.PathAndQuery, UriKind.Relative) + }; + return await httpClientHolder.HttpClient.SendAsync(request).ConfigureAwait(false); + } + else + { + return await httpClientHolder.HttpClient.PostAsync(uri.PathAndQuery, content) + .ConfigureAwait(false); + } + }) + .ConfigureAwait(false); // Check if successful if (response.Content is StreamContent) @@ -319,6 +334,35 @@ protected async Task> GetJsonObjectFromAPIAsync } } } + catch (BrokenCircuitException) + { + resultResponse = new ResultResponse + { + Status = false, + Messages = new Dictionary + { + {"CircuitBreakerError", new[] { + "Circuit breaker is open due to consecutive failures. " + + "Requests are temporarily blocked. Please try again later." + }} + } + }; + result = null; + } + catch (HttpRequestException hre) when (Route4MeConfig.RetryCount > 0) + { + resultResponse = new ResultResponse + { + Status = false, + Messages = new Dictionary + { + {"RetryExhausted", new[] { + $"Request failed after {Route4MeConfig.RetryCount} retry attempts: {hre.Message}" + }} + } + }; + result = null; + } catch (HttpListenerException e) { resultResponse = new ResultResponse @@ -409,18 +453,28 @@ protected T GetJsonObjectFromAPI(GenericParameters optimizationParameters, { case HttpMethodType.Get: { - var response = httpClientHolder.HttpClient.GetStreamAsync(uri.PathAndQuery); - response.Wait(); + var httpResponse = HttpResiliencePolicy.GetSyncPolicy() + .Execute(() => + { + var task = httpClientHolder.HttpClient.GetAsync(uri.PathAndQuery); + task.Wait(); + return task.Result; + }); + + httpResponse.EnsureSuccessStatusCode(); + var streamTask = httpResponse.Content.ReadAsStreamAsync(); + streamTask.Wait(); + var response = streamTask.Result; if (isString) { - result = response.Result.ReadString() as T; + result = response.ReadString() as T; } else { result = parseWithNewtonJson - ? response.Result.ReadObjectNew() - : response.Result.ReadObject(); + ? response.ReadObjectNew() + : response.ReadObject(); } break; } @@ -446,43 +500,45 @@ protected T GetJsonObjectFromAPI(GenericParameters optimizationParameters, content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } - Task response; - if (isPut) - { - response = httpClientHolder.HttpClient.PutAsync(uri.PathAndQuery, content); - } - else if (isPatch) - { - //content.Headers.ContentType = new MediaTypeHeaderValue("application/json-patch+json"); - response = httpClientHolder.HttpClient.PatchAsync(uri.PathAndQuery, content); - } - else if (isDelete) - { - var request = new HttpRequestMessage + var response = HttpResiliencePolicy.GetSyncPolicy() + .Execute(() => { - Content = content, - Method = HttpMethod.Delete, - RequestUri = new Uri(uri.PathAndQuery, UriKind.Relative) - }; - response = httpClientHolder.HttpClient.SendAsync(request); - } - else - { - var cts = new CancellationTokenSource(); - cts.CancelAfter(1000 * 60 * 5); // 3 seconds - - response = httpClientHolder.HttpClient.PostAsync(uri.PathAndQuery, content, cts.Token); - } + Task task; + if (isPut) + { + task = httpClientHolder.HttpClient.PutAsync(uri.PathAndQuery, content); + } + else if (isPatch) + { + //content.Headers.ContentType = new MediaTypeHeaderValue("application/json-patch+json"); + task = httpClientHolder.HttpClient.PatchAsync(uri.PathAndQuery, content); + } + else if (isDelete) + { + var request = new HttpRequestMessage + { + Content = content, + Method = HttpMethod.Delete, + RequestUri = new Uri(uri.PathAndQuery, UriKind.Relative) + }; + task = httpClientHolder.HttpClient.SendAsync(request); + } + else + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(1000 * 60 * 5); // 3 seconds + task = httpClientHolder.HttpClient.PostAsync(uri.PathAndQuery, content, cts.Token); + } - // Wait for response - response.Wait(); + task.Wait(); + return task.Result; + }); // Check if successful - if (response.IsCompleted && - response.Result.IsSuccessStatusCode && - response.Result.Content is StreamContent) + if (response.IsSuccessStatusCode && + response.Content is StreamContent) { - var streamTask = ((StreamContent)response.Result.Content).ReadAsStreamAsync(); + var streamTask = ((StreamContent)response.Content).ReadAsStreamAsync(); streamTask.Wait(); if (isString) @@ -496,18 +552,17 @@ protected T GetJsonObjectFromAPI(GenericParameters optimizationParameters, : streamTask.Result.ReadObject(); } } - else if (response.IsCompleted && - response.Result.IsSuccessStatusCode && - response.Result.Content + else if (response.IsSuccessStatusCode && + response.Content .GetType().ToString().ToLower() .Contains("httpconnectionresponsecontent")) { - var streamTask2 = response.Result.Content.ReadAsStreamAsync(); + var streamTask2 = response.Content.ReadAsStreamAsync(); streamTask2.Wait(); if (streamTask2.IsCompleted) { - var content2 = response.Result.Content; + var content2 = response.Content; if (isString) { @@ -528,13 +583,13 @@ protected T GetJsonObjectFromAPI(GenericParameters optimizationParameters, result = JsonConvert.DeserializeObject("{}"); } result.GetType().GetProperty("StatusCode") - ?.SetValue(result, (int)response.Result.StatusCode); + ?.SetValue(result, (int)response.StatusCode); result.GetType().GetProperty("IsSuccessStatusCode") - ?.SetValue(result, response.Result.IsSuccessStatusCode); + ?.SetValue(result, response.IsSuccessStatusCode); result.GetType().GetProperty("Status") - ?.SetValue(result, response.Result.IsSuccessStatusCode); + ?.SetValue(result, response.IsSuccessStatusCode); } } @@ -543,10 +598,10 @@ protected T GetJsonObjectFromAPI(GenericParameters optimizationParameters, Task streamTask = null; Task errorMessageContent = null; - if (response.Result.Content.GetType() == typeof(StreamContent)) - streamTask = ((StreamContent)response.Result.Content).ReadAsStreamAsync(); + if (response.Content.GetType() == typeof(StreamContent)) + streamTask = ((StreamContent)response.Content).ReadAsStreamAsync(); else - errorMessageContent = response.Result.Content.ReadAsStringAsync(); + errorMessageContent = response.Content.ReadAsStringAsync(); streamTask?.Wait(); errorMessageContent?.Wait(); @@ -574,7 +629,7 @@ protected T GetJsonObjectFromAPI(GenericParameters optimizationParameters, } else { - var responseStream = response.Result.Content.ReadAsStringAsync(); + var responseStream = response.Content.ReadAsStringAsync(); responseStream.Wait(); var responseString = responseStream.Result; @@ -594,6 +649,35 @@ protected T GetJsonObjectFromAPI(GenericParameters optimizationParameters, } } } + catch (BrokenCircuitException) + { + resultResponse = new ResultResponse + { + Status = false, + Messages = new Dictionary + { + {"CircuitBreakerError", new[] { + "Circuit breaker is open due to consecutive failures. " + + "Requests are temporarily blocked. Please try again later." + }} + } + }; + result = null; + } + catch (HttpRequestException hre) when (Route4MeConfig.RetryCount > 0) + { + resultResponse = new ResultResponse + { + Status = false, + Messages = new Dictionary + { + {"RetryExhausted", new[] { + $"Request failed after {Route4MeConfig.RetryCount} retry attempts: {hre.Message}" + }} + } + }; + result = null; + } catch (HttpListenerException e) { resultResponse = new ResultResponse diff --git a/route4me-csharp-sdk/Route4MeSDKLibrary/Resilience/HttpResiliencePolicy.cs b/route4me-csharp-sdk/Route4MeSDKLibrary/Resilience/HttpResiliencePolicy.cs new file mode 100644 index 0000000..87cc378 --- /dev/null +++ b/route4me-csharp-sdk/Route4MeSDKLibrary/Resilience/HttpResiliencePolicy.cs @@ -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> _asyncPolicy + = new Lazy>(CreateAsyncPolicy); + + private static readonly Lazy> _syncPolicy + = new Lazy>(CreateSyncPolicy); + + private static readonly Random _jitterer = new Random(); + + public static IAsyncPolicy GetAsyncPolicy() + { + return Route4MeConfig.RetryCount > 0 + ? _asyncPolicy.Value + : Policy.NoOpAsync(); + } + + public static ISyncPolicy GetSyncPolicy() + { + return Route4MeConfig.RetryCount > 0 + ? _syncPolicy.Value + : Policy.NoOp(); + } + + private static IAsyncPolicy CreateAsyncPolicy() + { + var retryPolicy = CreateAsyncRetryPolicy(); + + if (Route4MeConfig.EnableCircuitBreaker) + { + var circuitBreakerPolicy = CreateAsyncCircuitBreakerPolicy(); + return Policy.WrapAsync(retryPolicy, circuitBreakerPolicy); + } + + return retryPolicy; + } + + private static ISyncPolicy CreateSyncPolicy() + { + var retryPolicy = CreateSyncRetryPolicy(); + + if (Route4MeConfig.EnableCircuitBreaker) + { + var circuitBreakerPolicy = CreateSyncCircuitBreakerPolicy(); + return Policy.Wrap(retryPolicy, circuitBreakerPolicy); + } + + return retryPolicy; + } + + private static IAsyncPolicy CreateAsyncRetryPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .Or() + .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 CreateSyncRetryPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .Or() + .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 CreateAsyncCircuitBreakerPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .Or() + .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 CreateSyncCircuitBreakerPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .Or() + .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); + } + } +} \ No newline at end of file diff --git a/route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeConfig.cs b/route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeConfig.cs index 5aa784b..8677f80 100644 --- a/route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeConfig.cs +++ b/route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeConfig.cs @@ -1,5 +1,7 @@ using System; +using Polly; + namespace Route4MeSDKLibrary { /// @@ -23,5 +25,67 @@ public static TimeSpan HttpTimeout get => HttpClientHolderManager.RequestsTimeout; set => HttpClientHolderManager.RequestsTimeout = value; } + + /// + /// 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. + /// + /// + /// // Enable 3 retry attempts with exponential backoff + /// Route4MeConfig.RetryCount = 3; + /// + public static int RetryCount { get; set; } = 0; + + /// + /// Gets or sets the initial delay for exponential backoff retry strategy. + /// Default is 200ms. Each subsequent retry doubles this value with jitter. + /// + /// + /// // Set initial delay to 500ms + /// Route4MeConfig.RetryInitialDelay = TimeSpan.FromMilliseconds(500); + /// + public static TimeSpan RetryInitialDelay { get; set; } = TimeSpan.FromMilliseconds(200); + + /// + /// Gets or sets whether to enable circuit breaker pattern. + /// Default is false. When enabled, prevents cascading failures by temporarily + /// halting requests after consecutive failures. + /// + /// + /// // Enable circuit breaker + /// Route4MeConfig.EnableCircuitBreaker = true; + /// + public static bool EnableCircuitBreaker { get; set; } = false; + + /// + /// Gets or sets the number of consecutive failures before circuit breaker opens. + /// Default is 5. Only applies when EnableCircuitBreaker is true. + /// + public static int CircuitBreakerFailureThreshold { get; set; } = 5; + + /// + /// Gets or sets the duration the circuit breaker remains open before allowing retry. + /// Default is 30 seconds. Only applies when EnableCircuitBreaker is true. + /// + public static TimeSpan CircuitBreakerDuration { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the callback action invoked when a retry attempt occurs. + /// Useful for logging and monitoring retry behavior. + /// + /// + /// Route4MeConfig.OnRetry = (exception, timeSpan, retryCount, context) => + /// { + /// Console.WriteLine($"Retry {retryCount} after {timeSpan.TotalMilliseconds}ms due to {exception.Message}"); + /// }; + /// + public static Action OnRetry { get; set; } + + /// + /// Gets or sets the callback action invoked when circuit breaker opens. + /// Useful for alerting and monitoring. + /// + public static Action OnCircuitBreakerOpen { get; set; } } } \ No newline at end of file diff --git a/route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeSDKLibrary.csproj b/route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeSDKLibrary.csproj index 9358a53..fb87754 100644 --- a/route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeSDKLibrary.csproj +++ b/route4me-csharp-sdk/Route4MeSDKLibrary/Route4MeSDKLibrary.csproj @@ -36,10 +36,13 @@ The service is typically used by organizations who must route many drivers to ma - - - + + + + + + diff --git a/route4me-csharp-sdk/Route4MeSDKTest/Examples/Optimizations/BatchOptimizationWithRetry.cs b/route4me-csharp-sdk/Route4MeSDKTest/Examples/Optimizations/BatchOptimizationWithRetry.cs new file mode 100644 index 0000000..adb5c57 --- /dev/null +++ b/route4me-csharp-sdk/Route4MeSDKTest/Examples/Optimizations/BatchOptimizationWithRetry.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +using Route4MeSDK; +using Route4MeSDK.DataTypes; +using Route4MeSDK.QueryTypes; + +using Route4MeSDKLibrary; + +namespace Route4MeSDK.Examples +{ + /// + /// Example demonstrating batch route optimization with retry and resilience. + /// Creates 15 optimizations with retry logic enabled to handle transient failures. + /// + /// This example showcases: + /// - Configuration of Polly retry policies + /// - Exponential backoff with jitter + /// - Circuit breaker pattern + /// - Batch processing with retry metrics tracking + /// - Proper cleanup of created resources + /// + public sealed partial class Route4MeExamples + { + private static int _retryCount = 0; + private static readonly object _lockObject = new object(); + + public static async Task RunBatchOptimizations(string apiKey) + { + Console.WriteLine("==========================================="); + Console.WriteLine("Batch Optimization with Polly Retry Demo"); + Console.WriteLine("===========================================\n"); + + // Step 1: Configure retry settings + Console.WriteLine("Step 1: Configuring retry and resilience settings..."); + Route4MeConfig.RetryCount = 3; + Route4MeConfig.RetryInitialDelay = TimeSpan.FromMilliseconds(200); + Route4MeConfig.EnableCircuitBreaker = true; + Route4MeConfig.CircuitBreakerFailureThreshold = 5; + Route4MeConfig.CircuitBreakerDuration = TimeSpan.FromSeconds(30); + + Console.WriteLine($" - Retry Count: {Route4MeConfig.RetryCount}"); + Console.WriteLine($" - Initial Delay: {Route4MeConfig.RetryInitialDelay.TotalMilliseconds}ms"); + Console.WriteLine($" - Circuit Breaker: {(Route4MeConfig.EnableCircuitBreaker ? "Enabled" : "Disabled")}"); + Console.WriteLine($" - CB Failure Threshold: {Route4MeConfig.CircuitBreakerFailureThreshold}"); + Console.WriteLine($" - CB Duration: {Route4MeConfig.CircuitBreakerDuration.TotalSeconds}s\n"); + + // Step 2: Set up retry callback for monitoring + Console.WriteLine("Step 2: Setting up monitoring callbacks..."); + Route4MeConfig.OnRetry = (exception, timespan, retryAttempt, context) => + { + lock (_lockObject) + { + _retryCount++; + } + Console.WriteLine($" [RETRY {retryAttempt}] Waiting {timespan.TotalMilliseconds:F0}ms " + + $"before retry. Reason: {exception.Message}"); + }; + + Route4MeConfig.OnCircuitBreakerOpen = (exception, duration) => + { + Console.WriteLine($" [CIRCUIT BREAKER OPEN] Blocking requests for {duration.TotalSeconds}s " + + $"due to: {exception.Message}"); + }; + Console.WriteLine(" Callbacks configured.\n"); + + // Step 3: Create Route4Me manager + Console.WriteLine("Step 3: Creating Route4Me manager..."); + var route4Me = new Route4MeManager(apiKey); + Console.WriteLine(" Manager created.\n"); + + // Step 4: Prepare test addresses + Console.WriteLine("Step 4: Preparing sample addresses..."); + var baseAddresses = CreateSampleAddresses(); + Console.WriteLine($" {baseAddresses.Length} addresses prepared.\n"); + + // Step 5: Execute batch optimizations + var batchSize = 15; + var results = new List(); + var stopwatch = Stopwatch.StartNew(); + + Console.WriteLine($"Step 5: Starting batch of {batchSize} optimizations with retry enabled...\n"); + + // Process optimizations in parallel (simulate realistic load) + var tasks = Enumerable.Range(0, batchSize).Select(async i => + { + var optParams = CreateOptimizationParameters(baseAddresses, i); + + try + { + var startTime = DateTime.Now; + DataObject dataObject = route4Me.RunOptimization(optParams, out string errorString); + var elapsed = DateTime.Now - startTime; + + var optimizationResult = new OptimizationResult + { + Index = i, + Success = dataObject != null && string.IsNullOrEmpty(errorString), + OptimizationId = dataObject?.OptimizationProblemId, + Duration = elapsed, + ErrorMessage = errorString + }; + + var status = optimizationResult.Success ? "SUCCESS" : "FAILED"; + var idDisplay = optimizationResult.OptimizationId ?? "N/A"; + Console.WriteLine($" [{i + 1}/{batchSize}] {status} - ID: {idDisplay} ({elapsed.TotalMilliseconds:F0}ms)"); + + return optimizationResult; + } + catch (Exception ex) + { + Console.WriteLine($" [{i + 1}/{batchSize}] EXCEPTION - {ex.Message}"); + return new OptimizationResult + { + Index = i, + Success = false, + ErrorMessage = ex.Message + }; + } + }); + + results.AddRange(await Task.WhenAll(tasks)); + + stopwatch.Stop(); + + // Step 6: Display statistics + Console.WriteLine("\n==========================================="); + Console.WriteLine("Batch Optimization Results"); + Console.WriteLine("==========================================="); + Console.WriteLine($"Total Optimizations: {batchSize}"); + Console.WriteLine($"Successful: {results.Count(r => r.Success)}"); + Console.WriteLine($"Failed: {results.Count(r => !r.Success)}"); + Console.WriteLine($"Total Retries: {_retryCount}"); + Console.WriteLine($"Average Duration: {results.Average(r => r.Duration.TotalMilliseconds):F0}ms"); + Console.WriteLine($"Total Elapsed Time: {stopwatch.Elapsed.TotalSeconds:F1}s"); + + // Calculate success rate + var successRate = (results.Count(r => r.Success) * 100.0) / batchSize; + Console.WriteLine($"Success Rate: {successRate:F1}%"); + + // Display failed optimizations if any + var failedOpts = results.Where(r => !r.Success).ToList(); + if (failedOpts.Any()) + { + Console.WriteLine("\nFailed Optimizations:"); + foreach (var failed in failedOpts) + { + Console.WriteLine($" - Index {failed.Index + 1}: {failed.ErrorMessage}"); + } + } + + // Step 7: Cleanup + Console.WriteLine("\n==========================================="); + Console.WriteLine("Cleanup"); + Console.WriteLine("==========================================="); + Console.WriteLine("Removing created optimizations..."); + var successfulIds = results + .Where(r => r.Success && !string.IsNullOrEmpty(r.OptimizationId)) + .Select(r => r.OptimizationId) + .ToArray(); + + if (successfulIds.Length > 0) + { + bool cleanupResult = route4Me.RemoveOptimization(successfulIds, out string cleanupError); + if (cleanupResult) + { + Console.WriteLine($" Successfully removed {successfulIds.Length} optimizations."); + } + else + { + Console.WriteLine($" Cleanup FAILED: {cleanupError}"); + } + } + else + { + Console.WriteLine(" No optimizations to clean up."); + } + + Console.WriteLine("\n==========================================="); + Console.WriteLine("Demo Complete"); + Console.WriteLine("===========================================\n"); + } + + /// + /// Creates sample addresses for optimization. + /// These are realistic addresses in Milledgeville, GA. + /// + private static Address[] CreateSampleAddresses() + { + return new[] + { + new Address + { + AddressString = "151 Arbor Way Milledgeville GA 31061", + IsDepot = true, + Latitude = 33.132675170898, + Longitude = -83.244743347168, + Time = 0 + }, + new Address + { + AddressString = "230 Arbor Way Milledgeville GA 31061", + Latitude = 33.129695892334, + Longitude = -83.24577331543, + Time = 300 + }, + new Address + { + AddressString = "148 Bass Rd NE Milledgeville GA 31061", + Latitude = 33.143497, + Longitude = -83.224487, + Time = 300 + }, + new Address + { + AddressString = "117 Bill Johnson Rd NE Milledgeville GA 31061", + Latitude = 33.141784667969, + Longitude = -83.237518310547, + Time = 300 + }, + new Address + { + AddressString = "119 Bill Johnson Rd NE Milledgeville GA 31061", + Latitude = 33.141086578369, + Longitude = -83.238258361816, + Time = 300 + } + }; + } + + /// + /// Creates optimization parameters with unique route name. + /// + private static OptimizationParameters CreateOptimizationParameters(Address[] addresses, int index) + { + return new OptimizationParameters + { + Addresses = addresses, + Parameters = new RouteParameters + { + AlgorithmType = AlgorithmType.CVRP_TW_SD, + RouteName = $"Retry Demo Batch {index + 1} - {DateTime.Now:HHmmss}", + RouteDate = R4MeUtils.ConvertToUnixTimestamp(DateTime.UtcNow.Date.AddDays(1)), + RouteTime = 60 * 60 * 7, // 7 AM + RT = true, + Optimize = Optimize.Distance.Description(), + DistanceUnit = DistanceUnit.MI.Description(), + DeviceType = DeviceType.Web.Description(), + TravelMode = TravelMode.Driving.Description() + } + }; + } + + /// + /// Result container for optimization operations. + /// + private class OptimizationResult + { + public int Index { get; set; } + public bool Success { get; set; } + public string OptimizationId { get; set; } + public TimeSpan Duration { get; set; } + public string ErrorMessage { get; set; } + } + } +} \ No newline at end of file diff --git a/route4me-csharp-sdk/Route4MeSDKTest/Route4MeSDKTest.csproj b/route4me-csharp-sdk/Route4MeSDKTest/Route4MeSDKTest.csproj index e8b4e2a..18aeafd 100644 --- a/route4me-csharp-sdk/Route4MeSDKTest/Route4MeSDKTest.csproj +++ b/route4me-csharp-sdk/Route4MeSDKTest/Route4MeSDKTest.csproj @@ -128,7 +128,7 @@ - + diff --git a/route4me-csharp-sdk/Route4MeSDKTest/appsettings.json b/route4me-csharp-sdk/Route4MeSDKTest/appsettings.json index f9e7674..3bc8c21 100644 --- a/route4me-csharp-sdk/Route4MeSDKTest/appsettings.json +++ b/route4me-csharp-sdk/Route4MeSDKTest/appsettings.json @@ -7,4 +7,4 @@ "s1": "A31AC012AF983C07C772A84693D6BE17", "s2": "A31AC012AF983C07C772A84693D6BE17" } -} \ No newline at end of file +} diff --git a/route4me-csharp-sdk/Route4MeSdkV5UnitTest/V5/ResilienceTests.cs b/route4me-csharp-sdk/Route4MeSdkV5UnitTest/V5/ResilienceTests.cs new file mode 100644 index 0000000..d57f6b7 --- /dev/null +++ b/route4me-csharp-sdk/Route4MeSdkV5UnitTest/V5/ResilienceTests.cs @@ -0,0 +1,195 @@ +using System; + +using NUnit.Framework; + +using Route4MeSDKLibrary; + +namespace Route4MeSdkV5UnitTest.V5 +{ + [TestFixture] + public class ResilienceTests + { + [SetUp] + public void Setup() + { + // Reset to defaults before each test + Route4MeConfig.RetryCount = 0; + Route4MeConfig.RetryInitialDelay = TimeSpan.FromMilliseconds(200); + Route4MeConfig.EnableCircuitBreaker = false; + Route4MeConfig.CircuitBreakerFailureThreshold = 5; + Route4MeConfig.CircuitBreakerDuration = TimeSpan.FromSeconds(30); + Route4MeConfig.OnRetry = null; + Route4MeConfig.OnCircuitBreakerOpen = null; + } + + [Test] + public void RetryCount_DefaultValue_IsZero() + { + // Assert - Verify backward compatibility + Assert.AreEqual(0, Route4MeConfig.RetryCount, + "RetryCount should be 0 by default for backward compatibility"); + } + + [Test] + public void RetryInitialDelay_DefaultValue_Is200Milliseconds() + { + // Assert + Assert.AreEqual(TimeSpan.FromMilliseconds(200), Route4MeConfig.RetryInitialDelay, + "RetryInitialDelay should be 200ms by default"); + } + + [Test] + public void EnableCircuitBreaker_DefaultValue_IsFalse() + { + // Assert - Verify circuit breaker disabled by default + Assert.IsFalse(Route4MeConfig.EnableCircuitBreaker, + "EnableCircuitBreaker should be false by default"); + } + + [Test] + public void CircuitBreakerFailureThreshold_DefaultValue_IsFive() + { + // Assert + Assert.AreEqual(5, Route4MeConfig.CircuitBreakerFailureThreshold, + "CircuitBreakerFailureThreshold should be 5 by default"); + } + + [Test] + public void CircuitBreakerDuration_DefaultValue_Is30Seconds() + { + // Assert + Assert.AreEqual(TimeSpan.FromSeconds(30), Route4MeConfig.CircuitBreakerDuration, + "CircuitBreakerDuration should be 30 seconds by default"); + } + + [Test] + public void RetryCount_CanBeConfigured() + { + // Arrange & Act + Route4MeConfig.RetryCount = 3; + + // Assert + Assert.AreEqual(3, Route4MeConfig.RetryCount); + } + + [Test] + public void RetryInitialDelay_CanBeConfigured() + { + // Arrange & Act + var customDelay = TimeSpan.FromMilliseconds(500); + Route4MeConfig.RetryInitialDelay = customDelay; + + // Assert + Assert.AreEqual(customDelay, Route4MeConfig.RetryInitialDelay); + } + + [Test] + public void EnableCircuitBreaker_CanBeEnabled() + { + // Arrange & Act + Route4MeConfig.EnableCircuitBreaker = true; + + // Assert + Assert.IsTrue(Route4MeConfig.EnableCircuitBreaker); + } + + [Test] + public void CircuitBreakerFailureThreshold_CanBeConfigured() + { + // Arrange & Act + Route4MeConfig.CircuitBreakerFailureThreshold = 10; + + // Assert + Assert.AreEqual(10, Route4MeConfig.CircuitBreakerFailureThreshold); + } + + [Test] + public void CircuitBreakerDuration_CanBeConfigured() + { + // Arrange & Act + var customDuration = TimeSpan.FromMinutes(1); + Route4MeConfig.CircuitBreakerDuration = customDuration; + + // Assert + Assert.AreEqual(customDuration, Route4MeConfig.CircuitBreakerDuration); + } + + [Test] + public void OnRetry_CanBeSetToCallback() + { + // Arrange + bool callbackInvoked = false; + Action callback = (ex, ts, count, ctx) => + { + callbackInvoked = true; + }; + + // Act + Route4MeConfig.OnRetry = callback; + + // Assert + Assert.IsNotNull(Route4MeConfig.OnRetry); + Route4MeConfig.OnRetry?.Invoke(new Exception(), TimeSpan.Zero, 1, new Polly.Context()); + Assert.IsTrue(callbackInvoked, "Callback should have been invoked"); + } + + [Test] + public void OnCircuitBreakerOpen_CanBeSetToCallback() + { + // Arrange + bool callbackInvoked = false; + Action callback = (ex, duration) => + { + callbackInvoked = true; + }; + + // Act + Route4MeConfig.OnCircuitBreakerOpen = callback; + + // Assert + Assert.IsNotNull(Route4MeConfig.OnCircuitBreakerOpen); + Route4MeConfig.OnCircuitBreakerOpen?.Invoke(new Exception(), TimeSpan.Zero); + Assert.IsTrue(callbackInvoked, "Callback should have been invoked"); + } + + [Test] + public void Configuration_SupportsMultipleConfigurations() + { + // Arrange & Act - Configure multiple settings + Route4MeConfig.RetryCount = 5; + Route4MeConfig.RetryInitialDelay = TimeSpan.FromMilliseconds(100); + Route4MeConfig.EnableCircuitBreaker = true; + Route4MeConfig.CircuitBreakerFailureThreshold = 10; + Route4MeConfig.CircuitBreakerDuration = TimeSpan.FromMinutes(2); + + // Assert - All configurations should be maintained + Assert.AreEqual(5, Route4MeConfig.RetryCount); + Assert.AreEqual(100, Route4MeConfig.RetryInitialDelay.TotalMilliseconds); + Assert.IsTrue(Route4MeConfig.EnableCircuitBreaker); + Assert.AreEqual(10, Route4MeConfig.CircuitBreakerFailureThreshold); + Assert.AreEqual(120, Route4MeConfig.CircuitBreakerDuration.TotalSeconds); + } + + [Test] + public void Configuration_CanBeResetToDefaults() + { + // Arrange - Set custom values + Route4MeConfig.RetryCount = 10; + Route4MeConfig.EnableCircuitBreaker = true; + + // Act - Reset to defaults + Route4MeConfig.RetryCount = 0; + Route4MeConfig.RetryInitialDelay = TimeSpan.FromMilliseconds(200); + Route4MeConfig.EnableCircuitBreaker = false; + Route4MeConfig.CircuitBreakerFailureThreshold = 5; + Route4MeConfig.CircuitBreakerDuration = TimeSpan.FromSeconds(30); + + // Assert - Verify defaults + Assert.AreEqual(0, Route4MeConfig.RetryCount); + Assert.AreEqual(200, Route4MeConfig.RetryInitialDelay.TotalMilliseconds); + Assert.IsFalse(Route4MeConfig.EnableCircuitBreaker); + Assert.AreEqual(5, Route4MeConfig.CircuitBreakerFailureThreshold); + Assert.AreEqual(30, Route4MeConfig.CircuitBreakerDuration.TotalSeconds); + } + } +} \ No newline at end of file