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")); diff --git a/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs b/Example Projects/dotNetCoreExample/Examples/Basic/BasicSendWithRetry.cs new file mode 100644 index 0000000..cf4024c --- /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 = 120, + NumberOfRetries = 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/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/InjectionApi/Core/RetryHandler.cs b/src/SocketLabs/InjectionApi/Core/RetryHandler.cs new file mode 100644 index 0000000..e6924fe --- /dev/null +++ b/src/SocketLabs/InjectionApi/Core/RetryHandler.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +// ReSharper disable MethodSupportsCancellation +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); + + var attempts = 0; + do + { + var waitInterval = RetrySettings.GetNextWaitInterval(attempts); + + try + { + 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."); + + return response; + } + catch (TaskCanceledException) + { + attempts++; + if (attempts > RetrySettings.MaximumNumberOfRetries) throw new TimeoutException(); + await Task.Delay(waitInterval).ConfigureAwait(false); + } + catch (HttpRequestException) + { + attempts++; + if (attempts > RetrySettings.MaximumNumberOfRetries) throw; + await Task.Delay(waitInterval).ConfigureAwait(false); + } + + } while (true); + + } + + + + } +} diff --git a/src/SocketLabs/InjectionApi/RetrySettings.cs b/src/SocketLabs/InjectionApi/RetrySettings.cs new file mode 100644 index 0000000..a066bd0 --- /dev/null +++ b/src/SocketLabs/InjectionApi/RetrySettings.cs @@ -0,0 +1,65 @@ +using System; + +namespace SocketLabs.InjectionApi +{ + /// + /// + /// + internal class RetrySettings + { + + private const int _defaultNumberOfRetries = 0; + private const int _maximumAllowedNumberOfRetries = 5; + 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) + { + + 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; + + } + + + /// + /// 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; } + + + /// + /// 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); + + 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)); + } + } +} diff --git a/src/SocketLabs/InjectionApi/SocketLabsClient.cs b/src/SocketLabs/InjectionApi/SocketLabsClient.cs index 948d00b..6d61a00 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 int NumberOfRetries { get; set; } = 0; + /// /// 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, new RetrySettings(NumberOfRetries)); + 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, new RetrySettings(NumberOfRetries)); + var httpResponse = await retryHandler.SendAsync(json, cancellationToken); + + var response = new InjectionResponseParser().Parse(httpResponse); + return response; } /// 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