Skip to content

Commit

Permalink
Implement automatic rate-limit handling
Browse files Browse the repository at this point in the history
  • Loading branch information
frederikprijck committed Aug 17, 2021
1 parent b82e434 commit 57ec39b
Show file tree
Hide file tree
Showing 35 changed files with 287 additions and 268 deletions.
70 changes: 65 additions & 5 deletions src/Auth0.ManagementApi/HttpClientManagementConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Auth0.ManagementApi
Expand All @@ -18,17 +19,19 @@ public class HttpClientManagementConnection : IManagementConnection, IDisposable
static readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, DateParseHandling = DateParseHandling.DateTime };

readonly HttpClient httpClient;
readonly HttpClientManagementConnectionOptions options;
bool ownHttpClient;

/// <summary>
/// Initializes a new instance of the <see cref="HttpClientManagementConnection"/> class.
/// </summary>
/// <param name="httpClient">Optional <see cref="HttpClient"/> to use. If not specified one will
/// be created and be used for all requests made by this instance.</param>
public HttpClientManagementConnection(HttpClient httpClient = null)
public HttpClientManagementConnection(HttpClient httpClient = null, HttpClientManagementConnectionOptions options = null)
{
ownHttpClient = httpClient == null;
this.httpClient = httpClient ?? new HttpClient();
this.options = options ?? new HttpClientManagementConnectionOptions();
}

/// <summary>
Expand All @@ -37,14 +40,25 @@ public HttpClientManagementConnection(HttpClient httpClient = null)
/// <param name="handler"><see cref="HttpMessageHandler"/> to use with the managed
/// <see cref="HttpClient"/> that will be created and used for all requests made
/// by this instance.</param>
public HttpClientManagementConnection(HttpMessageHandler handler)
: this(new HttpClient(handler ?? new HttpClientHandler()))
public HttpClientManagementConnection(HttpMessageHandler handler, HttpClientManagementConnectionOptions options = null)
: this(new HttpClient(handler ?? new HttpClientHandler()), options)
{
ownHttpClient = true;
}

/// <inheritdoc />
public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string> headers, JsonConverter[] converters = null)
{
return await Retry(() => GetAsyncInternal<T>(uri, headers, converters)).ConfigureAwait(false);
}

/// <inheritdoc />
public async Task<T> SendAsync<T>(HttpMethod method, Uri uri, object body, IDictionary<string, string> headers, IList<FileUploadParameter> files = null)
{
return await Retry(() => SendAsyncInternal<T>(method, uri, body, headers, files)).ConfigureAwait(false);
}

private async Task<T> GetAsyncInternal<T>(Uri uri, IDictionary<string, string> headers, JsonConverter[] converters = null)
{
using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
{
Expand All @@ -53,8 +67,7 @@ public async Task<T> GetAsync<T>(Uri uri, IDictionary<string, string> headers, J
}
}

/// <inheritdoc />
public async Task<T> SendAsync<T>(HttpMethod method, Uri uri, object body, IDictionary<string, string> headers, IList<FileUploadParameter> files = null)
private async Task<T> SendAsyncInternal<T>(HttpMethod method, Uri uri, object body, IDictionary<string, string> headers, IList<FileUploadParameter> files = null)
{
using (var request = new HttpRequestMessage(method, uri) { Content = BuildMessageContent(body, files) })
{
Expand Down Expand Up @@ -166,5 +179,52 @@ private static string SerializeFormBodyValue(object value)
if (value is bool boolean) return boolean ? "true" : "false";
return Uri.EscapeDataString(value.ToString());
}

private async Task<TResult> Retry<TResult>(Func<Task<TResult>> retryable)
{
int? configuredNrOfTries = options.NumberOfHttpRetries;
var DEFAULT_NUMBER_RETRIES = 3;
var MAX_NUMBER_RETRIES = 10;
var BASE_DELAY = 100;
var nrOfTries = 0;

var nrOfTriesToAttempt = Math.Min(MAX_NUMBER_RETRIES, configuredNrOfTries ?? DEFAULT_NUMBER_RETRIES);

while (true)
{
try
{
nrOfTries++;

return await retryable();
}
catch (Exception ex)
{
if (!(ex is RateLimitApiException) || nrOfTries >= nrOfTriesToAttempt)
{
throw;
}
}

// Use an exponential back-off with the formula:
// max(MIN_REQUEST_RETRY_DELAY, min(MAX_REQUEST_RETRY_DELAY, (BASE_DELAY * (2 ** attempt - 1)) + random_between(0, MAX_REQUEST_RETRY_JITTER)))
//
// ✔ Each attempt increases base delay by (100ms * (2 ** attempt - 1))
// ✔ Randomizes jitter, adding up to MAX_REQUEST_RETRY_JITTER (250ms)
// ✔ Never less than MIN_REQUEST_RETRY_DELAY (100ms)
// ✔ Never more than MAX_REQUEST_RETRY_DELAY (500ms)
//
var MAX_REQUEST_RETRY_JITTER = 250;
var MAX_REQUEST_RETRY_DELAY = 500;
var MIN_REQUEST_RETRY_DELAY = 100;

var wait = Convert.ToInt32(BASE_DELAY * Math.Pow(2, nrOfTries - 1));
wait = new Random().Next(wait + 1, wait + MAX_REQUEST_RETRY_JITTER);
wait = Math.Min(wait, MAX_REQUEST_RETRY_DELAY);
wait = Math.Max(wait, MIN_REQUEST_RETRY_DELAY);

await Task.Delay(wait);
}
}
}
}
13 changes: 13 additions & 0 deletions src/Auth0.ManagementApi/HttpClientManagementConnectionOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Auth0.ManagementApi
{
public class HttpClientManagementConnectionOptions
{
/// <summary>
/// Set the maximum number of consecutive retries for Management API requests that fail due to rate-limits being reached.
/// By default, rate-limited requests will be retries a maximum of three times.To disable retries on rate-limit
/// errors, set this value to zero.
/// </summary>
/// <remarks>Must be a number between zero (do not retry) and ten.</remarks>
public int? NumberOfHttpRetries { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task InitializeAsync()
{
string token = await GenerateManagementApiToken();

_managementApiClient = new TestManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"));
_managementApiClient = new ManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"));

// We will need a connection to add the users to...
_connection = await _managementApiClient.Connections.CreateAsync(new ConnectionCreateRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<ItemGroup>
<Folder Include="Testing\" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public async Task InitializeAsync()
{
string token = await GenerateManagementApiToken();

_managementApiClient = new TestManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"));
_managementApiClient = new ManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"));

var tenantSettings = await _managementApiClient.TenantSettings.GetAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public async Task InitializeAsync()
{
string token = await GenerateManagementApiToken();

_managementApiClient = new TestManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"));
_managementApiClient = new ManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"));

// We will need a connection to add the users to...
_connection = await _managementApiClient.Connections.CreateAsync(new ConnectionCreateRequest
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task InitializeAsync()
{
string token = await GenerateManagementApiToken();

_managementApiClient = new TestManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"));
_managementApiClient = new ManagementApiClient(token, GetVariable("AUTH0_MANAGEMENT_API_URL"));

var tenantSettings = await _managementApiClient.TenantSettings.GetAsync();

Expand Down
2 changes: 2 additions & 0 deletions tests/Auth0.Core.UnitTests/Auth0.Core.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Moq" Version="4.14.7" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -19,6 +20,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Auth0.Core\Auth0.Core.csproj" />
<ProjectReference Include="..\..\src\Auth0.ManagementApi\Auth0.ManagementApi.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 57ec39b

Please sign in to comment.