From 2e724ecc975f60c2d99431ef49319b5e27e12b7a Mon Sep 17 00:00:00 2001 From: "david.schrenker" Date: Thu, 18 Mar 2021 12:35:49 -0400 Subject: [PATCH 1/7] adding retry handler with example --- .../Examples/Basic/BasicSendWithRetry.cs | 34 +++++ Example Projects/dotNetCoreExample/Program.cs | 51 ++++---- .../InjectionApi/Core/RetryHandler.cs | 116 ++++++++++++++++++ src/SocketLabs/InjectionApi/RetrySettings.cs | 83 +++++++++++++ .../InjectionApi/SocketLabsClient.cs | 77 ++++++------ 5 files changed, 298 insertions(+), 63 deletions(-) create mode 100644 Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs create mode 100644 src/SocketLabs/InjectionApi/Core/RetryHandler.cs create mode 100644 src/SocketLabs/InjectionApi/RetrySettings.cs diff --git a/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs new file mode 100644 index 0000000..09ab113 --- /dev/null +++ b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs @@ -0,0 +1,34 @@ +using System; +using System.Net; +using SocketLabs.InjectionApi; +using SocketLabs.InjectionApi.Message; + +namespace dotNetCoreExample.Examples.Basic +{ + public class BasicSendWithRetry : IExample + { + public SendResponse RunExample() + { + var proxy = new WebProxy("http://localhost:4433", false); + + var client = new SocketLabsClient(ExampleConfig.ServerId, ExampleConfig.ApiKey, proxy) + { + EndpointUrl = ExampleConfig.TargetApi, + RequestTimeout = 5, + RetrySettings = new RetrySettings(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3)) + }; + + var message = new BasicMessage(); + + message.Subject = "Sending A Test Message With Retry Enabled"; + message.HtmlBody = "This is the Html Body of my message."; + message.PlainTextBody = "This is the Plain Text Body of my message."; + + message.From.Email = "from@example.com"; + message.ReplyTo.Email = "replyto@example.com"; + message.To.Add("recipient1@example.com"); + + return client.Send(message); + } + } +} diff --git a/Example Projects/dotNetCoreExample/Program.cs b/Example Projects/dotNetCoreExample/Program.cs index 10b3c87..7e797bb 100644 --- a/Example Projects/dotNetCoreExample/Program.cs +++ b/Example Projects/dotNetCoreExample/Program.cs @@ -53,23 +53,24 @@ private static void DisplayTheMenu() Console.WriteLine(" 6: Basic Send With Custom-Headers "); Console.WriteLine(" 7: Basic Send With Embedded Image "); Console.WriteLine(" 8: Basic Send With Proxy "); - Console.WriteLine(" 9: Basic Send Complex Example "); + Console.WriteLine(" 9: Basic Send With Retry "); + Console.WriteLine(" 10: Basic Send Complex Example "); Console.WriteLine(); Console.WriteLine(" Validation Error Handling Examples: "); - Console.WriteLine(" 10: Basic Send With Invalid Attachment"); - Console.WriteLine(" 11: Basic Send With Invalid From "); - Console.WriteLine(" 12: Basic Send With Invalid Recipients "); + Console.WriteLine(" 11: Basic Send With Invalid Attachment"); + Console.WriteLine(" 12: Basic Send With Invalid From "); + Console.WriteLine(" 13: Basic Send With Invalid Recipients "); Console.WriteLine(); Console.WriteLine(" Bulk Send Examples: "); - Console.WriteLine(" 13: Bulk Send "); - Console.WriteLine(" 14: Bulk Send With MergeData "); - Console.WriteLine(" 15: Bulk Send With Ascii Charset And MergeData "); - Console.WriteLine(" 16: Bulk Send From DataSource With MergeData "); - Console.WriteLine(" 17: Bulk Send Complex Example (Everything including the Kitchen Sink) "); + Console.WriteLine(" 14: Bulk Send "); + Console.WriteLine(" 15: Bulk Send With MergeData "); + Console.WriteLine(" 16: Bulk Send With Ascii Charset And MergeData "); + Console.WriteLine(" 17: Bulk Send From DataSource With MergeData "); + Console.WriteLine(" 18: Bulk Send Complex Example (Everything including the Kitchen Sink) "); Console.WriteLine(); Console.WriteLine(" Amp Examples: "); - Console.WriteLine(" 18: Basic Send With Amp Body "); - Console.WriteLine(" 19: Bulk Send With Amp Body "); + Console.WriteLine(" 19: Basic Send With Amp Body "); + Console.WriteLine(" 20: Bulk Send With Amp Body "); Console.WriteLine(); Console.WriteLine("-------------------------------------------------------------------------"); } @@ -91,18 +92,22 @@ private static string GetExampleName(string selection) case 5: return "dotNetCoreExample.Examples.Basic.BasicSendWithAttachment"; case 6: return "dotNetCoreExample.Examples.Basic.BasicSendWithCustomHeaders"; case 7: return "dotNetCoreExample.Examples.Basic.BasicSendWithEmbeddedImage"; - case 8: return "dotNetCoreExample.Examples.Basic.BasicSendWithProxy"; - case 9: return "dotNetCoreExample.Examples.Basic.BasicComplexExample"; - case 10: return "dotNetCoreExample.Examples.Basic.Invalid.BasicSendWithInvalidAttachment"; - case 11: return "dotNetCoreExample.Examples.Basic.Invalid.BasicSendWithInvalidFrom"; - case 12: return "dotNetCoreExample.Examples.Basic.Invalid.BasicSendWithInvalidRecipients"; - case 13: return "dotNetCoreExample.Examples.Bulk.BulkSend"; - case 14: return "dotNetCoreExample.Examples.Bulk.BulkSendWithMergeData"; - case 15: return "dotNetCoreExample.Examples.Bulk.BulkSendWithAsciiCharsetMergeData"; - case 16: return "dotNetCoreExample.Examples.Bulk.BulkSendFromDataSourceWithMerge"; - case 17: return "dotNetCoreExample.Examples.Bulk.BulkSendComplexExample"; - case 18: return "dotNetCoreExample.Examples.Basic.BasicSendWithAmpBody"; - case 19: return "dotNetCoreExample.Examples.Bulk.BulkSendWithAmpBody"; + case 8: return "dotNetCoreExample.Examples.Basic.BasicSendWithRetry"; + case 9: return "dotNetCoreExample.Examples.Basic.BasicSendWithProxy"; + case 10: return "dotNetCoreExample.Examples.Basic.BasicComplexExample"; + + case 11: return "dotNetCoreExample.Examples.Basic.Invalid.BasicSendWithInvalidAttachment"; + case 12: return "dotNetCoreExample.Examples.Basic.Invalid.BasicSendWithInvalidFrom"; + case 13: return "dotNetCoreExample.Examples.Basic.Invalid.BasicSendWithInvalidRecipients"; + + case 14: return "dotNetCoreExample.Examples.Bulk.BulkSend"; + case 15: return "dotNetCoreExample.Examples.Bulk.BulkSendWithMergeData"; + case 16: return "dotNetCoreExample.Examples.Bulk.BulkSendWithAsciiCharsetMergeData"; + case 17: return "dotNetCoreExample.Examples.Bulk.BulkSendFromDataSourceWithMerge"; + case 18: return "dotNetCoreExample.Examples.Bulk.BulkSendComplexExample"; + + case 19: return "dotNetCoreExample.Examples.Basic.BasicSendWithAmpBody"; + case 20: return "dotNetCoreExample.Examples.Bulk.BulkSendWithAmpBody"; default: Console.WriteLine("Invalid Input (Out of Range)"); diff --git a/src/SocketLabs/InjectionApi/Core/RetryHandler.cs b/src/SocketLabs/InjectionApi/Core/RetryHandler.cs new file mode 100644 index 0000000..47057bb --- /dev/null +++ b/src/SocketLabs/InjectionApi/Core/RetryHandler.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace SocketLabs.InjectionApi.Core +{ + internal class RetryHandler + { + + private readonly HttpClient HttpClient; + private readonly string EndpointUrl; + private readonly RetrySettings RetrySettings; + + private readonly List ErrorStatusCodes = new List() + { + HttpStatusCode.InternalServerError, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout + }; + + /// + /// Creates a new instance of the RetryHandler. + /// + /// A HttpClient instance + /// The SocketLabs Injection API endpoint Url + /// A RetrySettings instance + public RetryHandler(HttpClient httpClient, string endpointUrl, RetrySettings settings) + { + HttpClient = httpClient; + EndpointUrl = endpointUrl; + RetrySettings = settings; + } + + + public async Task SendAsync(StringContent content, CancellationToken cancellationToken) + { + if (RetrySettings.MaximumNumberOfRetries == 0) + { + return await HttpClient.PostAsync(EndpointUrl, content, cancellationToken) + .ConfigureAwait(false); + } + + HttpResponseMessage response = null; + + var numberOfAttempts = 0; + var sent = false; + + while (!sent) + { + var waitFor = this.GetNextWaitInterval(numberOfAttempts); + + try + { + response = await HttpClient.PostAsync(EndpointUrl, content, cancellationToken).ConfigureAwait(false); + + if (ErrorStatusCodes.Contains(response.StatusCode)) + throw new HttpRequestException($"HttpStatusCode: '{response.StatusCode}'. Response contains server error."); + + + sent = true; + } + catch (TaskCanceledException) + { + numberOfAttempts++; + + if (numberOfAttempts > RetrySettings.MaximumNumberOfRetries) + { + throw new TimeoutException(); + } + + // ReSharper disable once MethodSupportsCancellation, cancel will be indicated on the token + await Task.Delay(waitFor).ConfigureAwait(false); + } + catch (HttpRequestException) + { + numberOfAttempts++; + + if (numberOfAttempts > RetrySettings.MaximumNumberOfRetries) + { + throw; + } + + await Task.Delay(waitFor).ConfigureAwait(false); + } + } + + return response; + } + + + + internal virtual int GetRetryDelta(int numberOfAttempts) + { + var random = new Random(); + + var min = (int) (TimeSpan.FromSeconds(1).TotalMilliseconds * 0.8); + var max = (int) (TimeSpan.FromSeconds(1).TotalMilliseconds * 1.2); + + return (int) ((Math.Pow(2.0, numberOfAttempts) - 1.0) * random.Next(min, max)); + } + + private TimeSpan GetNextWaitInterval(int numberOfAttempts) + { + var interval = (int)Math.Min( + RetrySettings.MinimumRetryTimeBetween.TotalMilliseconds + GetRetryDelta(numberOfAttempts), + RetrySettings.MaximumRetryTimeBetween.TotalMilliseconds); + + return TimeSpan.FromMilliseconds(interval); + } + + } +} diff --git a/src/SocketLabs/InjectionApi/RetrySettings.cs b/src/SocketLabs/InjectionApi/RetrySettings.cs new file mode 100644 index 0000000..e64841a --- /dev/null +++ b/src/SocketLabs/InjectionApi/RetrySettings.cs @@ -0,0 +1,83 @@ +using System; + +namespace SocketLabs.InjectionApi +{ + /// + /// + /// + public class RetrySettings + { + + /// + /// The maximum number of retries when sending an Injection API Request before throwing an exception. Default: 0, no retries, you must explicitly enable retry settings + /// + public int MaximumNumberOfRetries { get; } + + /// + /// The minimum wait time between between HTTP retries. Default: 1s + /// + public TimeSpan MinimumRetryTimeBetween { get; } + + /// + /// The maximum wait time between between retries. Default: 10s + /// + public TimeSpan MaximumRetryTimeBetween { get; } + + private const int _defaultNumberOfRetries = 0; + private const int _maximumAllowedNumberOfRetries = 5; + private readonly TimeSpan _defaultMinimumRetryTime = TimeSpan.FromSeconds(1); + private readonly TimeSpan _defaultMaximumRetryTime = TimeSpan.FromSeconds(10); + + /// + /// Creates a new instance of the RetrySettings. + /// + /// + /// + /// + public RetrySettings(int? maximumNumberOfRetries = null, TimeSpan? minimumRetryTimeBetween = null, TimeSpan? maximumRetryTimeBetween = null) + { + + if (maximumNumberOfRetries != null) + { + if (maximumNumberOfRetries < 0) throw new ArgumentOutOfRangeException(nameof(maximumNumberOfRetries), "maximumNumberOfRetries must be greater than 0"); + if (maximumNumberOfRetries > 5) throw new ArgumentOutOfRangeException(nameof(maximumNumberOfRetries), $"The maximum number of allowed retries is {_maximumAllowedNumberOfRetries}"); + + MaximumNumberOfRetries = maximumNumberOfRetries.Value; + } + else + MaximumNumberOfRetries = _defaultNumberOfRetries; + + + + if (minimumRetryTimeBetween != null) + { + if (minimumRetryTimeBetween.Value.Ticks < 0) throw new ArgumentOutOfRangeException(nameof(minimumRetryTimeBetween), "minimumRetryTimeBetween must be greater than 0"); + + MinimumRetryTimeBetween = minimumRetryTimeBetween.Value; + } + else + MinimumRetryTimeBetween = _defaultMinimumRetryTime; + + + if (maximumRetryTimeBetween != null) + { + if (maximumRetryTimeBetween.Value.Ticks < 0) throw new ArgumentOutOfRangeException(nameof(maximumRetryTimeBetween), "maximumRetryTimeBetween must be greater than 0"); + if (maximumRetryTimeBetween.Value.TotalSeconds > 30) throw new ArgumentOutOfRangeException(nameof(maximumRetryTimeBetween), "maximumRetryTimeBetween must be less than 30 seconds"); + + MaximumRetryTimeBetween = maximumRetryTimeBetween.Value; + } + else + MaximumRetryTimeBetween = _defaultMaximumRetryTime; + + + if (minimumRetryTimeBetween != null && maximumRetryTimeBetween != null) + { + if (minimumRetryTimeBetween.Value.TotalMilliseconds > maximumRetryTimeBetween.Value.TotalMilliseconds) + throw new ArgumentOutOfRangeException(nameof(minimumRetryTimeBetween), + "minimumRetryTimeBetween must be less than maximumRetryTimeBetween"); + } + + + } + } +} diff --git a/src/SocketLabs/InjectionApi/SocketLabsClient.cs b/src/SocketLabs/InjectionApi/SocketLabsClient.cs index 948d00b..d256746 100644 --- a/src/SocketLabs/InjectionApi/SocketLabsClient.cs +++ b/src/SocketLabs/InjectionApi/SocketLabsClient.cs @@ -37,7 +37,8 @@ public class SocketLabsClient : ISocketLabsClient, IDisposable private readonly int _serverId; private readonly string _apiKey; private readonly HttpClient _httpClient; - + + /// /// The SocketLabs Injection API endpoint Url /// @@ -47,7 +48,12 @@ public class SocketLabsClient : ISocketLabsClient, IDisposable /// A timeout period for the Injection API request (in Seconds). Default: 120s /// public int RequestTimeout { get; set; } = 120; - + + /// + /// RetrySettings object to define retry setting for the Injection API request. + /// + public RetrySettings RetrySettings { get; set; } = new RetrySettings(); + /// /// Creates a new instance of the SocketLabsClient. /// @@ -220,30 +226,25 @@ public static SendResponse QuickSend( /// public async Task SendAsync(IBasicMessage message, CancellationToken cancellationToken) { - try - { - var validator = new SendValidator(); + var validator = new SendValidator(); - var validationResult = validator.ValidateCredentials(_serverId, _apiKey); - if (validationResult.Result != SendResult.Success) return validationResult; + var validationResult = validator.ValidateCredentials(_serverId, _apiKey); + if (validationResult.Result != SendResult.Success) return validationResult; - validationResult = validator.ValidateMessage(message); - if (validationResult.Result != SendResult.Success) return validationResult; + validationResult = validator.ValidateMessage(message); + if (validationResult.Result != SendResult.Success) return validationResult; - var factory = new InjectionRequestFactory(_serverId, _apiKey); - var injectionRequest = factory.GenerateRequest(message); - var json = injectionRequest.GetAsJson(); + var factory = new InjectionRequestFactory(_serverId, _apiKey); + var injectionRequest = factory.GenerateRequest(message); + var json = injectionRequest.GetAsJson(); - _httpClient.Timeout = TimeSpan.FromSeconds(RequestTimeout); - var httpResponse = await _httpClient.PostAsync(EndpointUrl, json, cancellationToken); - - var response = new InjectionResponseParser().Parse(httpResponse); - return response; - } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) - { - throw new TimeoutException(); - } + _httpClient.Timeout = TimeSpan.FromSeconds(RequestTimeout); + + var retryHandler = new RetryHandler(_httpClient, EndpointUrl, RetrySettings); + var httpResponse = await retryHandler.SendAsync(json, cancellationToken); + + var response = new InjectionResponseParser().Parse(httpResponse); + return response; } /// @@ -280,29 +281,25 @@ public async Task SendAsync(IBasicMessage message, CancellationTok /// public async Task SendAsync(IBulkMessage message, CancellationToken cancellationToken) { - try - { - var validator = new SendValidator(); + var validator = new SendValidator(); - var validationResult = validator.ValidateCredentials(_serverId, _apiKey); - if (validationResult.Result != SendResult.Success) return validationResult; + var validationResult = validator.ValidateCredentials(_serverId, _apiKey); + if (validationResult.Result != SendResult.Success) return validationResult; - validationResult = validator.ValidateMessage(message); - if (validationResult.Result != SendResult.Success) return validationResult; + validationResult = validator.ValidateMessage(message); + if (validationResult.Result != SendResult.Success) return validationResult; - var factory = new InjectionRequestFactory(_serverId, _apiKey); - var injectionRequest = factory.GenerateRequest(message); + var factory = new InjectionRequestFactory(_serverId, _apiKey); + var injectionRequest = factory.GenerateRequest(message); + var json = injectionRequest.GetAsJson(); - _httpClient.Timeout = TimeSpan.FromSeconds(RequestTimeout); - var httpResponse = await _httpClient.PostAsync(EndpointUrl, injectionRequest.GetAsJson(), cancellationToken); + _httpClient.Timeout = TimeSpan.FromSeconds(RequestTimeout); - var response = new InjectionResponseParser().Parse(httpResponse); - return response; - } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) - { - throw new TimeoutException(); - } + var retryHandler = new RetryHandler(_httpClient, EndpointUrl, RetrySettings); + var httpResponse = await retryHandler.SendAsync(json, cancellationToken); + + var response = new InjectionResponseParser().Parse(httpResponse); + return response; } /// From 3038fa3ed1f840317d86393bef8ba685df3c6c19 Mon Sep 17 00:00:00 2001 From: "david.schrenker" Date: Thu, 18 Mar 2021 13:02:28 -0400 Subject: [PATCH 2/7] updating example. --- .../dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs index 09ab113..d8b4334 100644 --- a/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs +++ b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs @@ -14,8 +14,8 @@ public SendResponse RunExample() var client = new SocketLabsClient(ExampleConfig.ServerId, ExampleConfig.ApiKey, proxy) { EndpointUrl = ExampleConfig.TargetApi, - RequestTimeout = 5, - RetrySettings = new RetrySettings(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3)) + RequestTimeout = 120, + RetrySettings = new RetrySettings(3, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)) }; var message = new BasicMessage(); From bc374f915e7e9e32026aa777855f856b47e7f62d Mon Sep 17 00:00:00 2001 From: "david.schrenker" Date: Fri, 19 Mar 2021 13:51:42 -0400 Subject: [PATCH 3/7] refactoring retry handler. --- .../InjectionApi/Core/RetryHandler.cs | 71 ++++++------------ src/SocketLabs/InjectionApi/RetrySettings.cs | 74 +++++++------------ 2 files changed, 50 insertions(+), 95 deletions(-) diff --git a/src/SocketLabs/InjectionApi/Core/RetryHandler.cs b/src/SocketLabs/InjectionApi/Core/RetryHandler.cs index 47057bb..34fe704 100644 --- a/src/SocketLabs/InjectionApi/Core/RetryHandler.cs +++ b/src/SocketLabs/InjectionApi/Core/RetryHandler.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; +// ReSharper disable MethodSupportsCancellation namespace SocketLabs.InjectionApi.Core { internal class RetryHandler @@ -39,78 +40,50 @@ public RetryHandler(HttpClient httpClient, string endpointUrl, RetrySettings set public async Task SendAsync(StringContent content, CancellationToken cancellationToken) { if (RetrySettings.MaximumNumberOfRetries == 0) - { - return await HttpClient.PostAsync(EndpointUrl, content, cancellationToken) + return await HttpClient + .PostAsync(EndpointUrl, content, cancellationToken) .ConfigureAwait(false); - } + HttpResponseMessage response = null; - var numberOfAttempts = 0; - var sent = false; + var attempts = 0; + var waiting = true; - while (!sent) + do { - var waitFor = this.GetNextWaitInterval(numberOfAttempts); + var waitInterval = RetrySettings.GetNextWaitInterval(attempts); try { - response = await HttpClient.PostAsync(EndpointUrl, content, cancellationToken).ConfigureAwait(false); - + response = await HttpClient.PostAsync(EndpointUrl, content, cancellationToken) + .ConfigureAwait(false); + if (ErrorStatusCodes.Contains(response.StatusCode)) - throw new HttpRequestException($"HttpStatusCode: '{response.StatusCode}'. Response contains server error."); - + throw new HttpRequestException( + $"HttpStatusCode: '{response.StatusCode}'. Response contains server error."); - sent = true; + waiting = false; } catch (TaskCanceledException) { - numberOfAttempts++; - - if (numberOfAttempts > RetrySettings.MaximumNumberOfRetries) - { - throw new TimeoutException(); - } - - // ReSharper disable once MethodSupportsCancellation, cancel will be indicated on the token - await Task.Delay(waitFor).ConfigureAwait(false); + attempts++; + if (attempts > RetrySettings.MaximumNumberOfRetries) throw new TimeoutException(); + await Task.Delay(waitInterval).ConfigureAwait(false); } catch (HttpRequestException) { - numberOfAttempts++; - - if (numberOfAttempts > RetrySettings.MaximumNumberOfRetries) - { - throw; - } - - await Task.Delay(waitFor).ConfigureAwait(false); + attempts++; + if (attempts > RetrySettings.MaximumNumberOfRetries) throw; + await Task.Delay(waitInterval).ConfigureAwait(false); } - } - - return response; - } - - - - internal virtual int GetRetryDelta(int numberOfAttempts) - { - var random = new Random(); - var min = (int) (TimeSpan.FromSeconds(1).TotalMilliseconds * 0.8); - var max = (int) (TimeSpan.FromSeconds(1).TotalMilliseconds * 1.2); + } while (waiting); - return (int) ((Math.Pow(2.0, numberOfAttempts) - 1.0) * random.Next(min, max)); + return response; } - private TimeSpan GetNextWaitInterval(int numberOfAttempts) - { - var interval = (int)Math.Min( - RetrySettings.MinimumRetryTimeBetween.TotalMilliseconds + GetRetryDelta(numberOfAttempts), - RetrySettings.MaximumRetryTimeBetween.TotalMilliseconds); - return TimeSpan.FromMilliseconds(interval); - } } } diff --git a/src/SocketLabs/InjectionApi/RetrySettings.cs b/src/SocketLabs/InjectionApi/RetrySettings.cs index e64841a..519be01 100644 --- a/src/SocketLabs/InjectionApi/RetrySettings.cs +++ b/src/SocketLabs/InjectionApi/RetrySettings.cs @@ -8,33 +8,16 @@ namespace SocketLabs.InjectionApi public class RetrySettings { - /// - /// The maximum number of retries when sending an Injection API Request before throwing an exception. Default: 0, no retries, you must explicitly enable retry settings - /// - public int MaximumNumberOfRetries { get; } - - /// - /// The minimum wait time between between HTTP retries. Default: 1s - /// - public TimeSpan MinimumRetryTimeBetween { get; } - - /// - /// The maximum wait time between between retries. Default: 10s - /// - public TimeSpan MaximumRetryTimeBetween { get; } - private const int _defaultNumberOfRetries = 0; private const int _maximumAllowedNumberOfRetries = 5; - private readonly TimeSpan _defaultMinimumRetryTime = TimeSpan.FromSeconds(1); - private readonly TimeSpan _defaultMaximumRetryTime = TimeSpan.FromSeconds(10); + private readonly TimeSpan _minimumRetryTime = TimeSpan.FromSeconds(1); + private readonly TimeSpan _maximumRetryTime = TimeSpan.FromSeconds(10); /// /// Creates a new instance of the RetrySettings. /// /// - /// - /// - public RetrySettings(int? maximumNumberOfRetries = null, TimeSpan? minimumRetryTimeBetween = null, TimeSpan? maximumRetryTimeBetween = null) + public RetrySettings(int? maximumNumberOfRetries = null) { if (maximumNumberOfRetries != null) @@ -46,38 +29,37 @@ public RetrySettings(int? maximumNumberOfRetries = null, TimeSpan? minimumRetryT } else MaximumNumberOfRetries = _defaultNumberOfRetries; + + } + /// + /// The maximum number of retries when sending an Injection API Request before throwing an exception. Default: 0, no retries, you must explicitly enable retry settings + /// + public int MaximumNumberOfRetries { get; } + - if (minimumRetryTimeBetween != null) - { - if (minimumRetryTimeBetween.Value.Ticks < 0) throw new ArgumentOutOfRangeException(nameof(minimumRetryTimeBetween), "minimumRetryTimeBetween must be greater than 0"); - - MinimumRetryTimeBetween = minimumRetryTimeBetween.Value; - } - else - MinimumRetryTimeBetween = _defaultMinimumRetryTime; - - - if (maximumRetryTimeBetween != null) - { - if (maximumRetryTimeBetween.Value.Ticks < 0) throw new ArgumentOutOfRangeException(nameof(maximumRetryTimeBetween), "maximumRetryTimeBetween must be greater than 0"); - if (maximumRetryTimeBetween.Value.TotalSeconds > 30) throw new ArgumentOutOfRangeException(nameof(maximumRetryTimeBetween), "maximumRetryTimeBetween must be less than 30 seconds"); - - MaximumRetryTimeBetween = maximumRetryTimeBetween.Value; - } - else - MaximumRetryTimeBetween = _defaultMaximumRetryTime; - + /// + /// Get the time period to wait before next call + /// + /// + /// + public TimeSpan GetNextWaitInterval(int numberOfAttempts) + { + var interval = (int)Math.Min( + _minimumRetryTime.TotalMilliseconds + GetRetryDelta(numberOfAttempts), + _maximumRetryTime.TotalMilliseconds); - if (minimumRetryTimeBetween != null && maximumRetryTimeBetween != null) - { - if (minimumRetryTimeBetween.Value.TotalMilliseconds > maximumRetryTimeBetween.Value.TotalMilliseconds) - throw new ArgumentOutOfRangeException(nameof(minimumRetryTimeBetween), - "minimumRetryTimeBetween must be less than maximumRetryTimeBetween"); - } + return TimeSpan.FromMilliseconds(interval); + } + internal virtual int GetRetryDelta(int numberOfAttempts) + { + var random = new Random(); + var min = (int)(TimeSpan.FromSeconds(1).TotalMilliseconds * 0.8); + var max = (int)(TimeSpan.FromSeconds(1).TotalMilliseconds * 1.2); + return (int)((Math.Pow(2.0, numberOfAttempts) - 1.0) * random.Next(min, max)); } } } From 1c10fc798ddebf4c4264c66bc1cd35ee209e7c97 Mon Sep 17 00:00:00 2001 From: "david.schrenker" Date: Fri, 19 Mar 2021 15:25:56 -0400 Subject: [PATCH 4/7] refactor retyr loop --- src/SocketLabs/InjectionApi/Core/RetryHandler.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/SocketLabs/InjectionApi/Core/RetryHandler.cs b/src/SocketLabs/InjectionApi/Core/RetryHandler.cs index 34fe704..e6924fe 100644 --- a/src/SocketLabs/InjectionApi/Core/RetryHandler.cs +++ b/src/SocketLabs/InjectionApi/Core/RetryHandler.cs @@ -44,26 +44,21 @@ public async Task SendAsync(StringContent content, Cancella .PostAsync(EndpointUrl, content, cancellationToken) .ConfigureAwait(false); - - HttpResponseMessage response = null; - var attempts = 0; - var waiting = true; - do { var waitInterval = RetrySettings.GetNextWaitInterval(attempts); try { - response = await HttpClient.PostAsync(EndpointUrl, content, cancellationToken) + var response = await HttpClient.PostAsync(EndpointUrl, content, cancellationToken) .ConfigureAwait(false); if (ErrorStatusCodes.Contains(response.StatusCode)) throw new HttpRequestException( $"HttpStatusCode: '{response.StatusCode}'. Response contains server error."); - waiting = false; + return response; } catch (TaskCanceledException) { @@ -78,9 +73,8 @@ public async Task SendAsync(StringContent content, Cancella await Task.Delay(waitInterval).ConfigureAwait(false); } - } while (waiting); + } while (true); - return response; } From 69477d289833c27474588bd07103f2cfafc99f3c Mon Sep 17 00:00:00 2001 From: "david.schrenker" Date: Mon, 22 Mar 2021 11:36:48 -0400 Subject: [PATCH 5/7] refactoring Retry Example --- .../dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs | 2 +- src/SocketLabs/InjectionApi/RetrySettings.cs | 2 +- src/SocketLabs/InjectionApi/SocketLabsClient.cs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs index d8b4334..cf4024c 100644 --- a/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs +++ b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs @@ -15,7 +15,7 @@ public SendResponse RunExample() { EndpointUrl = ExampleConfig.TargetApi, RequestTimeout = 120, - RetrySettings = new RetrySettings(3, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)) + NumberOfRetries = 3 }; var message = new BasicMessage(); diff --git a/src/SocketLabs/InjectionApi/RetrySettings.cs b/src/SocketLabs/InjectionApi/RetrySettings.cs index 519be01..a066bd0 100644 --- a/src/SocketLabs/InjectionApi/RetrySettings.cs +++ b/src/SocketLabs/InjectionApi/RetrySettings.cs @@ -5,7 +5,7 @@ namespace SocketLabs.InjectionApi /// /// /// - public class RetrySettings + internal class RetrySettings { private const int _defaultNumberOfRetries = 0; diff --git a/src/SocketLabs/InjectionApi/SocketLabsClient.cs b/src/SocketLabs/InjectionApi/SocketLabsClient.cs index d256746..6d61a00 100644 --- a/src/SocketLabs/InjectionApi/SocketLabsClient.cs +++ b/src/SocketLabs/InjectionApi/SocketLabsClient.cs @@ -52,7 +52,7 @@ public class SocketLabsClient : ISocketLabsClient, IDisposable /// /// RetrySettings object to define retry setting for the Injection API request. /// - public RetrySettings RetrySettings { get; set; } = new RetrySettings(); + public int NumberOfRetries { get; set; } = 0; /// /// Creates a new instance of the SocketLabsClient. @@ -240,7 +240,7 @@ public async Task SendAsync(IBasicMessage message, CancellationTok _httpClient.Timeout = TimeSpan.FromSeconds(RequestTimeout); - var retryHandler = new RetryHandler(_httpClient, EndpointUrl, RetrySettings); + var retryHandler = new RetryHandler(_httpClient, EndpointUrl, new RetrySettings(NumberOfRetries)); var httpResponse = await retryHandler.SendAsync(json, cancellationToken); var response = new InjectionResponseParser().Parse(httpResponse); @@ -295,7 +295,7 @@ public async Task SendAsync(IBulkMessage message, CancellationToke _httpClient.Timeout = TimeSpan.FromSeconds(RequestTimeout); - var retryHandler = new RetryHandler(_httpClient, EndpointUrl, RetrySettings); + var retryHandler = new RetryHandler(_httpClient, EndpointUrl, new RetrySettings(NumberOfRetries)); var httpResponse = await retryHandler.SendAsync(json, cancellationToken); var response = new InjectionResponseParser().Parse(httpResponse); From ca53d9ec5a5433e4bb44e15c0b839f4398877e76 Mon Sep 17 00:00:00 2001 From: "david.schrenker" Date: Tue, 13 Apr 2021 10:05:16 -0400 Subject: [PATCH 6/7] fixing bug with example. should use synchronous method --- .../dotNetCoreExample/Examples/Basic/BasicSendWithAttachment.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithAttachment.cs b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithAttachment.cs index 7d82b9f..6d53a36 100644 --- a/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithAttachment.cs +++ b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithAttachment.cs @@ -22,7 +22,7 @@ public SendResponse RunExample() message.ReplyTo.Email = "replyto@example.com"; message.To.Add("recipient1@example.com"); - var attachment = message.Attachments.AddAsync("bus.png", MimeType.PNG, @".\examples\img\bus.png").Result; + var attachment = message.Attachments.Add("bus.png", MimeType.PNG, @".\examples\img\bus.png"); attachment.CustomHeaders.Add(new CustomHeader("Color", "Orange")); attachment.CustomHeaders.Add(new CustomHeader("Place", "Beach")); From 542b4f04e08c332e1a16cd2c05ca0466d549a01d Mon Sep 17 00:00:00 2001 From: "david.schrenker" Date: Tue, 13 Apr 2021 10:05:49 -0400 Subject: [PATCH 7/7] updating version - Adding optional retry logic for Http requests. --- README.MD | 4 ++++ manifest/socketlabs.nuspec | 2 +- src/SocketLabs/SocketLabs.csproj | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.MD b/README.MD index 02b5853..98f071c 100644 --- a/README.MD +++ b/README.MD @@ -173,6 +173,9 @@ This example demonstrates how to add custom headers to your email message. ### [Basic send with a web proxy](https://github.com/socketlabs/socketlabs-csharp/blob/master/Example%20Projects/dotNetCoreExample/Examples/Basic/BasicSendWithProxy.cs) This example demonstrates how to use a proxy with your HTTP client. +### [Basic send with retry enabled](https://github.com/socketlabs/socketlabs-csharp/blob/master/Example%20Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs) +This example demonstrates how to use the retry logic with your HTTP client. + ### [Basic send complex example](https://github.com/socketlabs/socketlabs-csharp/blob/master/Example%20Projects/dotNetCoreExample/Examples/Basic/BasicComplexExample.cs) This example demonstrates many features of the Basic Send, including adding multiple recipients, adding message and mailing id's, and adding an embedded image. @@ -205,6 +208,7 @@ For more information about AMP please see [AMP Project](https://amp.dev/document # Version +* 1.2.2 - Adding optional retry logic for Http requests. If configured, the request will retry when certain 500 errors occur (500, 502, 503, 504) * 1.2.1 - Adding request timeout value on the client for Http requests * 1.2.0 - Can now pass in instance of HTTP client, adds .NET 5.0 support * 1.1.0 - Adds Amp Html Support diff --git a/manifest/socketlabs.nuspec b/manifest/socketlabs.nuspec index c463abb..03ee81b 100644 --- a/manifest/socketlabs.nuspec +++ b/manifest/socketlabs.nuspec @@ -2,7 +2,7 @@ SocketLabs.EmailDelivery - 1.2.1 + 1.2.2 SocketLabs.EmailDelivery Joe Cooper, David Schrenker, Matt Reibach, Ryan Lydzinski (Contributor), Mike Goodfellow (Contributor), Saranya Kavuri (Contributor) license.txt diff --git a/src/SocketLabs/SocketLabs.csproj b/src/SocketLabs/SocketLabs.csproj index dd8a864..685bd05 100644 --- a/src/SocketLabs/SocketLabs.csproj +++ b/src/SocketLabs/SocketLabs.csproj @@ -8,7 +8,7 @@ SocketLabs false false - 1.2.1 + 1.2.2 (C) 2018-2021 SocketLabs https://www.socketlabs.com/assets/socketlabs-logo1.png SocketLabs Development Team