Skip to content

Commit adb243b

Browse files
Merge pull request #509 from dylan-asos/master
Transient Fault Handling
2 parents f4092b7 + a3312a4 commit adb243b

File tree

9 files changed

+785
-15
lines changed

9 files changed

+785
-15
lines changed

USE_CASES.md

Lines changed: 59 additions & 0 deletions
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
@@ -582,3 +583,61 @@ namespace Example
582583
}
583584
}
584585
```
586+
587+
<a name="transient_faults"></a>
588+
# Transient Fault Handling
589+
590+
The SendGridClient provides functionality for handling transient errors that might occur when sending an HttpRequest. This includes client side timeouts while sending the mail, or certain errors returned within the 500 range. Errors within the 500 range are limited to 500 Internal Server Error, 502 Bad Gateway, 503 Service unavailable and 504 Gateway timeout.
591+
592+
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**
593+
594+
### RetryCount
595+
596+
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, retry behaviour is not enabled. The maximum amount of retries permitted is 5.
597+
598+
### MinimumBackOff
599+
600+
The minimum amount of time to wait between retries.
601+
602+
### MaximumBackOff
603+
604+
The maximum possible amount of time to wait between retries. The maximum value allowed is 30 seconds
605+
606+
### DeltaBackOff
607+
608+
The value that will be used to calculate a random delta in the exponential delay between retries. A random element of time is factored into the delta calculation as this helps avoid many clients retrying at regular intervals.
609+
610+
611+
## Examples
612+
613+
In this example we are setting RetryCount to 2, with a mimimum wait time of 1 seconds, a maximum of 10 seconds and a delta of 3 seconds
614+
615+
```csharp
616+
617+
var options = new SendGridClientOptions
618+
{
619+
ApiKey = "Your-Api-Key",
620+
ReliabilitySettings = new ReliabilitySettings(2, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(3))
621+
};
622+
623+
var client = new SendGridClient(options);
624+
625+
```
626+
627+
The SendGridClientOptions object defines all the settings that can be set for the client, e.g.
628+
629+
```csharp
630+
631+
var options = new SendGridClientOptions
632+
{
633+
ApiKey = "Your-Api-Key",
634+
ReliabilitySettings = new ReliabilitySettings(2, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(3)),
635+
Host = "Your-Host",
636+
UrlPath = "Url-Path",
637+
Version = "3",
638+
RequestHeaders = new Dictionary<string, string>() {{"header-key", "header-value"}}
639+
};
640+
641+
var client = new SendGridClient(options);
642+
643+
```
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
/// <summary>
11+
/// Initializes a new instance of the <see cref="ReliabilitySettings"/> class with default settings.
12+
/// </summary>
13+
public ReliabilitySettings()
14+
: this(0, TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero)
15+
{
16+
}
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="ReliabilitySettings"/> class.
20+
/// </summary>
21+
/// <param name="maximumNumberOfRetries">The maximum number of retries to execute against when sending an HTTP Request before throwing an exception</param>
22+
/// <param name="minimumBackoff">The minimum amount of time to wait between between HTTP retries</param>
23+
/// <param name="maximumBackOff">the maximum amount of time to wait between between HTTP retries</param>
24+
/// <param name="deltaBackOff">the value that will be used to calculate a random delta in the exponential delay between retries</param>
25+
public ReliabilitySettings(int maximumNumberOfRetries, TimeSpan minimumBackoff, TimeSpan maximumBackOff, TimeSpan deltaBackOff)
26+
{
27+
if (maximumNumberOfRetries < 0)
28+
{
29+
throw new ArgumentOutOfRangeException(nameof(maximumNumberOfRetries), "maximumNumberOfRetries must be greater than 0");
30+
}
31+
32+
if (maximumNumberOfRetries > 5)
33+
{
34+
throw new ArgumentOutOfRangeException(nameof(maximumNumberOfRetries), "The maximum number of retries allowed is 5");
35+
}
36+
37+
if (minimumBackoff.Ticks < 0)
38+
{
39+
throw new ArgumentOutOfRangeException(nameof(minimumBackoff), "minimumBackoff must be greater than 0");
40+
}
41+
42+
if (maximumBackOff.Ticks < 0)
43+
{
44+
throw new ArgumentOutOfRangeException(nameof(maximumBackOff), "maximumBackOff must be greater than 0");
45+
}
46+
47+
if (maximumBackOff.TotalSeconds > 30)
48+
{
49+
throw new ArgumentOutOfRangeException(nameof(maximumBackOff), "maximumBackOff must be less than 30 seconds");
50+
}
51+
52+
if (deltaBackOff.Ticks < 0)
53+
{
54+
throw new ArgumentOutOfRangeException(nameof(deltaBackOff), "deltaBackOff must be greater than 0");
55+
}
56+
57+
if (minimumBackoff.TotalMilliseconds > maximumBackOff.TotalMilliseconds)
58+
{
59+
throw new ArgumentOutOfRangeException(nameof(minimumBackoff), "minimumBackoff must be less than maximumBackOff");
60+
}
61+
62+
this.MaximumNumberOfRetries = maximumNumberOfRetries;
63+
this.MinimumBackOff = minimumBackoff;
64+
this.DeltaBackOff = deltaBackOff;
65+
this.MaximumBackOff = maximumBackOff;
66+
}
67+
68+
/// <summary>
69+
/// Gets the maximum number of retries to execute against when sending an HTTP Request before throwing an exception. Defaults to 0 (no retries, you must explicitly enable)
70+
/// </summary>
71+
public int MaximumNumberOfRetries { get; }
72+
73+
/// <summary>
74+
/// Gets the minimum amount of time to wait between between HTTP retries. Defaults to 1 second
75+
/// </summary>
76+
public TimeSpan MinimumBackOff { get; }
77+
78+
/// <summary>
79+
/// Gets the maximum amount of time to wait between between HTTP retries. Defaults to 10 seconds
80+
/// </summary>
81+
public TimeSpan MaximumBackOff { get; }
82+
83+
/// <summary>
84+
/// Gets the value that will be used to calculate a random delta in the exponential delay between retries. Defaults to 1 second
85+
/// </summary>
86+
public TimeSpan DeltaBackOff { get; }
87+
}
88+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
namespace SendGrid.Helpers.Reliability
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Threading;
8+
using System.Threading.Tasks;
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 static readonly List<HttpStatusCode> RetriableServerErrorStatusCodes =
16+
new List<HttpStatusCode>()
17+
{
18+
HttpStatusCode.InternalServerError,
19+
HttpStatusCode.BadGateway,
20+
HttpStatusCode.ServiceUnavailable,
21+
HttpStatusCode.GatewayTimeout
22+
};
23+
24+
private readonly ReliabilitySettings settings;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="RetryDelegatingHandler"/> class.
28+
/// </summary>
29+
/// <param name="settings">A ReliabilitySettings instance</param>
30+
public RetryDelegatingHandler(ReliabilitySettings settings)
31+
: this(new HttpClientHandler(), settings)
32+
{
33+
}
34+
35+
/// <summary>
36+
/// Initializes a new instance of the <see cref="RetryDelegatingHandler"/> class.
37+
/// </summary>
38+
/// <param name="innerHandler">A HttpMessageHandler instance to set as the innner handler</param>
39+
/// <param name="settings">A ReliabilitySettings instance</param>
40+
public RetryDelegatingHandler(HttpMessageHandler innerHandler, ReliabilitySettings settings)
41+
: base(innerHandler)
42+
{
43+
this.settings = settings;
44+
}
45+
46+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
47+
{
48+
if (this.settings.MaximumNumberOfRetries == 0)
49+
{
50+
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
51+
}
52+
53+
HttpResponseMessage responseMessage = null;
54+
55+
var numberOfAttempts = 0;
56+
var sent = false;
57+
58+
while (!sent)
59+
{
60+
var waitFor = this.GetNextWaitInterval(numberOfAttempts);
61+
62+
try
63+
{
64+
responseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
65+
66+
ThrowHttpRequestExceptionIfResponseCodeCanBeRetried(responseMessage);
67+
68+
sent = true;
69+
}
70+
catch (TaskCanceledException)
71+
{
72+
numberOfAttempts++;
73+
74+
if (numberOfAttempts > this.settings.MaximumNumberOfRetries)
75+
{
76+
throw new TimeoutException();
77+
}
78+
79+
// ReSharper disable once MethodSupportsCancellation, cancel will be indicated on the token
80+
await Task.Delay(waitFor).ConfigureAwait(false);
81+
}
82+
catch (HttpRequestException)
83+
{
84+
numberOfAttempts++;
85+
86+
if (numberOfAttempts > this.settings.MaximumNumberOfRetries)
87+
{
88+
throw;
89+
}
90+
91+
await Task.Delay(waitFor).ConfigureAwait(false);
92+
}
93+
}
94+
95+
return responseMessage;
96+
}
97+
98+
private static void ThrowHttpRequestExceptionIfResponseCodeCanBeRetried(HttpResponseMessage responseMessage)
99+
{
100+
if (RetriableServerErrorStatusCodes.Contains(responseMessage.StatusCode))
101+
{
102+
throw new HttpRequestException(string.Format("Http status code '{0}' indicates server error", responseMessage.StatusCode));
103+
}
104+
}
105+
106+
private TimeSpan GetNextWaitInterval(int numberOfAttempts)
107+
{
108+
var random = new Random();
109+
110+
var delta = (int)((Math.Pow(2.0, numberOfAttempts) - 1.0) *
111+
random.Next(
112+
(int)(this.settings.DeltaBackOff.TotalMilliseconds * 0.8),
113+
(int)(this.settings.DeltaBackOff.TotalMilliseconds * 1.2)));
114+
115+
var interval = (int)Math.Min(this.settings.MinimumBackOff.TotalMilliseconds + delta, this.settings.MaximumBackOff.TotalMilliseconds);
116+
117+
return TimeSpan.FromMilliseconds(interval);
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)