From e59348998e30246f848cb77a2b6b7b10e24249e8 Mon Sep 17 00:00:00 2001 From: dylanm Date: Thu, 13 Jul 2017 09:39:05 +0100 Subject: [PATCH 01/14] Added retry handler and reliability settings classes --- .../Reliability/ReliabilitySettings.cs | 30 ++++++++ .../Reliability/RetryDelegatingHandler.cs | 70 +++++++++++++++++++ src/SendGrid/SendGrid.csproj | 1 + 3 files changed, 101 insertions(+) create mode 100644 src/SendGrid/Reliability/ReliabilitySettings.cs create mode 100644 src/SendGrid/Reliability/RetryDelegatingHandler.cs diff --git a/src/SendGrid/Reliability/ReliabilitySettings.cs b/src/SendGrid/Reliability/ReliabilitySettings.cs new file mode 100644 index 000000000..09fad0622 --- /dev/null +++ b/src/SendGrid/Reliability/ReliabilitySettings.cs @@ -0,0 +1,30 @@ +namespace SendGrid.Reliability +{ + using System; + + /// + /// Defines the reliability settings to use on HTTP requests + /// + public class ReliabilitySettings + { + /// + /// Initializes a new instance of the class. + /// Default ctor, sets default property values + /// + public ReliabilitySettings() + { + RetryCount = 0; + RetryInterval = TimeSpan.FromSeconds(1); + } + + /// + /// Gets or sets the number of retries to execute against an HTTP service endpoint before throwing an exceptions. Defaults to 2 + /// + public int RetryCount { get; set; } + + /// + /// Gets or sets the interval between HTTP retries. Defaults to 1 second + /// + public TimeSpan RetryInterval { get; set; } + } +} \ No newline at end of file diff --git a/src/SendGrid/Reliability/RetryDelegatingHandler.cs b/src/SendGrid/Reliability/RetryDelegatingHandler.cs new file mode 100644 index 000000000..64b175c9b --- /dev/null +++ b/src/SendGrid/Reliability/RetryDelegatingHandler.cs @@ -0,0 +1,70 @@ +namespace SendGrid.Reliability +{ + using System; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + + using Polly; + using Polly.Retry; + + public class RetryDelegatingHandler : DelegatingHandler + { + private readonly ReliabilitySettings settings; + + private RetryPolicy retryPolicy; + + public RetryDelegatingHandler(ReliabilitySettings settings) + : this(new HttpClientHandler(), settings) + { + } + + public RetryDelegatingHandler(HttpMessageHandler innerHandler, ReliabilitySettings settings) + : base(innerHandler) + { + this.settings = settings; + ConfigurePolicy(); + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpResponseMessage responseMessage; + + var result = await retryPolicy.ExecuteAndCaptureAsync( + async () => + { + try + { + responseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + EnsureResponseIsValid(responseMessage); + } + catch (TaskCanceledException) + { + throw new TimeoutException(); + } + + return responseMessage; + }); + + if (result.Outcome == OutcomeType.Successful) + { + return result.Result; + } + + throw result.FinalException; + } + + private static void EnsureResponseIsValid(HttpResponseMessage responseMessage) + { + if ((int)responseMessage.StatusCode >= 500 && (int)responseMessage.StatusCode < 600) + { + throw new HttpRequestException(string.Format("Response Http Status code {0} indicates server error", responseMessage.StatusCode)); + } + } + + private void ConfigurePolicy() + { + retryPolicy = Policy.Handle().Or().WaitAndRetryAsync(settings.RetryCount, i => settings.RetryInterval); + } + } +} \ No newline at end of file diff --git a/src/SendGrid/SendGrid.csproj b/src/SendGrid/SendGrid.csproj index cf3e3bf0f..269a853ea 100644 --- a/src/SendGrid/SendGrid.csproj +++ b/src/SendGrid/SendGrid.csproj @@ -18,6 +18,7 @@ + All From 5d0ae1be4b07d62b50cc27d8afcf8291376b3119 Mon Sep 17 00:00:00 2001 From: dylanm Date: Mon, 17 Jul 2017 18:14:32 +0100 Subject: [PATCH 02/14] Test cases for reliability handler --- .../Reliability/ReliabilitySettings.cs | 8 +- .../Reliability/RetryDelegatingHandler.cs | 33 ++++---- src/SendGrid/SendGridClient.cs | 39 +++++++-- src/SendGrid/SendGridClientOptions.cs | 53 ++++++++++++ tests/SendGrid.Tests/Integration.cs | 18 +++++ .../RetryDelegatingHandlerTests.cs | 81 +++++++++++++++++++ .../RetryTestBehaviourDelegatingHandler.cs | 63 +++++++++++++++ 7 files changed, 274 insertions(+), 21 deletions(-) create mode 100644 src/SendGrid/SendGridClientOptions.cs create mode 100644 tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs create mode 100644 tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs diff --git a/src/SendGrid/Reliability/ReliabilitySettings.cs b/src/SendGrid/Reliability/ReliabilitySettings.cs index 09fad0622..e17b18b47 100644 --- a/src/SendGrid/Reliability/ReliabilitySettings.cs +++ b/src/SendGrid/Reliability/ReliabilitySettings.cs @@ -8,8 +8,7 @@ namespace SendGrid.Reliability public class ReliabilitySettings { /// - /// Initializes a new instance of the class. - /// Default ctor, sets default property values + /// Initializes a new instance of the class. /// public ReliabilitySettings() { @@ -17,6 +16,11 @@ public ReliabilitySettings() RetryInterval = TimeSpan.FromSeconds(1); } + /// + /// Gets or sets whether the retry policy is enabled when sending HTTP requests. Defaults to false + /// + public bool UseRetryPolicy { get; set; } + /// /// Gets or sets the number of retries to execute against an HTTP service endpoint before throwing an exceptions. Defaults to 2 /// diff --git a/src/SendGrid/Reliability/RetryDelegatingHandler.cs b/src/SendGrid/Reliability/RetryDelegatingHandler.cs index 64b175c9b..3db3adc7b 100644 --- a/src/SendGrid/Reliability/RetryDelegatingHandler.cs +++ b/src/SendGrid/Reliability/RetryDelegatingHandler.cs @@ -7,7 +7,7 @@ using Polly; using Polly.Retry; - + public class RetryDelegatingHandler : DelegatingHandler { private readonly ReliabilitySettings settings; @@ -28,23 +28,28 @@ public RetryDelegatingHandler(HttpMessageHandler innerHandler, ReliabilitySettin protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + if (!settings.UseRetryPolicy) + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + HttpResponseMessage responseMessage; var result = await retryPolicy.ExecuteAndCaptureAsync( - async () => - { - try - { - responseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - EnsureResponseIsValid(responseMessage); - } - catch (TaskCanceledException) - { - throw new TimeoutException(); - } + async () => + { + try + { + responseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + EnsureResponseIsValid(responseMessage); + } + catch (TaskCanceledException) + { + throw new TimeoutException(); + } - return responseMessage; - }); + return responseMessage; + }); if (result.Outcome == OutcomeType.Successful) { diff --git a/src/SendGrid/SendGridClient.cs b/src/SendGrid/SendGridClient.cs index 6197967ce..7c85199ec 100644 --- a/src/SendGrid/SendGridClient.cs +++ b/src/SendGrid/SendGridClient.cs @@ -17,11 +17,15 @@ namespace SendGrid using System.Threading; using System.Threading.Tasks; + using SendGrid.Reliability; + /// /// A HTTP client wrapper for interacting with SendGrid's API /// public class SendGridClient : ISendGridClient { + private readonly SendGridClientOptions options; + /// /// Gets or sets the path to the API resource. /// @@ -63,16 +67,43 @@ public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dic PreAuthenticate = true, UseDefaultCredentials = false, }; - client = new HttpClient(httpClientHandler); + + var retryHandler = new RetryDelegatingHandler(httpClientHandler, options.ReliabilitySettings); + + client = new HttpClient(retryHandler); } else { - client = new HttpClient(); + client = new HttpClient(new RetryDelegatingHandler(options.ReliabilitySettings)); } InitiateClient(apiKey, host, requestHeaders, version, urlPath); } + /// + /// Initializes a new instance of the class. + /// + /// A instance that defines the configuration settings to use with the client + /// Interface to the SendGrid REST API + public SendGridClient(SendGridClientOptions options) + : this(null, options) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// An optional http client which may me injected in order to facilitate testing. + /// A instance that defines the configuration settings to use with the client + /// Interface to the SendGrid REST API + public SendGridClient(HttpClient httpClient, SendGridClientOptions options) + { + this.options = options; + client = (httpClient == null) ? new HttpClient(new RetryDelegatingHandler(options.ReliabilitySettings)) : httpClient; + + InitiateClient(options.ApiKey, options.Host, options.RequestHeaders, options.Version, options.UrlPath); + } + /// /// Initializes a new instance of the class. /// @@ -84,10 +115,8 @@ public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dic /// Path to endpoint (e.g. /path/to/endpoint) /// Interface to the SendGrid REST API public SendGridClient(HttpClient httpClient, string apiKey, string host = null, Dictionary requestHeaders = null, string version = "v3", string urlPath = null) + : this(httpClient, new SendGridClientOptions() { ApiKey = apiKey, Host = host, RequestHeaders = requestHeaders, Version = version, UrlPath = urlPath }) { - client = (httpClient == null) ? new HttpClient() : httpClient; - - InitiateClient(apiKey, host, requestHeaders, version, urlPath); } /// diff --git a/src/SendGrid/SendGridClientOptions.cs b/src/SendGrid/SendGridClientOptions.cs new file mode 100644 index 000000000..4e4bec5dd --- /dev/null +++ b/src/SendGrid/SendGridClientOptions.cs @@ -0,0 +1,53 @@ +namespace SendGrid +{ + using System.Collections.Generic; + + using SendGrid.Reliability; + + /// + /// Defines the options to use with the SendGrid client + /// + public class SendGridClientOptions + { + /// + /// Initializes a new instance of the class. + /// + public SendGridClientOptions() + { + ReliabilitySettings = new ReliabilitySettings(); + RequestHeaders = new Dictionary(); + Host = "https://api.sendgrid.com"; + Version = "v3"; + } + + /// + /// Gets the reliability settings to use on HTTP Requests + /// + public ReliabilitySettings ReliabilitySettings { get; private set; } + + /// + /// Gets or sets the SendGrid API key + /// + public string ApiKey { get; set; } + + /// + /// Gets or sets the request headers to use on HttpRequests sent to SendGrid + /// + public Dictionary RequestHeaders { get; set; } + + /// + /// Base url (e.g. https://api.sendgrid.com, this is the default) + /// + public string Host { get; set; } + + /// + /// Gets or sets API version, override AddVersion to customize + /// + public string Version { get; set; } + + /// + /// Gets or sets the path to the API endpoint. + /// + public string UrlPath { get; set; } + } +} \ No newline at end of file diff --git a/tests/SendGrid.Tests/Integration.cs b/tests/SendGrid.Tests/Integration.cs index 723c56e48..f2657d395 100644 --- a/tests/SendGrid.Tests/Integration.cs +++ b/tests/SendGrid.Tests/Integration.cs @@ -6047,6 +6047,24 @@ public void TestJsonNetReferenceHandling(string referenceHandlingProperty) bool containsReferenceHandlingProperty = serializedMessage.Contains(referenceHandlingProperty); Assert.False(containsReferenceHandlingProperty); } + + + [Fact] + public async Task TestRetryBehaviour() + { + var host = "http://localhost:4010"; + var headers = new Dictionary { { "X-Mock", "200" } }; + var options = new SendGridClientOptions() { ApiKey = fixture.apiKey, Host = host, RequestHeaders = headers }; + options.ReliabilitySettings.UseRetryPolicy = true; + options.ReliabilitySettings.RetryCount = 2; + var sg = new SendGridClient(options); + var id = "test_url_param"; + + var response = await sg.RequestAsync(method: SendGridClient.Method.POST, urlPath: "whitelabel/links/" + id + "/validate"); + + Assert.True(HttpStatusCode.OK == response.StatusCode); + } + } public class FakeHttpMessageHandler : HttpMessageHandler diff --git a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs new file mode 100644 index 000000000..0bf183f0b --- /dev/null +++ b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using SendGrid.Reliability; +using Xunit; + +namespace SendGrid.Tests.Reliability +{ + public class RetryDelegatingHandlerTests + { + private ReliabilitySettings reliabilitySettings; + + private readonly HttpClient client; + + private readonly RetryTestBehaviourDelegatingHandler innerHandler; + + public RetryDelegatingHandlerTests() + { + reliabilitySettings = new ReliabilitySettings + { + RetryCount = 2, + UseRetryPolicy = true + }; + innerHandler = new RetryTestBehaviourDelegatingHandler(); + client = new HttpClient(new RetryDelegatingHandler(innerHandler, reliabilitySettings)) + { + BaseAddress = new Uri("http://localhost") + }; + } + + [Fact] + public async Task Invoke_ShouldReturnNoErrorWhenOK() + { + innerHandler.ConfigureBehaviour(innerHandler.OK); + + await client.SendAsync(new HttpRequestMessage()); + + Assert.Equal(1, innerHandler.InvocationCount); + } + + [Fact] + public async Task Invoke_ShouldReturnHttpResponseAndNotRetryWhenUnauthorised() + { + innerHandler.ConfigureBehaviour(innerHandler.AuthenticationError); + + var result = await client.SendAsync(new HttpRequestMessage()); + + Assert.Equal(result.StatusCode, HttpStatusCode.Unauthorized); + Assert.Equal(1, innerHandler.InvocationCount); + } + + [Fact] + public async Task Invoke_ShouldReturnErrorWithoutRetryWhenErrorIsNotTransient() + { + innerHandler.ConfigureBehaviour(innerHandler.NonTransientException); + + await Assert.ThrowsAsync( + () => + { + return client.SendAsync(new HttpRequestMessage()); + }); + + Assert.Equal(1, innerHandler.InvocationCount); + } + + [Fact] + public async Task Invoke_ShouldRetryTheExpectedAmountOfTimesAndReturnTimeoutExceptionWhenTasksCancelled() + { + innerHandler.ConfigureBehaviour(innerHandler.TaskCancelled); + + await Assert.ThrowsAsync( + () => + { + return client.SendAsync(new HttpRequestMessage()); + }); + + Assert.Equal(3, innerHandler.InvocationCount); + } + } +} \ No newline at end of file diff --git a/tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs b/tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs new file mode 100644 index 000000000..71b7fcbc7 --- /dev/null +++ b/tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs @@ -0,0 +1,63 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace SendGrid.Tests.Reliability +{ + public class RetryTestBehaviourDelegatingHandler : DelegatingHandler + { + private Func> behaviour; + + public RetryTestBehaviourDelegatingHandler() + { + ConfigureBehaviour(OK); + } + + public int InvocationCount { get; private set; } + + public void ConfigureBehaviour(Func> configuredBehavior) + { + behaviour = () => + { + InvocationCount++; + return configuredBehavior(); + }; + } + + public Task OK() + { + var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("string content") }; + return Task.Factory.StartNew(() => httpResponseMessage); + } + + public Task InternalServerError() + { + var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent("string content") }; + return Task.Factory.StartNew(() => httpResponseMessage); + } + + + public Task AuthenticationError() + { + var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized) { Content = new StringContent("not authorizaed") }; + return Task.Factory.StartNew(() => httpResponseMessage); + } + + public Task TaskCancelled() + { + throw new TaskCanceledException(); + } + + public Task NonTransientException() + { + throw new InvalidOperationException(); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return behaviour(); + } + } +} \ No newline at end of file From 17a268ed78db2c3f17a2f4e6b25cc663fe6b0066 Mon Sep 17 00:00:00 2001 From: dylanm Date: Mon, 17 Jul 2017 18:17:28 +0100 Subject: [PATCH 03/14] Test for 500 internal server error --- src/SendGrid/Reliability/RetryDelegatingHandler.cs | 2 +- .../Reliability/RetryDelegatingHandlerTests.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/SendGrid/Reliability/RetryDelegatingHandler.cs b/src/SendGrid/Reliability/RetryDelegatingHandler.cs index 3db3adc7b..8a2b1c852 100644 --- a/src/SendGrid/Reliability/RetryDelegatingHandler.cs +++ b/src/SendGrid/Reliability/RetryDelegatingHandler.cs @@ -7,7 +7,7 @@ using Polly; using Polly.Retry; - + public class RetryDelegatingHandler : DelegatingHandler { private readonly ReliabilitySettings settings; diff --git a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs index 0bf183f0b..cc7fc2658 100644 --- a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs +++ b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs @@ -77,5 +77,19 @@ await Assert.ThrowsAsync( Assert.Equal(3, innerHandler.InvocationCount); } + + [Fact] + public async Task Invoke_ShouldRetryTheExpectedAmountOfTimesAndReturnExceptionWhenInternalServerErrorsEncountered() + { + innerHandler.ConfigureBehaviour(innerHandler.InternalServerError); + + await Assert.ThrowsAsync( + () => + { + return client.SendAsync(new HttpRequestMessage()); + }); + + Assert.Equal(3, innerHandler.InvocationCount); + } } } \ No newline at end of file From 61d8f1850ff53b858789ba5e39729df062b42fb6 Mon Sep 17 00:00:00 2001 From: dylanm Date: Tue, 18 Jul 2017 09:33:19 +0100 Subject: [PATCH 04/14] Simplified usage of ReliabilitySettings, RetryCount defins whether policy is enabled --- .../Reliability/ReliabilitySettings.cs | 7 +--- .../Reliability/RetryDelegatingHandler.cs | 10 ++++-- src/SendGrid/SendGridClient.cs | 5 +++ src/SendGrid/SendGridClientOptions.cs | 2 +- tests/SendGrid.Tests/Integration.cs | 35 +++++++++++++------ .../RetryDelegatingHandlerTests.cs | 8 ++--- 6 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/SendGrid/Reliability/ReliabilitySettings.cs b/src/SendGrid/Reliability/ReliabilitySettings.cs index e17b18b47..01040f775 100644 --- a/src/SendGrid/Reliability/ReliabilitySettings.cs +++ b/src/SendGrid/Reliability/ReliabilitySettings.cs @@ -17,12 +17,7 @@ public ReliabilitySettings() } /// - /// Gets or sets whether the retry policy is enabled when sending HTTP requests. Defaults to false - /// - public bool UseRetryPolicy { get; set; } - - /// - /// Gets or sets the number of retries to execute against an HTTP service endpoint before throwing an exceptions. Defaults to 2 + /// Gets or sets the number of retries to execute against an HTTP service endpoint before throwing an exceptions. Defaults to 0 (no retries, you must explicitly enable) /// public int RetryCount { get; set; } diff --git a/src/SendGrid/Reliability/RetryDelegatingHandler.cs b/src/SendGrid/Reliability/RetryDelegatingHandler.cs index 8a2b1c852..06a565b21 100644 --- a/src/SendGrid/Reliability/RetryDelegatingHandler.cs +++ b/src/SendGrid/Reliability/RetryDelegatingHandler.cs @@ -14,6 +14,10 @@ public class RetryDelegatingHandler : DelegatingHandler private RetryPolicy retryPolicy; + /// + /// Initializes a new instance of the class. + /// + /// A ReliabilitySettings instance public RetryDelegatingHandler(ReliabilitySettings settings) : this(new HttpClientHandler(), settings) { @@ -28,7 +32,7 @@ public RetryDelegatingHandler(HttpMessageHandler innerHandler, ReliabilitySettin protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - if (!settings.UseRetryPolicy) + if (settings.RetryCount == 0) { return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } @@ -41,7 +45,7 @@ protected override async Task SendAsync(HttpRequestMessage try { responseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - EnsureResponseIsValid(responseMessage); + ThrowHttpRequestExceptionIfResponseIsWithinTheServerErrorRange(responseMessage); } catch (TaskCanceledException) { @@ -59,7 +63,7 @@ protected override async Task SendAsync(HttpRequestMessage throw result.FinalException; } - private static void EnsureResponseIsValid(HttpResponseMessage responseMessage) + private static void ThrowHttpRequestExceptionIfResponseIsWithinTheServerErrorRange(HttpResponseMessage responseMessage) { if ((int)responseMessage.StatusCode >= 500 && (int)responseMessage.StatusCode < 600) { diff --git a/src/SendGrid/SendGridClient.cs b/src/SendGrid/SendGridClient.cs index 7c85199ec..e7a39a9ce 100644 --- a/src/SendGrid/SendGridClient.cs +++ b/src/SendGrid/SendGridClient.cs @@ -98,6 +98,11 @@ public SendGridClient(SendGridClientOptions options) /// Interface to the SendGrid REST API public SendGridClient(HttpClient httpClient, SendGridClientOptions options) { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + this.options = options; client = (httpClient == null) ? new HttpClient(new RetryDelegatingHandler(options.ReliabilitySettings)) : httpClient; diff --git a/src/SendGrid/SendGridClientOptions.cs b/src/SendGrid/SendGridClientOptions.cs index 4e4bec5dd..16970d307 100644 --- a/src/SendGrid/SendGridClientOptions.cs +++ b/src/SendGrid/SendGridClientOptions.cs @@ -36,7 +36,7 @@ public SendGridClientOptions() public Dictionary RequestHeaders { get; set; } /// - /// Base url (e.g. https://api.sendgrid.com, this is the default) + /// Gets or sets base url (e.g. https://api.sendgrid.com, this is the default) /// public string Host { get; set; } diff --git a/tests/SendGrid.Tests/Integration.cs b/tests/SendGrid.Tests/Integration.cs index f2657d395..0e729b5aa 100644 --- a/tests/SendGrid.Tests/Integration.cs +++ b/tests/SendGrid.Tests/Integration.cs @@ -1,4 +1,6 @@ -namespace SendGrid.Tests +using SendGrid.Reliability; + +namespace SendGrid.Tests { using Helpers.Mail; using Newtonsoft.Json; @@ -6050,21 +6052,32 @@ public void TestJsonNetReferenceHandling(string referenceHandlingProperty) [Fact] - public async Task TestRetryBehaviour() + public async Task TestRetryBehaviourThrowsTimeoutException() { - var host = "http://localhost:4010"; - var headers = new Dictionary { { "X-Mock", "200" } }; - var options = new SendGridClientOptions() { ApiKey = fixture.apiKey, Host = host, RequestHeaders = headers }; - options.ReliabilitySettings.UseRetryPolicy = true; - options.ReliabilitySettings.RetryCount = 2; - var sg = new SendGridClient(options); + var msg = new SendGridMessage(); + msg.SetFrom(new EmailAddress("test@example.com")); + msg.AddTo(new EmailAddress("test@example.com")); + msg.SetSubject("Hello World from the SendGrid CSharp Library"); + msg.AddContent(MimeType.Html, "HTML content"); + + var options = new SendGridClientOptions + { + ApiKey = fixture.apiKey, + ReliabilitySettings = {RetryCount = 2} + }; + var id = "test_url_param"; - var response = await sg.RequestAsync(method: SendGridClient.Method.POST, urlPath: "whitelabel/links/" + id + "/validate"); + var httpMessageHandler = new TimeOutExceptionThrowingHttpMessageHandler(20, "The operation timed out"); + var retryHandler = new RetryDelegatingHandler(httpMessageHandler, options.ReliabilitySettings); + + HttpClient clientToInject = new HttpClient(retryHandler); + var sg = new SendGridClient(clientToInject, options); - Assert.True(HttpStatusCode.OK == response.StatusCode); - } + var exception = await Assert.ThrowsAsync(() => sg.SendEmailAsync(msg)); + Assert.NotNull(exception); + } } public class FakeHttpMessageHandler : HttpMessageHandler diff --git a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs index cc7fc2658..2170216ed 100644 --- a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs +++ b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs @@ -19,8 +19,7 @@ public RetryDelegatingHandlerTests() { reliabilitySettings = new ReliabilitySettings { - RetryCount = 2, - UseRetryPolicy = true + RetryCount = 2 }; innerHandler = new RetryTestBehaviourDelegatingHandler(); client = new HttpClient(new RetryDelegatingHandler(innerHandler, reliabilitySettings)) @@ -30,12 +29,13 @@ public RetryDelegatingHandlerTests() } [Fact] - public async Task Invoke_ShouldReturnNoErrorWhenOK() + public async Task Invoke_ShouldReturnHttpResponseAndNotRetryWhenSuccessful() { innerHandler.ConfigureBehaviour(innerHandler.OK); - await client.SendAsync(new HttpRequestMessage()); + var result = await client.SendAsync(new HttpRequestMessage()); + Assert.Equal(result.StatusCode, HttpStatusCode.OK); Assert.Equal(1, innerHandler.InvocationCount); } From 6cce2674b961fee3e076c654ca89c73723e901e1 Mon Sep 17 00:00:00 2001 From: dylanm Date: Wed, 19 Jul 2017 17:23:02 +0100 Subject: [PATCH 05/14] Refactor: Reduced duplication --- src/SendGrid/SendGridClient.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/SendGrid/SendGridClient.cs b/src/SendGrid/SendGridClient.cs index e7a39a9ce..b0aaf361f 100644 --- a/src/SendGrid/SendGridClient.cs +++ b/src/SendGrid/SendGridClient.cs @@ -74,7 +74,7 @@ public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dic } else { - client = new HttpClient(new RetryDelegatingHandler(options.ReliabilitySettings)); + client = CreateHttpClientWithRetryHandler(); } InitiateClient(apiKey, host, requestHeaders, version, urlPath); @@ -104,7 +104,7 @@ public SendGridClient(HttpClient httpClient, SendGridClientOptions options) } this.options = options; - client = (httpClient == null) ? new HttpClient(new RetryDelegatingHandler(options.ReliabilitySettings)) : httpClient; + client = (httpClient == null) ? CreateHttpClientWithRetryHandler() : httpClient; InitiateClient(options.ApiKey, options.Host, options.RequestHeaders, options.Version, options.UrlPath); } @@ -193,6 +193,11 @@ private void InitiateClient(string apiKey, string host, Dictionary /// The supported API methods. /// From c2778f8817f7f986037332ac40b3cc5561d886c2 Mon Sep 17 00:00:00 2001 From: dylanm Date: Thu, 20 Jul 2017 09:24:49 +0100 Subject: [PATCH 06/14] Updated nuspec - added dependency on Polly --- nuspec/Sendgrid.9.5.0.nuspec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nuspec/Sendgrid.9.5.0.nuspec b/nuspec/Sendgrid.9.5.0.nuspec index 3302dbd78..69716ae41 100644 --- a/nuspec/Sendgrid.9.5.0.nuspec +++ b/nuspec/Sendgrid.9.5.0.nuspec @@ -20,11 +20,13 @@ + + From 0d1c9447802af6e2723109ccf2b7b6462b88ebdb Mon Sep 17 00:00:00 2001 From: dylanm Date: Thu, 20 Jul 2017 10:04:12 +0100 Subject: [PATCH 07/14] Defensive checks on ReliabilitySetting --- .../Reliability/ReliabilitySettings.cs | 43 +++++++++++++++++-- .../RetryDelegatingHandlerTests.cs | 16 +++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/SendGrid/Reliability/ReliabilitySettings.cs b/src/SendGrid/Reliability/ReliabilitySettings.cs index 01040f775..e127b8836 100644 --- a/src/SendGrid/Reliability/ReliabilitySettings.cs +++ b/src/SendGrid/Reliability/ReliabilitySettings.cs @@ -7,23 +7,58 @@ namespace SendGrid.Reliability /// public class ReliabilitySettings { + private int retryCount; + private TimeSpan retryInterval; + /// /// Initializes a new instance of the class. /// public ReliabilitySettings() { - RetryCount = 0; - RetryInterval = TimeSpan.FromSeconds(1); + retryCount = 0; + retryInterval = TimeSpan.FromSeconds(1); } /// /// Gets or sets the number of retries to execute against an HTTP service endpoint before throwing an exceptions. Defaults to 0 (no retries, you must explicitly enable) /// - public int RetryCount { get; set; } + public int RetryCount + { + get + { + return retryCount; + } + + set + { + if (value < 0) + { + throw new ArgumentException("Retry count must be greater than zero"); + } + + retryCount = value; + } + } /// /// Gets or sets the interval between HTTP retries. Defaults to 1 second /// - public TimeSpan RetryInterval { get; set; } + public TimeSpan RetryInterval + { + get + { + return retryInterval; + } + + set + { + if (value.TotalSeconds > 30) + { + throw new ArgumentException("Maximum retry interval is 30 seconds"); + } + + retryInterval = value; + } + } } } \ No newline at end of file diff --git a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs index 2170216ed..7988dc10e 100644 --- a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs +++ b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs @@ -91,5 +91,21 @@ await Assert.ThrowsAsync( Assert.Equal(3, innerHandler.InvocationCount); } + + [Fact] + public void ReliabilitySettingsShouldNotAllowNegativeRetryCount() + { + var settings = new ReliabilitySettings(); + + Assert.Throws(() => settings.RetryCount = -1); + } + + [Fact] + public void ReliabilitySettingsShouldNotAllowRetryIntervalGreaterThan30Seconds() + { + var settings = new ReliabilitySettings(); + + Assert.Throws(() => settings.RetryInterval = TimeSpan.FromSeconds(31)); + } } } \ No newline at end of file From 4acfce4cc744c805c664877ac4da336bab10b7ff Mon Sep 17 00:00:00 2001 From: dylanm Date: Thu, 20 Jul 2017 10:09:32 +0100 Subject: [PATCH 08/14] Setting maximum retry count at 5 --- src/SendGrid/Reliability/ReliabilitySettings.cs | 5 +++++ .../Reliability/RetryDelegatingHandlerTests.cs | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/SendGrid/Reliability/ReliabilitySettings.cs b/src/SendGrid/Reliability/ReliabilitySettings.cs index e127b8836..ce6ae8ac6 100644 --- a/src/SendGrid/Reliability/ReliabilitySettings.cs +++ b/src/SendGrid/Reliability/ReliabilitySettings.cs @@ -36,6 +36,11 @@ public int RetryCount throw new ArgumentException("Retry count must be greater than zero"); } + if (value > 5) + { + throw new ArgumentException("Retry count must be less than 5"); + } + retryCount = value; } } diff --git a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs index 7988dc10e..7fe61a581 100644 --- a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs +++ b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs @@ -100,6 +100,14 @@ public void ReliabilitySettingsShouldNotAllowNegativeRetryCount() Assert.Throws(() => settings.RetryCount = -1); } + [Fact] + public void ReliabilitySettingsShouldNotAllowRetryCountGreaterThan5() + { + var settings = new ReliabilitySettings(); + + Assert.Throws(() => settings.RetryCount = 6); + } + [Fact] public void ReliabilitySettingsShouldNotAllowRetryIntervalGreaterThan30Seconds() { From b847c079ee87e1fafc7c52964013d548c0f36902 Mon Sep 17 00:00:00 2001 From: dylanm Date: Tue, 25 Jul 2017 09:10:38 +0100 Subject: [PATCH 09/14] Moved reliability files into Helpers directory --- .../Reliability/ReliabilitySettings.cs | 6 +++--- .../Reliability/RetryDelegatingHandler.cs | 17 ++++++++--------- src/SendGrid/SendGridClient.cs | 4 ++-- src/SendGrid/SendGridClientOptions.cs | 6 +++--- .../Reliability/RetryDelegatingHandlerTests.cs | 4 ++-- .../RetryTestBehaviourDelegatingHandler.cs | 2 +- tests/SendGrid.Tests/Integration.cs | 2 +- 7 files changed, 20 insertions(+), 21 deletions(-) rename src/SendGrid/{ => Helpers}/Reliability/ReliabilitySettings.cs (97%) rename src/SendGrid/{ => Helpers}/Reliability/RetryDelegatingHandler.cs (93%) rename tests/SendGrid.Tests/{ => Helpers}/Reliability/RetryDelegatingHandlerTests.cs (97%) rename tests/SendGrid.Tests/{ => Helpers}/Reliability/RetryTestBehaviourDelegatingHandler.cs (97%) diff --git a/src/SendGrid/Reliability/ReliabilitySettings.cs b/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs similarity index 97% rename from src/SendGrid/Reliability/ReliabilitySettings.cs rename to src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs index ce6ae8ac6..ec3a9f33e 100644 --- a/src/SendGrid/Reliability/ReliabilitySettings.cs +++ b/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs @@ -1,7 +1,7 @@ -namespace SendGrid.Reliability -{ - using System; +using System; +namespace SendGrid.Helpers.Reliability +{ /// /// Defines the reliability settings to use on HTTP requests /// diff --git a/src/SendGrid/Reliability/RetryDelegatingHandler.cs b/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs similarity index 93% rename from src/SendGrid/Reliability/RetryDelegatingHandler.cs rename to src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs index 06a565b21..e5b83064e 100644 --- a/src/SendGrid/Reliability/RetryDelegatingHandler.cs +++ b/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs @@ -1,13 +1,12 @@ -namespace SendGrid.Reliability -{ - using System; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; - - using Polly; - using Polly.Retry; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Polly; +using Polly.Retry; +namespace SendGrid.Helpers.Reliability +{ public class RetryDelegatingHandler : DelegatingHandler { private readonly ReliabilitySettings settings; diff --git a/src/SendGrid/SendGridClient.cs b/src/SendGrid/SendGridClient.cs index b0aaf361f..4323b4930 100644 --- a/src/SendGrid/SendGridClient.cs +++ b/src/SendGrid/SendGridClient.cs @@ -3,6 +3,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using SendGrid.Helpers.Reliability; + namespace SendGrid { using Helpers.Mail; @@ -17,8 +19,6 @@ namespace SendGrid using System.Threading; using System.Threading.Tasks; - using SendGrid.Reliability; - /// /// A HTTP client wrapper for interacting with SendGrid's API /// diff --git a/src/SendGrid/SendGridClientOptions.cs b/src/SendGrid/SendGridClientOptions.cs index 16970d307..bd60b60d1 100644 --- a/src/SendGrid/SendGridClientOptions.cs +++ b/src/SendGrid/SendGridClientOptions.cs @@ -1,9 +1,9 @@ -namespace SendGrid +using SendGrid.Helpers.Reliability; + +namespace SendGrid { using System.Collections.Generic; - using SendGrid.Reliability; - /// /// Defines the options to use with the SendGrid client /// diff --git a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs b/tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs similarity index 97% rename from tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs rename to tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs index 7fe61a581..7936122be 100644 --- a/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs +++ b/tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs @@ -2,10 +2,10 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using SendGrid.Reliability; +using SendGrid.Helpers.Reliability; using Xunit; -namespace SendGrid.Tests.Reliability +namespace SendGrid.Tests.Helpers.Reliability { public class RetryDelegatingHandlerTests { diff --git a/tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs b/tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs similarity index 97% rename from tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs rename to tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs index 71b7fcbc7..472773dc3 100644 --- a/tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs +++ b/tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace SendGrid.Tests.Reliability +namespace SendGrid.Tests.Helpers.Reliability { public class RetryTestBehaviourDelegatingHandler : DelegatingHandler { diff --git a/tests/SendGrid.Tests/Integration.cs b/tests/SendGrid.Tests/Integration.cs index 0e729b5aa..dbf68bc94 100644 --- a/tests/SendGrid.Tests/Integration.cs +++ b/tests/SendGrid.Tests/Integration.cs @@ -1,4 +1,4 @@ -using SendGrid.Reliability; +using SendGrid.Helpers.Reliability; namespace SendGrid.Tests { From dbb6f1000df516ba04d61f17c2ab0b4ffb31ff59 Mon Sep 17 00:00:00 2001 From: dylanm Date: Tue, 25 Jul 2017 09:22:34 +0100 Subject: [PATCH 10/14] Tidied directives --- tests/SendGrid.Tests/Integration.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/SendGrid.Tests/Integration.cs b/tests/SendGrid.Tests/Integration.cs index dbf68bc94..74978630b 100644 --- a/tests/SendGrid.Tests/Integration.cs +++ b/tests/SendGrid.Tests/Integration.cs @@ -1,8 +1,6 @@ -using SendGrid.Helpers.Reliability; - -namespace SendGrid.Tests +namespace SendGrid.Tests { - using Helpers.Mail; + using SendGrid.Helpers.Mail; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -12,8 +10,8 @@ namespace SendGrid.Tests using System.Threading.Tasks; using Xunit; using System.Threading; - using System.Text; using Xunit.Abstractions; + using SendGrid.Helpers.Reliability; public class IntegrationFixture : IDisposable { From 1e9787743e3aa7aca625f3e57be5905e85094b41 Mon Sep 17 00:00:00 2001 From: dylanm Date: Tue, 25 Jul 2017 09:48:57 +0100 Subject: [PATCH 11/14] Changed retry tests to allow multiple behaviours --- .../Reliability/ReliabilitySettings.cs | 4 +- .../Reliability/RetryDelegatingHandler.cs | 16 +++---- .../RetryDelegatingHandlerTests.cs | 48 ++++++++++--------- .../RetryTestBehaviourDelegatingHandler.cs | 15 +++--- 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs b/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs index ec3a9f33e..66ce003b0 100644 --- a/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs +++ b/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs @@ -38,7 +38,7 @@ public int RetryCount if (value > 5) { - throw new ArgumentException("Retry count must be less than 5"); + throw new ArgumentException("The maximum number of retries is 5"); } retryCount = value; @@ -59,7 +59,7 @@ public TimeSpan RetryInterval { if (value.TotalSeconds > 30) { - throw new ArgumentException("Maximum retry interval is 30 seconds"); + throw new ArgumentException("The maximum retry interval is 30 seconds"); } retryInterval = value; diff --git a/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs b/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs index e5b83064e..33556107a 100644 --- a/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs +++ b/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs @@ -1,12 +1,12 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Polly; -using Polly.Retry; - -namespace SendGrid.Helpers.Reliability +namespace SendGrid.Helpers.Reliability { + using System; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using Polly; + using Polly.Retry; + public class RetryDelegatingHandler : DelegatingHandler { private readonly ReliabilitySettings settings; diff --git a/tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs b/tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs index 7936122be..f93c54dfc 100644 --- a/tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs +++ b/tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs @@ -19,7 +19,7 @@ public RetryDelegatingHandlerTests() { reliabilitySettings = new ReliabilitySettings { - RetryCount = 2 + RetryCount = 1 }; innerHandler = new RetryTestBehaviourDelegatingHandler(); client = new HttpClient(new RetryDelegatingHandler(innerHandler, reliabilitySettings)) @@ -31,7 +31,7 @@ public RetryDelegatingHandlerTests() [Fact] public async Task Invoke_ShouldReturnHttpResponseAndNotRetryWhenSuccessful() { - innerHandler.ConfigureBehaviour(innerHandler.OK); + innerHandler.AddBehaviour(innerHandler.OK); var result = await client.SendAsync(new HttpRequestMessage()); @@ -42,7 +42,7 @@ public async Task Invoke_ShouldReturnHttpResponseAndNotRetryWhenSuccessful() [Fact] public async Task Invoke_ShouldReturnHttpResponseAndNotRetryWhenUnauthorised() { - innerHandler.ConfigureBehaviour(innerHandler.AuthenticationError); + innerHandler.AddBehaviour(innerHandler.AuthenticationError); var result = await client.SendAsync(new HttpRequestMessage()); @@ -53,43 +53,45 @@ public async Task Invoke_ShouldReturnHttpResponseAndNotRetryWhenUnauthorised() [Fact] public async Task Invoke_ShouldReturnErrorWithoutRetryWhenErrorIsNotTransient() { - innerHandler.ConfigureBehaviour(innerHandler.NonTransientException); + innerHandler.AddBehaviour(innerHandler.NonTransientException); - await Assert.ThrowsAsync( - () => - { - return client.SendAsync(new HttpRequestMessage()); - }); + await Assert.ThrowsAsync(() => client.SendAsync(new HttpRequestMessage())); Assert.Equal(1, innerHandler.InvocationCount); } + [Fact] + public async Task Invoke_ShoulddRetryOnceWhenFailedOnFirstAttemptThenSuccessful() + { + innerHandler.AddBehaviour(innerHandler.TaskCancelled); + innerHandler.AddBehaviour(innerHandler.OK); + + var result = await client.SendAsync(new HttpRequestMessage()); + + Assert.Equal(result.StatusCode, HttpStatusCode.OK); + Assert.Equal(2, innerHandler.InvocationCount); + } + [Fact] public async Task Invoke_ShouldRetryTheExpectedAmountOfTimesAndReturnTimeoutExceptionWhenTasksCancelled() { - innerHandler.ConfigureBehaviour(innerHandler.TaskCancelled); + innerHandler.AddBehaviour(innerHandler.TaskCancelled); + innerHandler.AddBehaviour(innerHandler.TaskCancelled); - await Assert.ThrowsAsync( - () => - { - return client.SendAsync(new HttpRequestMessage()); - }); + await Assert.ThrowsAsync(() => client.SendAsync(new HttpRequestMessage())); - Assert.Equal(3, innerHandler.InvocationCount); + Assert.Equal(2, innerHandler.InvocationCount); } [Fact] public async Task Invoke_ShouldRetryTheExpectedAmountOfTimesAndReturnExceptionWhenInternalServerErrorsEncountered() { - innerHandler.ConfigureBehaviour(innerHandler.InternalServerError); + innerHandler.AddBehaviour(innerHandler.InternalServerError); + innerHandler.AddBehaviour(innerHandler.InternalServerError); - await Assert.ThrowsAsync( - () => - { - return client.SendAsync(new HttpRequestMessage()); - }); + await Assert.ThrowsAsync(() => client.SendAsync(new HttpRequestMessage())); - Assert.Equal(3, innerHandler.InvocationCount); + Assert.Equal(2, innerHandler.InvocationCount); } [Fact] diff --git a/tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs b/tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs index 472773dc3..06850271d 100644 --- a/tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs +++ b/tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading; @@ -8,22 +9,24 @@ namespace SendGrid.Tests.Helpers.Reliability { public class RetryTestBehaviourDelegatingHandler : DelegatingHandler { - private Func> behaviour; + private readonly List>> behaviours; public RetryTestBehaviourDelegatingHandler() { - ConfigureBehaviour(OK); + behaviours = new List>>(); } public int InvocationCount { get; private set; } - public void ConfigureBehaviour(Func> configuredBehavior) + public void AddBehaviour(Func> configuredBehavior) { - behaviour = () => + Task behaviour() { InvocationCount++; return configuredBehavior(); - }; + } + + behaviours.Add(behaviour); } public Task OK() @@ -57,7 +60,7 @@ public Task NonTransientException() protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - return behaviour(); + return behaviours[InvocationCount](); } } } \ No newline at end of file From 572ffaae8c034ec1902b143e575cefcd3139da56 Mon Sep 17 00:00:00 2001 From: dylanm Date: Tue, 25 Jul 2017 14:17:38 +0100 Subject: [PATCH 12/14] Fixed some naming rules, move retry tests to reliability directory --- .../Reliability/ReliabilitySettings.cs | 17 +++---- .../Reliability/RetryDelegatingHandler.cs | 11 +++-- tests/SendGrid.Tests/Integration.cs | 44 +++++++++++++++++-- .../RetryDelegatingHandlerTests.cs | 16 +++---- .../RetryTestBehaviourDelegatingHandler.cs | 16 +++---- 5 files changed, 72 insertions(+), 32 deletions(-) rename tests/SendGrid.Tests/{Helpers => }/Reliability/RetryDelegatingHandlerTests.cs (95%) rename tests/SendGrid.Tests/{Helpers => }/Reliability/RetryTestBehaviourDelegatingHandler.cs (90%) diff --git a/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs b/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs index 66ce003b0..98b95ada3 100644 --- a/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs +++ b/src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs @@ -1,13 +1,14 @@ -using System; - namespace SendGrid.Helpers.Reliability { + using System; + /// /// Defines the reliability settings to use on HTTP requests /// public class ReliabilitySettings { private int retryCount; + private TimeSpan retryInterval; /// @@ -15,8 +16,8 @@ public class ReliabilitySettings /// public ReliabilitySettings() { - retryCount = 0; - retryInterval = TimeSpan.FromSeconds(1); + this.retryCount = 0; + this.retryInterval = TimeSpan.FromSeconds(1); } /// @@ -26,7 +27,7 @@ public int RetryCount { get { - return retryCount; + return this.retryCount; } set @@ -41,7 +42,7 @@ public int RetryCount throw new ArgumentException("The maximum number of retries is 5"); } - retryCount = value; + this.retryCount = value; } } @@ -52,7 +53,7 @@ public TimeSpan RetryInterval { get { - return retryInterval; + return this.retryInterval; } set @@ -62,7 +63,7 @@ public TimeSpan RetryInterval throw new ArgumentException("The maximum retry interval is 30 seconds"); } - retryInterval = value; + this.retryInterval = value; } } } diff --git a/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs b/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs index 33556107a..bfaa9d810 100644 --- a/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs +++ b/src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs @@ -7,6 +7,9 @@ using Polly; using Polly.Retry; + /// + /// A delegating handler that provides retry functionality while executing a request + /// public class RetryDelegatingHandler : DelegatingHandler { private readonly ReliabilitySettings settings; @@ -26,19 +29,19 @@ public RetryDelegatingHandler(HttpMessageHandler innerHandler, ReliabilitySettin : base(innerHandler) { this.settings = settings; - ConfigurePolicy(); + this.ConfigurePolicy(); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - if (settings.RetryCount == 0) + if (this.settings.RetryCount == 0) { return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } HttpResponseMessage responseMessage; - var result = await retryPolicy.ExecuteAndCaptureAsync( + var result = await this.retryPolicy.ExecuteAndCaptureAsync( async () => { try @@ -72,7 +75,7 @@ private static void ThrowHttpRequestExceptionIfResponseIsWithinTheServerErrorRan private void ConfigurePolicy() { - retryPolicy = Policy.Handle().Or().WaitAndRetryAsync(settings.RetryCount, i => settings.RetryInterval); + this.retryPolicy = Policy.Handle().Or().WaitAndRetryAsync(settings.RetryCount, i => settings.RetryInterval); } } } \ No newline at end of file diff --git a/tests/SendGrid.Tests/Integration.cs b/tests/SendGrid.Tests/Integration.cs index 74978630b..bf9132604 100644 --- a/tests/SendGrid.Tests/Integration.cs +++ b/tests/SendGrid.Tests/Integration.cs @@ -1,4 +1,6 @@ -namespace SendGrid.Tests +using SendGrid.Tests.Reliability; + +namespace SendGrid.Tests { using SendGrid.Helpers.Mail; using Newtonsoft.Json; @@ -43,7 +45,7 @@ public void Dispose() { if (Environment.GetEnvironmentVariable("TRAVIS") != "true") { - process.Kill(); + process.Kill(); Trace.WriteLine("Shutting Down Prism"); } } @@ -6061,12 +6063,15 @@ public async Task TestRetryBehaviourThrowsTimeoutException() var options = new SendGridClientOptions { ApiKey = fixture.apiKey, - ReliabilitySettings = {RetryCount = 2} + ReliabilitySettings = {RetryCount = 1} }; var id = "test_url_param"; - var httpMessageHandler = new TimeOutExceptionThrowingHttpMessageHandler(20, "The operation timed out"); + var httpMessageHandler = new RetryTestBehaviourDelegatingHandler(); + httpMessageHandler.AddBehaviour(httpMessageHandler.TaskCancelled); + httpMessageHandler.AddBehaviour(httpMessageHandler.TaskCancelled); + var retryHandler = new RetryDelegatingHandler(httpMessageHandler, options.ReliabilitySettings); HttpClient clientToInject = new HttpClient(retryHandler); @@ -6076,6 +6081,37 @@ public async Task TestRetryBehaviourThrowsTimeoutException() Assert.NotNull(exception); } + + [Fact] + public async Task TestRetryBehaviourSucceedsOnSecondAttempt() + { + var msg = new SendGridMessage(); + msg.SetFrom(new EmailAddress("test@example.com")); + msg.AddTo(new EmailAddress("test@example.com")); + msg.SetSubject("Hello World from the SendGrid CSharp Library"); + msg.AddContent(MimeType.Html, "HTML content"); + + var options = new SendGridClientOptions + { + ApiKey = fixture.apiKey, + ReliabilitySettings = { RetryCount = 1 } + }; + + var id = "test_url_param"; + + var httpMessageHandler = new RetryTestBehaviourDelegatingHandler(); + httpMessageHandler.AddBehaviour(httpMessageHandler.TaskCancelled); + httpMessageHandler.AddBehaviour(httpMessageHandler.OK); + + var retryHandler = new RetryDelegatingHandler(httpMessageHandler, options.ReliabilitySettings); + + HttpClient clientToInject = new HttpClient(retryHandler); + var sg = new SendGridClient(clientToInject, options); + + var result = await sg.SendEmailAsync(msg); + + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + } } public class FakeHttpMessageHandler : HttpMessageHandler diff --git a/tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs similarity index 95% rename from tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs rename to tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs index f93c54dfc..7f1f581fc 100644 --- a/tests/SendGrid.Tests/Helpers/Reliability/RetryDelegatingHandlerTests.cs +++ b/tests/SendGrid.Tests/Reliability/RetryDelegatingHandlerTests.cs @@ -1,12 +1,12 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using SendGrid.Helpers.Reliability; -using Xunit; - -namespace SendGrid.Tests.Helpers.Reliability +namespace SendGrid.Tests.Reliability { + using System; + using System.Net; + using System.Net.Http; + using System.Threading.Tasks; + using SendGrid.Helpers.Reliability; + using Xunit; + public class RetryDelegatingHandlerTests { private ReliabilitySettings reliabilitySettings; diff --git a/tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs b/tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs similarity index 90% rename from tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs rename to tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs index 06850271d..ad17cae45 100644 --- a/tests/SendGrid.Tests/Helpers/Reliability/RetryTestBehaviourDelegatingHandler.cs +++ b/tests/SendGrid.Tests/Reliability/RetryTestBehaviourDelegatingHandler.cs @@ -1,12 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace SendGrid.Tests.Helpers.Reliability +namespace SendGrid.Tests.Reliability { + using System; + using System.Collections.Generic; + using System.Net; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + public class RetryTestBehaviourDelegatingHandler : DelegatingHandler { private readonly List>> behaviours; From 39a8269600f0f6ef11d6486e05fd8fa3c8b22cc4 Mon Sep 17 00:00:00 2001 From: Dylan Morley Date: Wed, 26 Jul 2017 09:35:36 +0100 Subject: [PATCH 13/14] Added Transient Fault Handling documentation --- USE_CASES.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/USE_CASES.md b/USE_CASES.md index e60b428a1..2b80cc8e2 100644 --- a/USE_CASES.md +++ b/USE_CASES.md @@ -8,6 +8,7 @@ This documentation provides examples for specific use cases. Please [open an iss * [Email - Send a Single Email to a Single Recipient](#singleemailsinglerecipient) * [Email - Send Multiple Emails to Multiple Recipients](#multipleemailsmultiplerecipients) * [Email - Transactional Templates](#transactional_templates) +* [Transient Fault Handling](#transient_faults) # Attachments @@ -580,3 +581,57 @@ namespace Example } } ``` + + + +# Transient Fault Handling + +The SendGridClient provides functionality for handling transient errors that might occur when sending an HttpRequest. The library makes use of the [Polly](https://github.com/App-vNext/Polly "Polly") package to attempt to recover from failures. + +By default, retry behaviour is off, you must explicitly enable it by setting the retry count to a value greater than zero. To set the retry count, you must use the SendGridClient construct that takes a **SendGridClientOptions** object, allowing you to configure the **ReliabilitySettings** + +##RetryCount + +The amount of times to retry the operation before reporting an exception to the caller. This is in addition to the initial attempt, so setting a value of 1 would result in 2 attempts - the initial attempt and the retry. Defaults to zero, so retry behaviour is not enabled. The maximum amount of retries permitted is 5. + +##RetryInterval + +The policy implemented is a 'wait and retry'. The RetryInterval setting defines the amount of time to wait between operations that fail before attmepting a retry. By default, this is set to 1 second - the maximum amount of time allowed is 30 seconds. + +##Examples + +In this example, we are setting RetryCount to 1, with an interval between attempts of 5 seconds. This means that if the inital attempt to send mail fails, then it will wait 5 seconds then try again a single time. + +```csharp + +var options = new SendGridClientOptions +{ + ApiKey = "Your-Api-Key", + ReliabilitySettings = {RetryCount = 1, RetryInterval = TimeSpan.FromSeconds(5)} +}; + +var client = new SendGridClient(options); + +``` + +The SendGridClientOptions object defines all the settings that can be set for the client, e.g. + +```csharp + +var options = new SendGridClientOptions +{ + ApiKey = "Your-Api-Key", + ReliabilitySettings = {RetryCount = 1, RetryInterval = TimeSpan.FromSeconds(5)}, + Host = "Your-Host", + UrlPath = "Url-Path", + Version = "3", + RequestHeaders = new Dictionary() {{"header-key", "header-value"}} +}; + +var client = new SendGridClient(options); + +``` +Use the SendGridClientOptions object to set ReliabilitySettings when you want to introduce an element of resiliancy against transient faults into your solution. + + + From a8172ffbea89e394d674e4096f10eb413b892dcd Mon Sep 17 00:00:00 2001 From: Dylan Morley Date: Wed, 26 Jul 2017 09:36:28 +0100 Subject: [PATCH 14/14] Corrected spacing on markdown --- USE_CASES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/USE_CASES.md b/USE_CASES.md index 2b80cc8e2..ad6e0199a 100644 --- a/USE_CASES.md +++ b/USE_CASES.md @@ -590,15 +590,15 @@ The SendGridClient provides functionality for handling transient errors that mig By default, retry behaviour is off, you must explicitly enable it by setting the retry count to a value greater than zero. To set the retry count, you must use the SendGridClient construct that takes a **SendGridClientOptions** object, allowing you to configure the **ReliabilitySettings** -##RetryCount +### RetryCount The amount of times to retry the operation before reporting an exception to the caller. This is in addition to the initial attempt, so setting a value of 1 would result in 2 attempts - the initial attempt and the retry. Defaults to zero, so retry behaviour is not enabled. The maximum amount of retries permitted is 5. -##RetryInterval +### RetryInterval The policy implemented is a 'wait and retry'. The RetryInterval setting defines the amount of time to wait between operations that fail before attmepting a retry. By default, this is set to 1 second - the maximum amount of time allowed is 30 seconds. -##Examples +## Examples In this example, we are setting RetryCount to 1, with an interval between attempts of 5 seconds. This means that if the inital attempt to send mail fails, then it will wait 5 seconds then try again a single time.