Skip to content

Commit

Permalink
HttpClient cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
Jérôme Quiles committed Jul 25, 2017
1 parent 0d58b61 commit 90c8595
Show file tree
Hide file tree
Showing 17 changed files with 222 additions and 94 deletions.
6 changes: 3 additions & 3 deletions Skeleton.Tests.Web/HttpClientAsyncCachedEntityReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace Skeleton.Tests.Web
[TestFixture]
public class HttpClientAsyncCachedEntityReaderTests
{
private readonly AsyncCrudHttpClient<CustomerDto> Client =
new AsyncCrudHttpClient<CustomerDto>(AppConfiguration.AsyncCachedCustomersUriBuilder);
private readonly AsyncJsonCrudHttpClient<CustomerDto> Client =
new AsyncJsonCrudHttpClient<CustomerDto>(AppConfiguration.AsyncCachedCustomersUriBuilder);

[Test]
public async Task AsyncCachedEntityReader_GetAllAsync()
Expand Down Expand Up @@ -38,7 +38,7 @@ public async Task AsyncCachedEntityReader_FirstOrDefaultAsync()
[Test]
public void AsyncCachedEntityReader_FirstOrDefaultAsync_With_Wrong_Id()
{
Assert.CatchAsync(typeof(CustomHttpException), async () => await Client.FirstOrDefaultAsync(100000));
Assert.CatchAsync(typeof(HttpResponseMessageException), async () => await Client.FirstOrDefaultAsync(100000));
}

[Test]
Expand Down
6 changes: 3 additions & 3 deletions Skeleton.Tests.Web/HttpClientAsyncEntityReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace Skeleton.Tests.Web
[TestFixture]
public class HttpClientAsyncEntityReaderTests
{
private readonly AsyncCrudHttpClient<CustomerDto> Client =
new AsyncCrudHttpClient<CustomerDto>(AppConfiguration.AsyncCustomersUriBuilder);
private readonly AsyncJsonCrudHttpClient<CustomerDto> Client =
new AsyncJsonCrudHttpClient<CustomerDto>(AppConfiguration.AsyncCustomersUriBuilder);

[Test]
public async Task AsyncEntityReader_GetAllAsync()
Expand Down Expand Up @@ -38,7 +38,7 @@ public async Task AsyncEntityReader_FirstOrDefaultAsync()
[Test]
public void AsyncEntityReader_FirstOrDefault_With_Wrong_Id()
{
Assert.CatchAsync(typeof(CustomHttpException), async () => await Client.FirstOrDefaultAsync(100000));
Assert.CatchAsync(typeof(HttpResponseMessageException), async () => await Client.FirstOrDefaultAsync(100000));
}

[Test]
Expand Down
4 changes: 2 additions & 2 deletions Skeleton.Tests.Web/HttpClientAsyncEntityWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace Skeleton.Tests.Web
[TestFixture]
public class HttpClientAsyncEntityWriterTests
{
private readonly AsyncCrudHttpClient<CustomerDto> Client =
new AsyncCrudHttpClient<CustomerDto>(AppConfiguration.AsyncCustomersUriBuilder);
private readonly AsyncJsonCrudHttpClient<CustomerDto> Client =
new AsyncJsonCrudHttpClient<CustomerDto>(AppConfiguration.AsyncCustomersUriBuilder);

[Test]
public async Task AsyncEntityWriter_UpdateAsync()
Expand Down
6 changes: 3 additions & 3 deletions Skeleton.Tests.Web/HttpClientCachedEntityReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace Skeleton.Tests.Web
[TestFixture]
public class HttpClientCachedEntityReaderTests
{
private readonly CrudHttpClient<CustomerDto> Client =
new CrudHttpClient<CustomerDto>(AppConfiguration.CachedCustomersUriBuilder);
private readonly JsonCrudHttpClient<CustomerDto> Client =
new JsonCrudHttpClient<CustomerDto>(AppConfiguration.CachedCustomersUriBuilder);

[Test]
public void CachedEntityReader_GetAll()
Expand Down Expand Up @@ -38,7 +38,7 @@ public void CachedEntityReader_FirstOrDefault()
[Test]
public void CachedEntityReader_FirstOrDefault_With_Wrong_Id()
{
Assert.Catch(typeof(CustomHttpException), () => Client.FirstOrDefault(1000000));
Assert.Catch(typeof(HttpResponseMessageException), () => Client.FirstOrDefault(1000000));
}

[Test]
Expand Down
6 changes: 3 additions & 3 deletions Skeleton.Tests.Web/HttpClientEntityReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public class HttpClientEntityReaderTests
private const int pageSize = 50;
private const int numberOfPages = 5;

private readonly CrudHttpClient<CustomerDto> Client =
new CrudHttpClient<CustomerDto>(AppConfiguration.CustomersUriBuilder);
private readonly JsonCrudHttpClient<CustomerDto> Client =
new JsonCrudHttpClient<CustomerDto>(AppConfiguration.CustomersUriBuilder);

[Test]
public void EntityReader_GetAll()
Expand Down Expand Up @@ -41,7 +41,7 @@ public void EntityReader_FirstOrDefault()
[Test]
public void EntityReader_FirstOrDefault_With_Wrong_Id()
{
Assert.Catch(typeof(CustomHttpException), () => Client.FirstOrDefault(100000));
Assert.Catch(typeof(HttpResponseMessageException), () => Client.FirstOrDefault(100000));
}

[Test]
Expand Down
10 changes: 5 additions & 5 deletions Skeleton.Tests.Web/HttpClientEntityWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace Skeleton.Tests.Web
[TestFixture]
public class HttpClientEntityWriterTests
{
private readonly CrudHttpClient<CustomerDto> Client =
new CrudHttpClient<CustomerDto>(AppConfiguration.CustomersUriBuilder);
private readonly JsonCrudHttpClient<CustomerDto> Client =
new JsonCrudHttpClient<CustomerDto>(AppConfiguration.CustomersUriBuilder);

[Test]
public void EntityWriter_Update()
Expand Down Expand Up @@ -40,7 +40,7 @@ public void EntityWriter_Update_With_Wrong_Id()
Name = "CustomerUpdated"
};

Assert.Catch(typeof(CustomHttpException), () => Client.Update(customer));
Assert.Catch(typeof(HttpResponseMessageException), () => Client.Update(customer));
}

[Test]
Expand Down Expand Up @@ -73,7 +73,7 @@ public void EntityWriter_Create_With_Wrong_Id()
var customer = MemorySeeder.SeedCustomerDto();
customer.CustomerId = 100000;

Assert.Catch(typeof(CustomHttpException), () => Client.Create(customer));
Assert.Catch(typeof(HttpResponseMessageException), () => Client.Create(customer));
}

[Test]
Expand Down Expand Up @@ -112,7 +112,7 @@ public void EntityWriter_BatchDelete()
[Test]
public void EntityWriter_Delete_With_Wrong_Id()
{
Assert.Catch(typeof(CustomHttpException), () => Client.Delete(100000));
Assert.Catch(typeof(HttpResponseMessageException), () => Client.Delete(100000));
}
}
}
13 changes: 9 additions & 4 deletions Skeleton.Web.Client/AsyncJsonCrudHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@

namespace Skeleton.Web.Client
{
public class AsyncCrudHttpClient<TDto> :
public class AsyncJsonCrudHttpClient<TDto> :
JsonHttpClient where TDto : class
{
public AsyncCrudHttpClient(IRestUriBuilder uriBuilder)
public AsyncJsonCrudHttpClient(IRestUriBuilder uriBuilder)
: base(uriBuilder)
{
}

public AsyncCrudHttpClient(IRestUriBuilder uriBuilder, HttpClientHandler handler)
: base(uriBuilder, handler)
public AsyncJsonCrudHttpClient(IRestUriBuilder uriBuilder, IRetryPolicy retryPolicy)
: base(uriBuilder, retryPolicy)
{
}

public AsyncJsonCrudHttpClient(IRestUriBuilder uriBuilder, IRetryPolicy retryPolicy, HttpClientHandler handler)
: base(uriBuilder, retryPolicy, handler)
{
}

Expand Down
24 changes: 0 additions & 24 deletions Skeleton.Web.Client/CustomHttpException.cs

This file was deleted.

99 changes: 67 additions & 32 deletions Skeleton.Web.Client/ExponentialRetryPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,64 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace Skeleton.Web.Client
{
internal sealed class ExponentialRetryPolicy
public sealed class ExponentialRetryPolicy : IRetryPolicy
{
private static readonly TimeSpan DefaultRetryInterval = TimeSpan.FromSeconds(1.0);
private static readonly TimeSpan DefaultMaxBackoff = TimeSpan.FromSeconds(30.0);
private static readonly TimeSpan DefaultMinBackoff = TimeSpan.FromSeconds(1.0);
private static readonly int DefaultRetryCount = 10;
private static readonly bool FirstFastRetry = true;
private static readonly int[] httpStatusCodesWorthRetrying = { 408, 500, 502, 503, 504 };
private static readonly int DefaultRetryCount = 5;
private static readonly HttpStatusCode[] httpStatusCodesWorthRetrying =
{
HttpStatusCode.RequestTimeout,
HttpStatusCode.InternalServerError,
HttpStatusCode.BadGateway,
HttpStatusCode.ServiceUnavailable,
HttpStatusCode.GatewayTimeout
};

private readonly int maxRetries;
private readonly TimeSpan retryInterval;

public ExponentialRetryPolicy()
:this (DefaultRetryCount, DefaultRetryInterval)
{
}

public ExponentialRetryPolicy(int maxRetries)
: this(maxRetries, DefaultRetryInterval)
{
}

public ExponentialRetryPolicy(int maxRetries, TimeSpan retryInterval)
{
this.maxRetries = maxRetries;
this.retryInterval = retryInterval;
}

public int RetryCount
{
get;
private set;
}

internal T Execute<T>(Func<T> func)
public TimeSpan DelayInterval
{
var retryCount = 0;
get;
private set;
}

public T Execute<T>(Func<T> func)
{
RetryCount = 0;

for (;;)
{
var exponentialInterval = CalculateExponentialBackoff(retryCount);
DelayInterval = CalculateExponentialBackoff();

try
{
Expand All @@ -29,84 +67,81 @@ internal T Execute<T>(Func<T> func)
// Connection error
catch (HttpRequestException)
{
++retryCount;
++RetryCount;

if (retryCount == DefaultRetryCount)
if (RetryCount == maxRetries)
throw;

Task.Delay(exponentialInterval).Wait();
Task.Delay(DelayInterval).Wait();
}
// Enriched exception (statuscode)
catch (CustomHttpException e)
catch (HttpResponseMessageException e)
{
++retryCount;
++RetryCount;

if (retryCount == DefaultRetryCount)
if (RetryCount == maxRetries)
throw;

if (!httpStatusCodesWorthRetrying.Contains(e.StatusCode))
throw;

Task.Delay(exponentialInterval).Wait();
Task.Delay(DelayInterval).Wait();
}
}
}

internal async Task<T> ExecuteAsync<T>(Func<Task<T>> func)
public async Task<T> ExecuteAsync<T>(Func<Task<T>> func)
{
var retryCount = 0;
RetryCount = 0;

for (;;)
{
var exponentialInterval = CalculateExponentialBackoff(retryCount);
DelayInterval = CalculateExponentialBackoff();

try
{
return await func();
}
catch (HttpRequestException)
{
++retryCount;
++RetryCount;

if (retryCount == DefaultRetryCount)
if (RetryCount == DefaultRetryCount)
throw;

Task.Delay(exponentialInterval).Wait();
Task.Delay(DelayInterval).Wait();
}
catch (CustomHttpException e)
catch (HttpResponseMessageException e)
{
++retryCount;
++RetryCount;

if (retryCount == DefaultRetryCount)
if (RetryCount == DefaultRetryCount)
throw;

if (!httpStatusCodesWorthRetrying.Contains(e.StatusCode))
throw;

Task.Delay(exponentialInterval).Wait();
Task.Delay(DelayInterval).Wait();
}
}
}

private static TimeSpan CalculateExponentialBackoff(int retryCount)
private TimeSpan CalculateExponentialBackoff()
{
if (FirstFastRetry && retryCount == 0)
return DefaultRetryInterval;

int randomInterval = GetRandomInterval();
var delta = (int)(Math.Pow(2.0, retryCount) * randomInterval);
var delta = (int)(Math.Pow(2.0, RetryCount) * randomInterval);
var interval = (int)Math.Min(checked(DefaultMinBackoff.TotalMilliseconds + delta),
DefaultMaxBackoff.TotalMilliseconds);

return TimeSpan.FromMilliseconds(interval);
}

private static int GetRandomInterval()
private int GetRandomInterval()
{
var random = new Random();
var randomInterval = random.Next(
(int)(DefaultRetryInterval.TotalMilliseconds * 0.8),
(int)(DefaultRetryInterval.TotalMilliseconds * 1.2));
(int)(retryInterval.TotalMilliseconds * 0.8),
(int)(retryInterval.TotalMilliseconds * 1.2));

return randomInterval;
}
Expand Down
30 changes: 30 additions & 0 deletions Skeleton.Web.Client/HttpResponseMessageException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;

namespace Skeleton.Web.Client
{
[Serializable]
public class HttpResponseMessageException : Exception
{
public HttpStatusCode StatusCode { get; }
public HttpResponseMessage Response { get; }

public HttpResponseMessageException()
{
}

public HttpResponseMessageException(HttpResponseMessage response)
: base(response.ReasonPhrase)
{
Response = response;
StatusCode = response.StatusCode;
}

protected HttpResponseMessageException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}
Loading

0 comments on commit 90c8595

Please sign in to comment.