Created Retry and CircuitBreaker policies for MVC App
This commit is contained in:
parent
faf4ada8ac
commit
cb3f682872
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
src/Web/WebMVC/Services/Utilities/HttpClientWrapper.cs
Normal file
99
src/Web/WebMVC/Services/Utilities/HttpClientWrapper.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
@error.ErrorMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<h4 class="esh-orders_new-title">Shipping address</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
@ -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…
x
Reference in New Issue
Block a user