Skip to content

Commit bd58fdf

Browse files
Merge pull request #497 from dylan-asos/master
Transient Fault Handling
2 parents bbd2420 + a8172ff commit bd58fdf

10 files changed

+562
-9
lines changed

USE_CASES.md

+55
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This documentation provides examples for specific use cases. Please [open an iss
88
* [Email - Send a Single Email to a Single Recipient](#singleemailsinglerecipient)
99
* [Email - Send Multiple Emails to Multiple Recipients](#multipleemailsmultiplerecipients)
1010
* [Email - Transactional Templates](#transactional_templates)
11+
* [Transient Fault Handling](#transient_faults)
1112

1213
<a name="attachments"></a>
1314
# Attachments
@@ -580,3 +581,57 @@ namespace Example
580581
}
581582
}
582583
```
584+
585+
586+
<a name="transient_faults"></a>
587+
# Transient Fault Handling
588+
589+
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.
590+
591+
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**
592+
593+
### RetryCount
594+
595+
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.
596+
597+
### RetryInterval
598+
599+
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.
600+
601+
## Examples
602+
603+
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.
604+
605+
```csharp
606+
607+
var options = new SendGridClientOptions
608+
{
609+
ApiKey = "Your-Api-Key",
610+
ReliabilitySettings = {RetryCount = 1, RetryInterval = TimeSpan.FromSeconds(5)}
611+
};
612+
613+
var client = new SendGridClient(options);
614+
615+
```
616+
617+
The SendGridClientOptions object defines all the settings that can be set for the client, e.g.
618+
619+
```csharp
620+
621+
var options = new SendGridClientOptions
622+
{
623+
ApiKey = "Your-Api-Key",
624+
ReliabilitySettings = {RetryCount = 1, RetryInterval = TimeSpan.FromSeconds(5)},
625+
Host = "Your-Host",
626+
UrlPath = "Url-Path",
627+
Version = "3",
628+
RequestHeaders = new Dictionary<string, string>() {{"header-key", "header-value"}}
629+
};
630+
631+
var client = new SendGridClient(options);
632+
633+
```
634+
Use the SendGridClientOptions object to set ReliabilitySettings when you want to introduce an element of resiliancy against transient faults into your solution.
635+
636+
637+

nuspec/Sendgrid.9.5.2.nuspec

+2
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
<group targetFramework=".NETFramework4.0">
2020
<dependency id="Newtonsoft.Json" version="9.0.1" />
2121
<dependency id="System.Net.Http" version="4.0.0" />
22+
<dependency id="Polly" version="5.2.0" />
2223
</group>
2324
<group targetFramework=".NETStandard1.3">
2425
<dependency id="NETStandard.Library" version="1.6.1" />
2526
<dependency id="Newtonsoft.Json" version="9.0.1" />
2627
<dependency id="Microsoft.AspNetCore.Http.Abstractions" version="1.1.0" />
28+
<dependency id="Polly" version="5.2.0" />
2729
</group>
2830
</dependencies>
2931
</metadata>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
namespace SendGrid.Helpers.Reliability
2+
{
3+
using System;
4+
5+
/// <summary>
6+
/// Defines the reliability settings to use on HTTP requests
7+
/// </summary>
8+
public class ReliabilitySettings
9+
{
10+
private int retryCount;
11+
12+
private TimeSpan retryInterval;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="ReliabilitySettings"/> class.
16+
/// </summary>
17+
public ReliabilitySettings()
18+
{
19+
this.retryCount = 0;
20+
this.retryInterval = TimeSpan.FromSeconds(1);
21+
}
22+
23+
/// <summary>
24+
/// 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)
25+
/// </summary>
26+
public int RetryCount
27+
{
28+
get
29+
{
30+
return this.retryCount;
31+
}
32+
33+
set
34+
{
35+
if (value < 0)
36+
{
37+
throw new ArgumentException("Retry count must be greater than zero");
38+
}
39+
40+
if (value > 5)
41+
{
42+
throw new ArgumentException("The maximum number of retries is 5");
43+
}
44+
45+
this.retryCount = value;
46+
}
47+
}
48+
49+
/// <summary>
50+
/// Gets or sets the interval between HTTP retries. Defaults to 1 second
51+
/// </summary>
52+
public TimeSpan RetryInterval
53+
{
54+
get
55+
{
56+
return this.retryInterval;
57+
}
58+
59+
set
60+
{
61+
if (value.TotalSeconds > 30)
62+
{
63+
throw new ArgumentException("The maximum retry interval is 30 seconds");
64+
}
65+
66+
this.retryInterval = value;
67+
}
68+
}
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
namespace SendGrid.Helpers.Reliability
2+
{
3+
using System;
4+
using System.Net.Http;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Polly;
8+
using Polly.Retry;
9+
10+
/// <summary>
11+
/// A delegating handler that provides retry functionality while executing a request
12+
/// </summary>
13+
public class RetryDelegatingHandler : DelegatingHandler
14+
{
15+
private readonly ReliabilitySettings settings;
16+
17+
private RetryPolicy retryPolicy;
18+
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="RetryDelegatingHandler"/> class.
21+
/// </summary>
22+
/// <param name="settings">A ReliabilitySettings instance</param>
23+
public RetryDelegatingHandler(ReliabilitySettings settings)
24+
: this(new HttpClientHandler(), settings)
25+
{
26+
}
27+
28+
public RetryDelegatingHandler(HttpMessageHandler innerHandler, ReliabilitySettings settings)
29+
: base(innerHandler)
30+
{
31+
this.settings = settings;
32+
this.ConfigurePolicy();
33+
}
34+
35+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
36+
{
37+
if (this.settings.RetryCount == 0)
38+
{
39+
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
40+
}
41+
42+
HttpResponseMessage responseMessage;
43+
44+
var result = await this.retryPolicy.ExecuteAndCaptureAsync(
45+
async () =>
46+
{
47+
try
48+
{
49+
responseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
50+
ThrowHttpRequestExceptionIfResponseIsWithinTheServerErrorRange(responseMessage);
51+
}
52+
catch (TaskCanceledException)
53+
{
54+
throw new TimeoutException();
55+
}
56+
57+
return responseMessage;
58+
});
59+
60+
if (result.Outcome == OutcomeType.Successful)
61+
{
62+
return result.Result;
63+
}
64+
65+
throw result.FinalException;
66+
}
67+
68+
private static void ThrowHttpRequestExceptionIfResponseIsWithinTheServerErrorRange(HttpResponseMessage responseMessage)
69+
{
70+
if ((int)responseMessage.StatusCode >= 500 && (int)responseMessage.StatusCode < 600)
71+
{
72+
throw new HttpRequestException(string.Format("Response Http Status code {0} indicates server error", responseMessage.StatusCode));
73+
}
74+
}
75+
76+
private void ConfigurePolicy()
77+
{
78+
this.retryPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().WaitAndRetryAsync(settings.RetryCount, i => settings.RetryInterval);
79+
}
80+
}
81+
}

src/SendGrid/SendGrid.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
</PropertyGroup>
2121

2222
<ItemGroup>
23+
<PackageReference Include="Polly" Version="5.2.0" />
2324
<PackageReference Include="StyleCop.Analyzers" Version="1.0.0">
2425
<PrivateAssets>All</PrivateAssets>
2526
</PackageReference>

src/SendGrid/SendGridClient.cs

+44-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
// </copyright>
55

6+
using SendGrid.Helpers.Reliability;
7+
68
namespace SendGrid
79
{
810
using Helpers.Mail;
@@ -22,6 +24,8 @@ namespace SendGrid
2224
/// </summary>
2325
public class SendGridClient : ISendGridClient
2426
{
27+
private readonly SendGridClientOptions options;
28+
2529
/// <summary>
2630
/// Gets or sets the path to the API resource.
2731
/// </summary>
@@ -63,16 +67,48 @@ public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dic
6367
PreAuthenticate = true,
6468
UseDefaultCredentials = false,
6569
};
66-
client = new HttpClient(httpClientHandler);
70+
71+
var retryHandler = new RetryDelegatingHandler(httpClientHandler, options.ReliabilitySettings);
72+
73+
client = new HttpClient(retryHandler);
6774
}
6875
else
6976
{
70-
client = new HttpClient();
77+
client = CreateHttpClientWithRetryHandler();
7178
}
7279

7380
InitiateClient(apiKey, host, requestHeaders, version, urlPath);
7481
}
7582

83+
/// <summary>
84+
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
85+
/// </summary>
86+
/// <param name="options">A <see cref="SendGridClientOptions"/> instance that defines the configuration settings to use with the client </param>
87+
/// <returns>Interface to the SendGrid REST API</returns>
88+
public SendGridClient(SendGridClientOptions options)
89+
: this(null, options)
90+
{
91+
}
92+
93+
/// <summary>
94+
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
95+
/// </summary>
96+
/// <param name="httpClient">An optional http client which may me injected in order to facilitate testing.</param>
97+
/// <param name="options">A <see cref="SendGridClientOptions"/> instance that defines the configuration settings to use with the client </param>
98+
/// <returns>Interface to the SendGrid REST API</returns>
99+
public SendGridClient(HttpClient httpClient, SendGridClientOptions options)
100+
{
101+
if (options == null)
102+
{
103+
throw new ArgumentNullException(nameof(options));
104+
}
105+
106+
this.options = options;
107+
client = (httpClient == null) ? CreateHttpClientWithRetryHandler() : httpClient;
108+
109+
InitiateClient(options.ApiKey, options.Host, options.RequestHeaders, options.Version, options.UrlPath);
110+
}
111+
76112
/// <summary>
77113
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
78114
/// </summary>
@@ -84,10 +120,8 @@ public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dic
84120
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
85121
/// <returns>Interface to the SendGrid REST API</returns>
86122
public SendGridClient(HttpClient httpClient, string apiKey, string host = null, Dictionary<string, string> requestHeaders = null, string version = "v3", string urlPath = null)
123+
: this(httpClient, new SendGridClientOptions() { ApiKey = apiKey, Host = host, RequestHeaders = requestHeaders, Version = version, UrlPath = urlPath })
87124
{
88-
client = (httpClient == null) ? new HttpClient() : httpClient;
89-
90-
InitiateClient(apiKey, host, requestHeaders, version, urlPath);
91125
}
92126

93127
/// <summary>
@@ -159,6 +193,11 @@ private void InitiateClient(string apiKey, string host, Dictionary<string, strin
159193
}
160194
}
161195

196+
private HttpClient CreateHttpClientWithRetryHandler()
197+
{
198+
return new HttpClient(new RetryDelegatingHandler(options.ReliabilitySettings));
199+
}
200+
162201
/// <summary>
163202
/// The supported API methods.
164203
/// </summary>

src/SendGrid/SendGridClientOptions.cs

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using SendGrid.Helpers.Reliability;
2+
3+
namespace SendGrid
4+
{
5+
using System.Collections.Generic;
6+
7+
/// <summary>
8+
/// Defines the options to use with the SendGrid client
9+
/// </summary>
10+
public class SendGridClientOptions
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="SendGridClientOptions"/> class.
14+
/// </summary>
15+
public SendGridClientOptions()
16+
{
17+
ReliabilitySettings = new ReliabilitySettings();
18+
RequestHeaders = new Dictionary<string, string>();
19+
Host = "https://api.sendgrid.com";
20+
Version = "v3";
21+
}
22+
23+
/// <summary>
24+
/// Gets the reliability settings to use on HTTP Requests
25+
/// </summary>
26+
public ReliabilitySettings ReliabilitySettings { get; private set; }
27+
28+
/// <summary>
29+
/// Gets or sets the SendGrid API key
30+
/// </summary>
31+
public string ApiKey { get; set; }
32+
33+
/// <summary>
34+
/// Gets or sets the request headers to use on HttpRequests sent to SendGrid
35+
/// </summary>
36+
public Dictionary<string, string> RequestHeaders { get; set; }
37+
38+
/// <summary>
39+
/// Gets or sets base url (e.g. https://api.sendgrid.com, this is the default)
40+
/// </summary>
41+
public string Host { get; set; }
42+
43+
/// <summary>
44+
/// Gets or sets API version, override AddVersion to customize
45+
/// </summary>
46+
public string Version { get; set; }
47+
48+
/// <summary>
49+
/// Gets or sets the path to the API endpoint.
50+
/// </summary>
51+
public string UrlPath { get; set; }
52+
}
53+
}

0 commit comments

Comments
 (0)