Browse Source

Created Retry and CircuitBreaker policies for MVC App

pull/126/head
Ramón Tomás 8 years ago
parent
commit
cb3f682872
8 changed files with 161 additions and 147 deletions
  1. +15
    -8
      src/Web/WebMVC/Controllers/OrderController.cs
  2. +16
    -17
      src/Web/WebMVC/Services/BasketService.cs
  3. +11
    -20
      src/Web/WebMVC/Services/CatalogService.cs
  4. +12
    -12
      src/Web/WebMVC/Services/OrderingService.cs
  5. +99
    -0
      src/Web/WebMVC/Services/Utilities/HttpClientWrapper.cs
  6. +0
    -90
      src/Web/WebMVC/Services/Utilities/RetryWithExponentialBackoff.cs
  7. +7
    -0
      src/Web/WebMVC/Views/Order/Create.cshtml
  8. +1
    -0
      src/Web/WebMVC/WebMVC.csproj

+ 15
- 8
src/Web/WebMVC/Controllers/OrderController.cs View File

@ -7,6 +7,7 @@ using Microsoft.eShopOnContainers.WebMVC.Services;
using Microsoft.eShopOnContainers.WebMVC.ViewModels;
using Microsoft.AspNetCore.Authorization;
using System.Net.Http;
using Polly.CircuitBreaker;
namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
@ -37,18 +38,24 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers
[HttpPost]
public async Task<IActionResult> Create(Order model, string action)
{
if (ModelState.IsValid)
try
{
var user = _appUserParser.Parse(HttpContext.User);
await _orderSvc.CreateOrder(model);
if (ModelState.IsValid)
{
var user = _appUserParser.Parse(HttpContext.User);
await _orderSvc.CreateOrder(model);
//Empty basket for current user.
await _basketSvc.CleanBasket(user);
//Empty basket for current user.
await _basketSvc.CleanBasket(user);
//Redirect to historic list.
return RedirectToAction("Index");
//Redirect to historic list.
return RedirectToAction("Index");
}
}
catch(BrokenCircuitException ex)
{
ModelState.AddModelError("Error", "Not possible to create a new order, please try later on");
}
return View(model);
}


+ 16
- 17
src/Web/WebMVC/Services/BasketService.cs View File

@ -1,21 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.eShopOnContainers.WebMVC.ViewModels;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using System.Net.Http;
using Microsoft.eShopOnContainers.WebMVC.ViewModels;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.eShopOnContainers.WebMVC.Extensions;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using WebMVC.Services.Utilities;
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
public class BasketService : IBasketService
{
private readonly IOptionsSnapshot<AppSettings> _settings;
private HttpClient _apiClient;
private HttpClientWrapper _apiClient;
private readonly string _remoteServiceBaseUrl;
private IHttpContextAccessor _httpContextAccesor;
@ -31,8 +30,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
var context = _httpContextAccesor.HttpContext;
var token = await context.Authentication.GetTokenAsync("access_token");
_apiClient = new HttpClient();
_apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var _apiClient = new HttpClientWrapper();
_apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var basketUrl = $"{_remoteServiceBaseUrl}/{user.Id.ToString()}";
var dataString = await _apiClient.GetStringAsync(basketUrl);
@ -53,12 +52,12 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
var context = _httpContextAccesor.HttpContext;
var token = await context.Authentication.GetTokenAsync("access_token");
_apiClient = new HttpClient();
_apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
_apiClient = new HttpClientWrapper();
_apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var basketUrl = _remoteServiceBaseUrl;
StringContent content = new StringContent(JsonConvert.SerializeObject(basket), System.Text.Encoding.UTF8, "application/json");
var response = await _apiClient.PostAsync(basketUrl, content);
var response = await _apiClient.PostAsync(basketUrl, basket);
return basket;
}
@ -120,8 +119,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
var context = _httpContextAccesor.HttpContext;
var token = await context.Authentication.GetTokenAsync("access_token");
_apiClient = new HttpClient();
_apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
_apiClient = new HttpClientWrapper();
_apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var basketUrl = $"{_remoteServiceBaseUrl}/{user.Id.ToString()}";
var response = await _apiClient.DeleteAsync(basketUrl);


+ 11
- 20
src/Web/WebMVC/Services/CatalogService.cs View File

@ -1,22 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.eShopOnContainers.WebMVC.ViewModels;
using Microsoft.CodeAnalysis.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
using WebMVC.Services.Utilities;
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
public class CatalogService : ICatalogService
{
private readonly IOptionsSnapshot<AppSettings> _settings;
private HttpClient _apiClient;
private HttpClientWrapper _apiClient;
private readonly string _remoteServiceBaseUrl;
public CatalogService(IOptionsSnapshot<AppSettings> settings, ILoggerFactory loggerFactory) {
@ -29,7 +26,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task<Catalog> GetCatalogItems(int page,int take, int? brand, int? type)
{
_apiClient = new HttpClient();
_apiClient = new HttpClientWrapper();
var itemsQs = $"items?pageIndex={page}&pageSize={take}";
var filterQs = "";
@ -45,16 +42,10 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
var dataString = "";
//
// Using HttpClient with Retry and Exponential Backoff
// Using a HttpClient wrapper with Retry and Exponential Backoff
//
var retry = new RetryWithExponentialBackoff();
await retry.RunAsync(async () =>
{
// work with HttpClient call
dataString = await _apiClient.GetStringAsync(catalogUrl);
});
dataString = await _apiClient.GetStringAsync(catalogUrl);
//var dataString = await _apiClient.GetStringAsync(catalogUrl);
var response = JsonConvert.DeserializeObject<Catalog>(dataString);
return response;
@ -62,7 +53,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task<IEnumerable<SelectListItem>> GetBrands()
{
_apiClient = new HttpClient();
_apiClient = new HttpClientWrapper();
var url = $"{_remoteServiceBaseUrl}catalogBrands";
var dataString = await _apiClient.GetStringAsync(url);
@ -81,7 +72,7 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
public async Task<IEnumerable<SelectListItem>> GetTypes()
{
_apiClient = new HttpClient();
_apiClient = new HttpClientWrapper();
var url = $"{_remoteServiceBaseUrl}catalogTypes";
var dataString = await _apiClient.GetStringAsync(url);


+ 12
- 12
src/Web/WebMVC/Services/OrderingService.cs View File

@ -8,12 +8,13 @@ using Microsoft.Extensions.Options;
using System.Net.Http;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authentication;
using WebMVC.Services.Utilities;
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
public class OrderingService : IOrderingService
{
private HttpClient _apiClient;
private HttpClientWrapper _apiClient;
private readonly string _remoteServiceBaseUrl;
private readonly IOptionsSnapshot<AppSettings> _settings;
private readonly IHttpContextAccessor _httpContextAccesor;
@ -30,8 +31,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
var context = _httpContextAccesor.HttpContext;
var token = await context.Authentication.GetTokenAsync("access_token");
_apiClient = new HttpClient();
_apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
_apiClient = new HttpClientWrapper();
_apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var ordersUrl = $"{_remoteServiceBaseUrl}/{Id}";
var dataString = await _apiClient.GetStringAsync(ordersUrl);
@ -46,8 +47,8 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
var context = _httpContextAccesor.HttpContext;
var token = await context.Authentication.GetTokenAsync("access_token");
_apiClient = new HttpClient();
_apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
_apiClient = new HttpClientWrapper();
_apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var ordersUrl = _remoteServiceBaseUrl;
var dataString = await _apiClient.GetStringAsync(ordersUrl);
@ -77,17 +78,16 @@ namespace Microsoft.eShopOnContainers.WebMVC.Services
var context = _httpContextAccesor.HttpContext;
var token = await context.Authentication.GetTokenAsync("access_token");
_apiClient = new HttpClient();
_apiClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
_apiClient.DefaultRequestHeaders.Add("x-requestid", order.RequestId.ToString());
_apiClient = new HttpClientWrapper();
_apiClient.Inst.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
_apiClient.Inst.DefaultRequestHeaders.Add("x-requestid", order.RequestId.ToString());
var ordersUrl = $"{_remoteServiceBaseUrl}/new";
order.CardTypeId = 1;
order.CardExpirationApiFormat();
SetFakeIdToProducts(order);
StringContent content = new StringContent(JsonConvert.SerializeObject(order), System.Text.Encoding.UTF8, "application/json");
var response = await _apiClient.PostAsync(ordersUrl, content);
var response = await _apiClient.PostAsync(ordersUrl, order);
if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
throw new Exception("Error creating order, try later");


+ 99
- 0
src/Web/WebMVC/Services/Utilities/HttpClientWrapper.cs View File

@ -0,0 +1,99 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Polly;
using Polly.Wrap;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace WebMVC.Services.Utilities
{
public class HttpClientWrapper
{
private HttpClient _client;
private PolicyWrap _policyWrapper;
private ILogger _logger;
public HttpClient Inst => _client;
public HttpClientWrapper()
{
_client = new HttpClient();
_logger = new LoggerFactory().CreateLogger(nameof(HttpClientWrapper));
// Add Policies to be applied
_policyWrapper = Policy.WrapAsync(
CreateRetryPolicy(),
CreateCircuitBreakerPolicy()
);
}
private Policy CreateCircuitBreakerPolicy()
{
return Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
// number of exceptions before breaking circuit
3,
// time circuit opened before retry
TimeSpan.FromMinutes(1),
(exception, duration) => {
// on circuit opened
_logger.LogTrace("Circuit breaker opened");
},
() => {
// on circuit closed
_logger.LogTrace("Circuit breaker reset");
}
);
}
private Policy CreateRetryPolicy()
{
return Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(
// number of retries
3,
// exponential backofff
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
// on retry
(exception, timeSpan, retryCount, context) =>
{
_logger.LogTrace($"Retry {retryCount} " +
$"of {context.PolicyKey} " +
$"at {context.ExecutionKey}, " +
$"due to: {exception}.");
});
}
public async Task<string> GetStringAsync(string uri)
{
return await HttpInvoker(async () =>
await _client.GetStringAsync(uri));
}
public async Task<HttpResponseMessage> PostAsync<T>(string uri, T item)
{
// a new StringContent must be created for each retry
// as it is disposed after each call
return await HttpInvoker(async () =>
await _client.PostAsync(uri,
new StringContent(JsonConvert.SerializeObject(item),
System.Text.Encoding.UTF8, "application/json")));
}
public async Task<HttpResponseMessage> DeleteAsync(string uri)
{
return await HttpInvoker(async () =>
await _client.DeleteAsync(uri));
}
private async Task<T> HttpInvoker<T>(Func<Task<T>> action)
{
// Executes the action applying all
// the policies defined in the wrapper
return await _policyWrapper
.ExecuteAsync(async () => await action());
}
}
}

+ 0
- 90
src/Web/WebMVC/Services/Utilities/RetryWithExponentialBackoff.cs View File

@ -1,90 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
/// <summary>
/// When working with cloud services and Docker containers, it's very important to always catch
/// TimeoutException, and retry the operation.
/// RetryWithExponentialBackoff makes it easy to implement such pattern.
/// Usage:
/// var retry = new RetryWithExponentialBackoff();
/// await retry.RunAsync(async ()=>
/// {
/// // work with HttpClient
/// });
/// </summary>
public sealed class RetryWithExponentialBackoff
{
private readonly int maxRetries, delayMilliseconds, maxDelayMilliseconds;
public RetryWithExponentialBackoff(int maxRetries = 50, int delayMilliseconds = 200, int maxDelayMilliseconds = 2000)
{
this.maxRetries = maxRetries;
this.delayMilliseconds = delayMilliseconds;
this.maxDelayMilliseconds = maxDelayMilliseconds;
}
public async Task RunAsync(Func<Task> func)
{
ExponentialBackoff backoff = new ExponentialBackoff(this.maxRetries, this.delayMilliseconds, this.maxDelayMilliseconds);
retry:
try
{
await func();
}
catch (Exception ex) when (ex is TimeoutException || ex is System.Net.Http.HttpRequestException)
{
Debug.WriteLine("Exception raised is: " + ex.GetType().ToString() + " -- Message: " + ex.Message + " -- Inner Message: " + ex.InnerException.Message);
await backoff.Delay();
goto retry;
}
}
}
/// <summary>
/// Usage:
/// ExponentialBackoff backoff = new ExponentialBackoff(3, 10, 100);
/// retry:
/// try {
/// // ...
/// }
/// catch (Exception ex) {
/// await backoff.Delay(cancellationToken);
/// goto retry;
/// }
/// </summary>
public struct ExponentialBackoff
{
private readonly int m_maxRetries, m_delayMilliseconds, m_maxDelayMilliseconds;
private int m_retries, m_pow;
public ExponentialBackoff(int maxRetries, int delayMilliseconds, int maxDelayMilliseconds)
{
m_maxRetries = maxRetries;
m_delayMilliseconds = delayMilliseconds;
m_maxDelayMilliseconds = maxDelayMilliseconds;
m_retries = 0;
m_pow = 1;
}
public Task Delay()
{
if (m_retries == m_maxRetries)
{
throw new TimeoutException("Max retry attempts exceeded.");
}
++m_retries;
if (m_retries < 31)
{
m_pow = m_pow << 1; // m_pow = Pow(2, m_retries - 1)
}
int delay = Math.Min(m_delayMilliseconds * (m_pow - 1) / 2, m_maxDelayMilliseconds);
return Task.Delay(delay);
}
}
}

+ 7
- 0
src/Web/WebMVC/Views/Order/Create.cshtml View File

@ -11,6 +11,13 @@
<div class="container">
<form method="post" asp-controller="Order" asp-action="Create">
<section class="esh-orders_new-section">
<div class="row">
@foreach (var error in ViewData.ModelState.Values.SelectMany(err => err.Errors)) {
<div class="alert alert-warning" role="alert">
&nbsp;@error.ErrorMessage
</div>
}
</div>
<h4 class="esh-orders_new-title">Shipping address</h4>
<div class="row">
<div class="col-md-6">


+ 1
- 0
src/Web/WebMVC/WebMVC.csproj View File

@ -32,6 +32,7 @@
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="Polly" Version="5.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="1.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.0" />


Loading…
Cancel
Save