Skip to content

Transient Fault Handling #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Aug 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions USE_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<a name="attachments"></a>
# Attachments
Expand Down Expand Up @@ -580,3 +581,57 @@ namespace Example
}
}
```


<a name="transient_faults"></a>
# 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<string, string>() {{"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.



2 changes: 2 additions & 0 deletions nuspec/Sendgrid.9.5.2.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
<group targetFramework=".NETFramework4.0">
<dependency id="Newtonsoft.Json" version="9.0.1" />
<dependency id="System.Net.Http" version="4.0.0" />
<dependency id="Polly" version="5.2.0" />
</group>
<group targetFramework=".NETStandard1.3">
<dependency id="NETStandard.Library" version="1.6.1" />
<dependency id="Newtonsoft.Json" version="9.0.1" />
<dependency id="Microsoft.AspNetCore.Http.Abstractions" version="1.1.0" />
<dependency id="Polly" version="5.2.0" />
</group>
</dependencies>
</metadata>
Expand Down
70 changes: 70 additions & 0 deletions src/SendGrid/Helpers/Reliability/ReliabilitySettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
namespace SendGrid.Helpers.Reliability
{
using System;

/// <summary>
/// Defines the reliability settings to use on HTTP requests
/// </summary>
public class ReliabilitySettings
{
private int retryCount;

private TimeSpan retryInterval;

/// <summary>
/// Initializes a new instance of the <see cref="ReliabilitySettings"/> class.
/// </summary>
public ReliabilitySettings()
{
this.retryCount = 0;
this.retryInterval = TimeSpan.FromSeconds(1);
}

/// <summary>
/// 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)
/// </summary>
public int RetryCount
{
get
{
return this.retryCount;
}

set
{
if (value < 0)
{
throw new ArgumentException("Retry count must be greater than zero");
}

if (value > 5)
{
throw new ArgumentException("The maximum number of retries is 5");
}

this.retryCount = value;
}
}

/// <summary>
/// Gets or sets the interval between HTTP retries. Defaults to 1 second
/// </summary>
public TimeSpan RetryInterval
{
get
{
return this.retryInterval;
}

set
{
if (value.TotalSeconds > 30)
{
throw new ArgumentException("The maximum retry interval is 30 seconds");
}

this.retryInterval = value;
}
}
}
}
81 changes: 81 additions & 0 deletions src/SendGrid/Helpers/Reliability/RetryDelegatingHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
namespace SendGrid.Helpers.Reliability
{
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Polly;
using Polly.Retry;

/// <summary>
/// A delegating handler that provides retry functionality while executing a request
/// </summary>
public class RetryDelegatingHandler : DelegatingHandler
{
private readonly ReliabilitySettings settings;

private RetryPolicy retryPolicy;

/// <summary>
/// Initializes a new instance of the <see cref="RetryDelegatingHandler"/> class.
/// </summary>
/// <param name="settings">A ReliabilitySettings instance</param>
public RetryDelegatingHandler(ReliabilitySettings settings)
: this(new HttpClientHandler(), settings)
{
}

public RetryDelegatingHandler(HttpMessageHandler innerHandler, ReliabilitySettings settings)
: base(innerHandler)
{
this.settings = settings;
this.ConfigurePolicy();
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (this.settings.RetryCount == 0)
{
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}

HttpResponseMessage responseMessage;

var result = await this.retryPolicy.ExecuteAndCaptureAsync(
async () =>
{
try
{
responseMessage = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
ThrowHttpRequestExceptionIfResponseIsWithinTheServerErrorRange(responseMessage);
}
catch (TaskCanceledException)
{
throw new TimeoutException();
}

return responseMessage;
});

if (result.Outcome == OutcomeType.Successful)
{
return result.Result;
}

throw result.FinalException;
}

private static void ThrowHttpRequestExceptionIfResponseIsWithinTheServerErrorRange(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()
{
this.retryPolicy = Policy.Handle<HttpRequestException>().Or<TimeoutException>().WaitAndRetryAsync(settings.RetryCount, i => settings.RetryInterval);
}
}
}
1 change: 1 addition & 0 deletions src/SendGrid/SendGrid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Polly" Version="5.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.0">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
Expand Down
49 changes: 44 additions & 5 deletions src/SendGrid/SendGridClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
// </copyright>

using SendGrid.Helpers.Reliability;

namespace SendGrid
{
using Helpers.Mail;
Expand All @@ -22,6 +24,8 @@ namespace SendGrid
/// </summary>
public class SendGridClient : ISendGridClient
{
private readonly SendGridClientOptions options;

/// <summary>
/// Gets or sets the path to the API resource.
/// </summary>
Expand Down Expand Up @@ -63,16 +67,48 @@ 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 = CreateHttpClientWithRetryHandler();
}

InitiateClient(apiKey, host, requestHeaders, version, urlPath);
}

/// <summary>
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
/// </summary>
/// <param name="options">A <see cref="SendGridClientOptions"/> instance that defines the configuration settings to use with the client </param>
/// <returns>Interface to the SendGrid REST API</returns>
public SendGridClient(SendGridClientOptions options)
: this(null, options)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
/// </summary>
/// <param name="httpClient">An optional http client which may me injected in order to facilitate testing.</param>
/// <param name="options">A <see cref="SendGridClientOptions"/> instance that defines the configuration settings to use with the client </param>
/// <returns>Interface to the SendGrid REST API</returns>
public SendGridClient(HttpClient httpClient, SendGridClientOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

this.options = options;
client = (httpClient == null) ? CreateHttpClientWithRetryHandler() : httpClient;

InitiateClient(options.ApiKey, options.Host, options.RequestHeaders, options.Version, options.UrlPath);
}

/// <summary>
/// Initializes a new instance of the <see cref="SendGridClient"/> class.
/// </summary>
Expand All @@ -84,10 +120,8 @@ public SendGridClient(IWebProxy webProxy, string apiKey, string host = null, Dic
/// <param name="urlPath">Path to endpoint (e.g. /path/to/endpoint)</param>
/// <returns>Interface to the SendGrid REST API</returns>
public SendGridClient(HttpClient httpClient, string apiKey, string host = null, Dictionary<string, string> 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);
}

/// <summary>
Expand Down Expand Up @@ -159,6 +193,11 @@ private void InitiateClient(string apiKey, string host, Dictionary<string, strin
}
}

private HttpClient CreateHttpClientWithRetryHandler()
{
return new HttpClient(new RetryDelegatingHandler(options.ReliabilitySettings));
}

/// <summary>
/// The supported API methods.
/// </summary>
Expand Down
53 changes: 53 additions & 0 deletions src/SendGrid/SendGridClientOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using SendGrid.Helpers.Reliability;

namespace SendGrid
{
using System.Collections.Generic;

/// <summary>
/// Defines the options to use with the SendGrid client
/// </summary>
public class SendGridClientOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="SendGridClientOptions"/> class.
/// </summary>
public SendGridClientOptions()
{
ReliabilitySettings = new ReliabilitySettings();
RequestHeaders = new Dictionary<string, string>();
Host = "https://api.sendgrid.com";
Version = "v3";
}

/// <summary>
/// Gets the reliability settings to use on HTTP Requests
/// </summary>
public ReliabilitySettings ReliabilitySettings { get; private set; }

/// <summary>
/// Gets or sets the SendGrid API key
/// </summary>
public string ApiKey { get; set; }

/// <summary>
/// Gets or sets the request headers to use on HttpRequests sent to SendGrid
/// </summary>
public Dictionary<string, string> RequestHeaders { get; set; }

/// <summary>
/// Gets or sets base url (e.g. https://api.sendgrid.com, this is the default)
/// </summary>
public string Host { get; set; }

/// <summary>
/// Gets or sets API version, override AddVersion to customize
/// </summary>
public string Version { get; set; }

/// <summary>
/// Gets or sets the path to the API endpoint.
/// </summary>
public string UrlPath { get; set; }
}
}
Loading