diff --git a/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs new file mode 100644 index 00000000..8d953d5c --- /dev/null +++ b/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs @@ -0,0 +1,34 @@ +namespace Ocelot.Configuration.Builder +{ + public class QoSOptionsBuilder + { + private int _exceptionsAllowedBeforeBreaking; + + private int _durationOfBreak; + + private int _timeoutValue; + + public QoSOptionsBuilder WithExceptionsAllowedBeforeBreaking(int exceptionsAllowedBeforeBreaking) + { + _exceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; + return this; + } + + public QoSOptionsBuilder WithDurationOfBreak(int durationOfBreak) + { + _durationOfBreak = durationOfBreak; + return this; + } + + public QoSOptionsBuilder WithTimeoutValue(int timeoutValue) + { + _timeoutValue = timeoutValue; + return this; + } + + public QoSOptions Build() + { + return new QoSOptions(_exceptionsAllowedBeforeBreaking, _durationOfBreak, _timeoutValue); + } + } +} diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index 25888681..380e193f 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -10,9 +9,9 @@ using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Requester.QoS; using Ocelot.Responses; using Ocelot.Utilities; -using Ocelot.Values; namespace Ocelot.Configuration.Creator { @@ -26,11 +25,14 @@ namespace Ocelot.Configuration.Creator private const string RegExMatchEverything = ".*"; private const string RegExMatchEndString = "$"; private const string RegExIgnoreCase = "(?i)"; + private const string RegExForwardSlashOnly = "^/$"; private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; private readonly ILogger _logger; private readonly ILoadBalancerFactory _loadBalanceFactory; private readonly ILoadBalancerHouse _loadBalancerHouse; + private readonly IQoSProviderFactory _qoSProviderFactory; + private readonly IQosProviderHouse _qosProviderHouse; public FileOcelotConfigurationCreator( IOptions options, @@ -38,10 +40,14 @@ namespace Ocelot.Configuration.Creator IClaimToThingConfigurationParser claimToThingConfigurationParser, ILogger logger, ILoadBalancerFactory loadBalancerFactory, - ILoadBalancerHouse loadBalancerHouse) + ILoadBalancerHouse loadBalancerHouse, + IQoSProviderFactory qoSProviderFactory, + IQosProviderHouse qosProviderHouse) { _loadBalanceFactory = loadBalancerFactory; _loadBalancerHouse = loadBalancerHouse; + _qoSProviderFactory = qoSProviderFactory; + _qosProviderHouse = qosProviderHouse; _options = options; _configurationValidator = configurationValidator; _claimToThingConfigurationParser = claimToThingConfigurationParser; @@ -86,17 +92,17 @@ namespace Ocelot.Configuration.Creator { var isAuthenticated = IsAuthenticated(fileReRoute); - var isAuthorised = IsAuthenticated(fileReRoute); + var isAuthorised = IsAuthorised(fileReRoute); var isCached = IsCached(fileReRoute); var requestIdKey = BuildRequestId(fileReRoute, globalConfiguration); - var loadBalancerKey = BuildLoadBalancerKey(fileReRoute); + var reRouteKey = BuildReRouteKey(fileReRoute); - var upstreamTemplatePattern = BuildUpstreamTemplate(fileReRoute); + var upstreamTemplatePattern = BuildUpstreamTemplatePattern(fileReRoute); - var isQos = fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions.TimeoutValue >0; + var isQos = IsQoS(fileReRoute); var serviceProviderConfiguration = BuildServiceProviderConfiguration(fileReRoute, globalConfiguration); @@ -108,6 +114,8 @@ namespace Ocelot.Configuration.Creator var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest); + var qosOptions = BuildQoSOptions(fileReRoute); + var reRoute = new ReRouteBuilder() .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) @@ -127,16 +135,31 @@ namespace Ocelot.Configuration.Creator .WithLoadBalancer(fileReRoute.LoadBalancer) .WithDownstreamHost(fileReRoute.DownstreamHost) .WithDownstreamPort(fileReRoute.DownstreamPort) - .WithLoadBalancerKey(loadBalancerKey) + .WithLoadBalancerKey(reRouteKey) .WithServiceProviderConfiguraion(serviceProviderConfiguration) .WithIsQos(isQos) - .WithQosOptions(new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue)) + .WithQosOptions(qosOptions) .Build(); await SetupLoadBalancer(reRoute); + SetupQosProvider(reRoute); return reRoute; } + private QoSOptions BuildQoSOptions(FileReRoute fileReRoute) + { + return new QoSOptionsBuilder() + .WithExceptionsAllowedBeforeBreaking(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking) + .WithDurationOfBreak(fileReRoute.QoSOptions.DurationOfBreak) + .WithTimeoutValue(fileReRoute.QoSOptions.TimeoutValue) + .Build(); + } + + private bool IsQoS(FileReRoute fileReRoute) + { + return fileReRoute.QoSOptions?.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions?.TimeoutValue > 0; + } + private bool IsAuthenticated(FileReRoute fileReRoute) { return !string.IsNullOrEmpty(fileReRoute.AuthenticationOptions?.Provider); @@ -163,7 +186,7 @@ namespace Ocelot.Configuration.Creator return requestIdKey; } - private string BuildLoadBalancerKey(FileReRoute fileReRoute) + private string BuildReRouteKey(FileReRoute fileReRoute) { //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain var loadBalancerKey = $"{fileReRoute.UpstreamPathTemplate}{fileReRoute.UpstreamHttpMethod}"; @@ -185,7 +208,13 @@ namespace Ocelot.Configuration.Creator private async Task SetupLoadBalancer(ReRoute reRoute) { var loadBalancer = await _loadBalanceFactory.Get(reRoute); - _loadBalancerHouse.Add(reRoute.LoadBalancerKey, loadBalancer); + _loadBalancerHouse.Add(reRoute.ReRouteKey, loadBalancer); + } + + private void SetupQosProvider(ReRoute reRoute) + { + var loadBalancer = _qoSProviderFactory.Get(reRoute); + _qosProviderHouse.Add(reRoute.ReRouteKey, loadBalancer); } private ServiceProviderConfiguraion BuildServiceProviderConfiguration(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) @@ -206,7 +235,7 @@ namespace Ocelot.Configuration.Creator .Build(); } - private string BuildUpstreamTemplate(FileReRoute reRoute) + private string BuildUpstreamTemplatePattern(FileReRoute reRoute) { var upstreamTemplate = reRoute.UpstreamPathTemplate; @@ -230,6 +259,11 @@ namespace Ocelot.Configuration.Creator upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); } + if (upstreamTemplate == "/") + { + return RegExForwardSlashOnly; + } + var route = reRoute.ReRouteIsCaseSensitive ? $"{upstreamTemplate}{RegExMatchEndString}" : $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 9deaaeda..29145888 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -1,8 +1,5 @@ -using Polly.Timeout; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System; +using Polly.Timeout; namespace Ocelot.Configuration { diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 45b31d38..ab9405d0 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -25,12 +25,12 @@ namespace Ocelot.Configuration string loadBalancer, string downstreamHost, int downstreamPort, - string loadBalancerKey, + string reRouteKey, ServiceProviderConfiguraion serviceProviderConfiguraion, bool isQos, QoSOptions qos) { - LoadBalancerKey = loadBalancerKey; + ReRouteKey = reRouteKey; ServiceProviderConfiguraion = serviceProviderConfiguraion; LoadBalancer = loadBalancer; DownstreamHost = downstreamHost; @@ -57,7 +57,7 @@ namespace Ocelot.Configuration QosOptions = qos; } - public string LoadBalancerKey {get;private set;} + public string ReRouteKey {get;private set;} public PathTemplate DownstreamPathTemplate { get; private set; } public PathTemplate UpstreamPathTemplate { get; private set; } public string UpstreamTemplatePattern { get; private set; } diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 0a6bd42c..e9be9841 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -28,6 +28,7 @@ using Ocelot.Logging; using Ocelot.QueryStrings; using Ocelot.Request.Builder; using Ocelot.Requester; +using Ocelot.Requester.QoS; using Ocelot.Responder; using Ocelot.ServiceDiscovery; @@ -61,6 +62,8 @@ namespace Ocelot.DependencyInjection { services.AddMvcCore().AddJsonFormatters(); services.AddLogging(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index e504a430..8b6447c7 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -30,6 +30,13 @@ namespace Ocelot.DownstreamRouteFinder.Finder foreach (var reRoute in applicableReRoutes) { + if (upstreamUrlPath == reRoute.UpstreamTemplatePattern) + { + var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamPathTemplate.Value); + + return new OkResponse(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute)); + } + var urlMatch = _urlMatcher.Match(upstreamUrlPath, reRoute.UpstreamTemplatePattern); if (urlMatch.Data.Match) diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index e2fe4ada..373ac441 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -26,6 +26,7 @@ ServicesAreEmptyError, UnableToFindServiceDiscoveryProviderError, UnableToFindLoadBalancerError, - RequestTimedOutError + RequestTimedOutError, + UnableToFindQoSProviderError } } diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs index ce37f828..8e26cbf2 100644 --- a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs @@ -6,7 +6,6 @@ using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.QueryStrings.Middleware; -using Ocelot.ServiceDiscovery; namespace Ocelot.LoadBalancer.Middleware { @@ -31,7 +30,7 @@ namespace Ocelot.LoadBalancer.Middleware { _logger.LogDebug("started calling load balancing middleware"); - var loadBalancer = _loadBalancerHouse.Get(DownstreamRoute.ReRoute.LoadBalancerKey); + var loadBalancer = _loadBalancerHouse.Get(DownstreamRoute.ReRoute.ReRouteKey); if(loadBalancer.IsError) { SetPipelineError(loadBalancer.Errors); diff --git a/src/Ocelot/Request/Builder/HttpRequestCreator.cs b/src/Ocelot/Request/Builder/HttpRequestCreator.cs index f326933c..3264db59 100644 --- a/src/Ocelot/Request/Builder/HttpRequestCreator.cs +++ b/src/Ocelot/Request/Builder/HttpRequestCreator.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Ocelot.Responses; using Ocelot.Configuration; +using Ocelot.Requester.QoS; namespace Ocelot.Request.Builder { @@ -18,7 +19,7 @@ namespace Ocelot.Request.Builder string contentType, RequestId.RequestId requestId, bool isQos, - QoSOptions qos) + IQoSProvider qosProvider) { var request = await new RequestBuilder() .WithHttpMethod(httpMethod) @@ -30,7 +31,7 @@ namespace Ocelot.Request.Builder .WithRequestId(requestId) .WithCookies(cookies) .WithIsQos(isQos) - .WithQos(qos) + .WithQos(qosProvider) .Build(); return new OkResponse(request); diff --git a/src/Ocelot/Request/Builder/IRequestCreator.cs b/src/Ocelot/Request/Builder/IRequestCreator.cs index 3417d933..7db999b1 100644 --- a/src/Ocelot/Request/Builder/IRequestCreator.cs +++ b/src/Ocelot/Request/Builder/IRequestCreator.cs @@ -1,8 +1,8 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Ocelot.Requester.QoS; using Ocelot.Responses; -using Ocelot.Configuration; namespace Ocelot.Request.Builder { @@ -17,6 +17,6 @@ namespace Ocelot.Request.Builder string contentType, RequestId.RequestId requestId, bool isQos, - QoSOptions qos); + IQoSProvider qosProvider); } } diff --git a/src/Ocelot/Request/Builder/RequestBuilder.cs b/src/Ocelot/Request/Builder/RequestBuilder.cs index e0201bc8..ea6511ce 100644 --- a/src/Ocelot/Request/Builder/RequestBuilder.cs +++ b/src/Ocelot/Request/Builder/RequestBuilder.cs @@ -8,7 +8,7 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -using Ocelot.Configuration; +using Ocelot.Requester.QoS; namespace Ocelot.Request.Builder { @@ -24,7 +24,7 @@ namespace Ocelot.Request.Builder private IRequestCookieCollection _cookies; private readonly string[] _unsupportedHeaders = {"host"}; private bool _isQos; - private QoSOptions _qos; + private IQoSProvider _qoSProvider; public RequestBuilder WithHttpMethod(string httpMethod) { @@ -80,9 +80,9 @@ namespace Ocelot.Request.Builder return this; } - public RequestBuilder WithQos(QoSOptions qos) + public RequestBuilder WithQos(IQoSProvider qoSProvider) { - _qos = qos; + _qoSProvider = qoSProvider; return this; } @@ -105,7 +105,7 @@ namespace Ocelot.Request.Builder var cookieContainer = CreateCookieContainer(uri); - return new Request(httpRequestMessage, cookieContainer,_isQos,_qos); + return new Request(httpRequestMessage, cookieContainer,_isQos, _qoSProvider); } private Uri CreateUri() diff --git a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs index aafa2f3d..991e84da 100644 --- a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs +++ b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs @@ -1,10 +1,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Builder; +using Ocelot.Requester.QoS; namespace Ocelot.Request.Middleware { @@ -13,15 +13,18 @@ namespace Ocelot.Request.Middleware private readonly RequestDelegate _next; private readonly IRequestCreator _requestCreator; private readonly IOcelotLogger _logger; + private readonly IQosProviderHouse _qosProviderHouse; public HttpRequestBuilderMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository, - IRequestCreator requestCreator) + IRequestCreator requestCreator, + IQosProviderHouse qosProviderHouse) :base(requestScopedDataRepository) { _next = next; _requestCreator = requestCreator; + _qosProviderHouse = qosProviderHouse; _logger = loggerFactory.CreateLogger(); } @@ -29,17 +32,35 @@ namespace Ocelot.Request.Middleware { _logger.LogDebug("started calling request builder middleware"); + var qosProvider = _qosProviderHouse.Get(DownstreamRoute.ReRoute.ReRouteKey); + + if (qosProvider.IsError) + { + _logger.LogDebug("IQosProviderHouse returned an error, setting pipeline error"); + + SetPipelineError(qosProvider.Errors); + + return; + } + var buildResult = await _requestCreator - .Build(context.Request.Method, DownstreamUrl, context.Request.Body, - context.Request.Headers, context.Request.Cookies, context.Request.QueryString, - context.Request.ContentType, new RequestId.RequestId(DownstreamRoute?.ReRoute?.RequestIdKey, context.TraceIdentifier), - DownstreamRoute.ReRoute.IsQos,DownstreamRoute.ReRoute.QosOptions); + .Build(context.Request.Method, + DownstreamUrl, + context.Request.Body, + context.Request.Headers, + context.Request.Cookies, + context.Request.QueryString, + context.Request.ContentType, + new RequestId.RequestId(DownstreamRoute?.ReRoute?.RequestIdKey, context.TraceIdentifier), + DownstreamRoute.ReRoute.IsQos, + qosProvider.Data); if (buildResult.IsError) { _logger.LogDebug("IRequestCreator returned an error, setting pipeline error"); SetPipelineError(buildResult.Errors); + return; } _logger.LogDebug("setting upstream request"); diff --git a/src/Ocelot/Request/Request.cs b/src/Ocelot/Request/Request.cs index a1e62834..680ab757 100644 --- a/src/Ocelot/Request/Request.cs +++ b/src/Ocelot/Request/Request.cs @@ -2,22 +2,27 @@ using Ocelot.Values; using System.Net; using System.Net.Http; +using Ocelot.Requester.QoS; namespace Ocelot.Request { public class Request { - public Request(HttpRequestMessage httpRequestMessage, CookieContainer cookieContainer,bool isQos, QoSOptions qos) + public Request( + HttpRequestMessage httpRequestMessage, + CookieContainer cookieContainer, + bool isQos, + IQoSProvider qosProvider) { HttpRequestMessage = httpRequestMessage; CookieContainer = cookieContainer; IsQos = isQos; - Qos = qos; + QosProvider = qosProvider; } public HttpRequestMessage HttpRequestMessage { get; private set; } public CookieContainer CookieContainer { get; private set; } public bool IsQos { get; private set; } - public QoSOptions Qos { get; private set; } + public IQoSProvider QosProvider { get; private set; } } } diff --git a/src/Ocelot/Requester/CircuitBreakingDelegatingHandler.cs b/src/Ocelot/Requester/CircuitBreakingDelegatingHandler.cs deleted file mode 100644 index b4091296..00000000 --- a/src/Ocelot/Requester/CircuitBreakingDelegatingHandler.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Ocelot.Logging; -using Polly; -using Polly.CircuitBreaker; -using Polly.Timeout; -using System; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Ocelot.Requester -{ - public class CircuitBreakingDelegatingHandler : DelegatingHandler - { - private readonly IOcelotLogger _logger; - private readonly int _exceptionsAllowedBeforeBreaking; - private readonly TimeSpan _durationOfBreak; - private readonly Policy _circuitBreakerPolicy; - private readonly TimeoutPolicy _timeoutPolicy; - - public CircuitBreakingDelegatingHandler( - int exceptionsAllowedBeforeBreaking, - TimeSpan durationOfBreak, - TimeSpan timeoutValue, - TimeoutStrategy timeoutStrategy, - IOcelotLogger logger, - HttpMessageHandler innerHandler) - : base(innerHandler) - { - this._exceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; - - this._durationOfBreak = durationOfBreak; - - _circuitBreakerPolicy = Policy - .Handle() - .Or() - .Or() - .CircuitBreakerAsync( - exceptionsAllowedBeforeBreaking: exceptionsAllowedBeforeBreaking, - durationOfBreak: durationOfBreak, - onBreak: (ex, breakDelay) => - { - _logger.LogError(".Breaker logging: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms!", ex); - }, - onReset: () => _logger.LogDebug(".Breaker logging: Call ok! Closed the circuit again."), - onHalfOpen: () => _logger.LogDebug(".Breaker logging: Half-open; next call is a trial.") - ); - - _timeoutPolicy = Policy.TimeoutAsync(timeoutValue, timeoutStrategy); - - _logger = logger; - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - try - { - return await Policy.WrapAsync(_circuitBreakerPolicy, _timeoutPolicy).ExecuteAsync(() => base.SendAsync(request,cancellationToken)); - } - catch (BrokenCircuitException ex) - { - _logger.LogError($"Reached to allowed number of exceptions. Circuit is open. AllowedExceptionCount: {_exceptionsAllowedBeforeBreaking}, DurationOfBreak: {_durationOfBreak}",ex); - throw; - } - catch (HttpRequestException ex) - { - _logger.LogError($"Error in CircuitBreakingDelegatingHandler.SendAync", ex); - throw; - } - } - - private static bool IsTransientFailure(HttpResponseMessage result) - { - return result.StatusCode >= HttpStatusCode.InternalServerError; - } - } -} diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index c3c98ac0..da37ee02 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -1,35 +1,39 @@ -using Ocelot.Configuration; -using Ocelot.Logging; -using Ocelot.Values; -using Polly.Timeout; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Requester.QoS; namespace Ocelot.Requester { internal class HttpClientBuilder { - private readonly Dictionary> handlers = new Dictionary>(); + private readonly Dictionary> _handlers = new Dictionary>(); - public HttpClientBuilder WithCircuitBreaker(QoSOptions qos, IOcelotLogger logger, HttpMessageHandler innerHandler) + public HttpClientBuilder WithQoS(IQoSProvider qoSProvider, IOcelotLogger logger, HttpMessageHandler innerHandler) { - handlers.Add(5000, () => new CircuitBreakingDelegatingHandler(qos.ExceptionsAllowedBeforeBreaking, qos.DurationOfBreak, qos.TimeoutValue, qos.TimeoutStrategy, logger, innerHandler)); + _handlers.Add(5000, () => new PollyCircuitBreakingDelegatingHandler(qoSProvider, logger, innerHandler)); return this; } internal HttpClient Build(HttpMessageHandler innerHandler) { - return handlers.Any() ? new HttpClient(CreateHttpMessageHandler()) : new HttpClient(innerHandler); + return _handlers.Any() ? + new HttpClient(CreateHttpMessageHandler()) : + new HttpClient(innerHandler); } private HttpMessageHandler CreateHttpMessageHandler() { HttpMessageHandler httpMessageHandler = new HttpClientHandler(); - handlers.OrderByDescending(handler => handler.Key).Select(handler => handler.Value).Reverse().ToList().ForEach(handler => + _handlers + .OrderByDescending(handler => handler.Key) + .Select(handler => handler.Value) + .Reverse() + .ToList() + .ForEach(handler => { var delegatingHandler = handler(); delegatingHandler.InnerHandler = httpMessageHandler; diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index cc5332f3..5a2c86c7 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -1,17 +1,17 @@ using System; -using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Ocelot.Errors; -using Ocelot.Responses; using Ocelot.Logging; +using Ocelot.Responses; +using Polly.CircuitBreaker; +using Polly.Timeout; namespace Ocelot.Requester { public class HttpClientHttpRequester : IHttpRequester { private readonly IOcelotLogger _logger; - + public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); @@ -19,13 +19,13 @@ namespace Ocelot.Requester public async Task> GetResponse(Request.Request request) { - HttpClientBuilder builder = new HttpClientBuilder(); + var builder = new HttpClientBuilder(); using (var handler = new HttpClientHandler { CookieContainer = request.CookieContainer }) { if (request.IsQos) { - builder.WithCircuitBreaker(request.Qos, _logger, handler); + builder.WithQoS(request.QosProvider, _logger, handler); } using (var httpClient = builder.Build(handler)) @@ -35,10 +35,15 @@ namespace Ocelot.Requester var response = await httpClient.SendAsync(request.HttpRequestMessage); return new OkResponse(response); } - catch (Polly.Timeout.TimeoutRejectedException exception) + catch (TimeoutRejectedException exception) { return - new ErrorResponse(new RequestTimedOutError(exception)); + new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (BrokenCircuitException exception) + { + return + new ErrorResponse(new RequestTimedOutError(exception)); } catch (Exception exception) { diff --git a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs new file mode 100644 index 00000000..a50ec91b --- /dev/null +++ b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs @@ -0,0 +1,47 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Requester.QoS; +using Polly; +using Polly.CircuitBreaker; +using Polly.Timeout; + +namespace Ocelot.Requester +{ + public class PollyCircuitBreakingDelegatingHandler : DelegatingHandler + { + private readonly IQoSProvider _qoSProvider; + private readonly IOcelotLogger _logger; + + public PollyCircuitBreakingDelegatingHandler( + IQoSProvider qoSProvider, + IOcelotLogger logger, + HttpMessageHandler innerHandler) + : base(innerHandler) + { + _qoSProvider = qoSProvider; + _logger = logger; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + return await Policy + .WrapAsync(_qoSProvider.CircuitBreaker.CircuitBreakerPolicy, _qoSProvider.CircuitBreaker.TimeoutPolicy) + .ExecuteAsync(() => base.SendAsync(request,cancellationToken)); + } + catch (BrokenCircuitException ex) + { + _logger.LogError($"Reached to allowed number of exceptions. Circuit is open",ex); + throw; + } + catch (HttpRequestException ex) + { + _logger.LogError($"Error in CircuitBreakingDelegatingHandler.SendAync", ex); + throw; + } + } + } +} diff --git a/src/Ocelot/Requester/QoS/IQoSProviderFactory.cs b/src/Ocelot/Requester/QoS/IQoSProviderFactory.cs new file mode 100644 index 00000000..d1f63ea1 --- /dev/null +++ b/src/Ocelot/Requester/QoS/IQoSProviderFactory.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Configuration; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Responses; +using Polly; +using Polly.CircuitBreaker; +using Polly.Timeout; + +namespace Ocelot.Requester.QoS +{ + public interface IQoSProviderFactory + { + IQoSProvider Get(ReRoute reRoute); + } + + public class QoSProviderFactory : IQoSProviderFactory + { + private readonly IOcelotLoggerFactory _loggerFactory; + + public QoSProviderFactory(IOcelotLoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + } + + public IQoSProvider Get(ReRoute reRoute) + { + if (reRoute.IsQos) + { + return new PollyQoSProvider(reRoute, _loggerFactory); + } + + return new NoQoSProvider(); + } + } + + public interface IQoSProvider + { + CircuitBreaker CircuitBreaker { get; } + } + + public class NoQoSProvider : IQoSProvider + { + public CircuitBreaker CircuitBreaker { get; } + } + + public class PollyQoSProvider : IQoSProvider + { + private readonly CircuitBreakerPolicy _circuitBreakerPolicy; + private readonly TimeoutPolicy _timeoutPolicy; + private readonly IOcelotLogger _logger; + private readonly CircuitBreaker _circuitBreaker; + + public PollyQoSProvider(ReRoute reRoute, IOcelotLoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + + _timeoutPolicy = Policy.TimeoutAsync(reRoute.QosOptions.TimeoutValue, reRoute.QosOptions.TimeoutStrategy); + + _circuitBreakerPolicy = Policy + .Handle() + .Or() + .Or() + .CircuitBreakerAsync( + exceptionsAllowedBeforeBreaking: reRoute.QosOptions.ExceptionsAllowedBeforeBreaking, + durationOfBreak: reRoute.QosOptions.DurationOfBreak, + onBreak: (ex, breakDelay) => + { + _logger.LogError( + ".Breaker logging: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms!", ex); + }, + onReset: () => + { + _logger.LogDebug(".Breaker logging: Call ok! Closed the circuit again."); + }, + onHalfOpen: () => + { + _logger.LogDebug(".Breaker logging: Half-open; next call is a trial."); + } + ); + + _circuitBreaker = new CircuitBreaker(_circuitBreakerPolicy, _timeoutPolicy); + } + + public CircuitBreaker CircuitBreaker => _circuitBreaker; + } + + public class CircuitBreaker + { + public CircuitBreaker(CircuitBreakerPolicy circuitBreakerPolicy, TimeoutPolicy timeoutPolicy) + { + CircuitBreakerPolicy = circuitBreakerPolicy; + TimeoutPolicy = timeoutPolicy; + } + + public CircuitBreakerPolicy CircuitBreakerPolicy { get; private set; } + public TimeoutPolicy TimeoutPolicy { get; private set; } + } + + + public interface IQosProviderHouse + { + Response Get(string key); + Response Add(string key, IQoSProvider loadBalancer); + } + + public class QosProviderHouse : IQosProviderHouse + { + private readonly Dictionary _qoSProviders; + + public QosProviderHouse() + { + _qoSProviders = new Dictionary(); + } + + public Response Get(string key) + { + IQoSProvider qoSProvider; + + if (_qoSProviders.TryGetValue(key, out qoSProvider)) + { + return new OkResponse(_qoSProviders[key]); + } + + return new ErrorResponse(new List() + { + new UnableToFindQoSProviderError($"unabe to find qos provider for {key}") + }); + } + + public Response Add(string key, IQoSProvider loadBalancer) + { + if (!_qoSProviders.ContainsKey(key)) + { + _qoSProviders.Add(key, loadBalancer); + } + + _qoSProviders.Remove(key); + _qoSProviders.Add(key, loadBalancer); + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs b/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs new file mode 100644 index 00000000..46ec65f3 --- /dev/null +++ b/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Errors; + +namespace Ocelot.Requester.QoS +{ + public class UnableToFindQoSProviderError : Error + { + public UnableToFindQoSProviderError(string message) + : base(message, OcelotErrorCode.UnableToFindQoSProviderError) + { + } + } +} diff --git a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs index b9fcb299..45aeafd1 100644 --- a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs +++ b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs @@ -24,7 +24,7 @@ namespace Ocelot.Responder if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError)) { - return new OkResponse(408); + return new OkResponse(503); } return new OkResponse(404); diff --git a/test/Ocelot.AcceptanceTests/QoSTests.cs b/test/Ocelot.AcceptanceTests/QoSTests.cs index 50c1fb66..0eb3d8a4 100644 --- a/test/Ocelot.AcceptanceTests/QoSTests.cs +++ b/test/Ocelot.AcceptanceTests/QoSTests.cs @@ -15,16 +15,16 @@ namespace Ocelot.AcceptanceTests { public class QoSTests : IDisposable { - private IWebHost _builder; + private IWebHost _brokenService; private readonly Steps _steps; private int _requestCount; + private IWebHost _workingService; public QoSTests() { _steps = new Steps(); } - [Fact] public void should_open_circuit_breaker_then_close() { @@ -50,33 +50,90 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "Hello from Laura")) + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51879", "Hello from Laura")) .Given(x => _steps.GivenThereIsAConfiguration(configuration)) .Given(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.RequestTimeout)) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.RequestTimeout)) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.RequestTimeout)) - .Given(x => x.GivenIWaitMilliSeconds(2000)) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => x.GivenIWaitMilliseconds(3000)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } - private void GivenIWaitMilliSeconds(int ms) + [Fact] + public void open_circuit_should_not_effect_different_reRoute() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = "Get", + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 1, + TimeoutValue = 500, + DurationOfBreak = 1000 + } + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51880, + UpstreamPathTemplate = "working", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51879", "Hello from Laura")) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => x.GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenIWaitMilliseconds(int ms) { Thread.Sleep(ms); } - private void GivenThereIsAServiceRunningOn(string url, string responseBody) + private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) { - _builder = new WebHostBuilder() + _brokenService = new WebHostBuilder() .UseUrls(url) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) @@ -95,7 +152,7 @@ namespace Ocelot.AcceptanceTests return; } - //request one times out and polly throws exception + //request one times out and polly throws exception, circuit opens if (_requestCount == 1) { _requestCount++; @@ -104,17 +161,8 @@ namespace Ocelot.AcceptanceTests return; } - //request two times out and polly throws exception circuit opens - if (_requestCount == 2) - { - _requestCount++; - await Task.Delay(1000); - context.Response.StatusCode = 200; - return; - } - //after break closes we return 200 OK - if (_requestCount == 3) + if (_requestCount == 2) { context.Response.StatusCode = 200; await context.Response.WriteAsync(responseBody); @@ -124,12 +172,34 @@ namespace Ocelot.AcceptanceTests }) .Build(); - _builder.Start(); + _brokenService.Start(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _workingService = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _workingService.Start(); } public void Dispose() { - _builder?.Dispose(); + _workingService?.Dispose(); + _brokenService?.Dispose(); _steps.Dispose(); } } diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 5080c9e0..fa1bbda3 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -9,6 +9,7 @@ using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Requester.QoS; using Ocelot.Responses; using Shouldly; using TestStack.BDDfy; @@ -28,9 +29,15 @@ namespace Ocelot.UnitTests.Configuration private readonly Mock _loadBalancerFactory; private readonly Mock _loadBalancerHouse; private readonly Mock _loadBalancer; + private readonly Mock _qosProviderFactory; + private readonly Mock _qosProviderHouse; + private readonly Mock _qosProvider; public FileConfigurationCreatorTests() { + _qosProviderFactory = new Mock(); + _qosProviderHouse = new Mock(); + _qosProvider = new Mock(); _logger = new Mock>(); _configParser = new Mock(); _validator = new Mock(); @@ -40,7 +47,8 @@ namespace Ocelot.UnitTests.Configuration _loadBalancer = new Mock(); _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( _fileConfig.Object, _validator.Object, _configParser.Object, _logger.Object, - _loadBalancerFactory.Object, _loadBalancerHouse.Object); + _loadBalancerFactory.Object, _loadBalancerHouse.Object, + _qosProviderFactory.Object, _qosProviderHouse.Object); } [Fact] @@ -64,10 +72,39 @@ namespace Ocelot.UnitTests.Configuration .When(x => x.WhenICreateTheConfig()) .Then(x => x.TheLoadBalancerFactoryIsCalledCorrectly()) .And(x => x.ThenTheLoadBalancerHouseIsCalledCorrectly()) - .BDDfy(); } + [Fact] + public void should_create_qos_provider() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHost = "127.0.0.1", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + DurationOfBreak = 1, + ExceptionsAllowedBeforeBreaking = 1 + } + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheQosProviderFactoryReturns()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.TheQosProviderFactoryIsCalledCorrectly()) + .And(x => x.ThenTheQosProviderHouseIsCalledCorrectly()) + .BDDfy(); + } + [Fact] public void should_use_downstream_host() { @@ -568,7 +605,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamPathTemplate("/api/products/") .WithUpstreamPathTemplate("/") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("/$") + .WithUpstreamTemplatePattern("^/$") .Build() })) .BDDfy(); @@ -643,5 +680,24 @@ namespace Ocelot.UnitTests.Configuration _loadBalancerHouse .Verify(x => x.Add(It.IsAny(), _loadBalancer.Object), Times.Once); } + + private void GivenTheQosProviderFactoryReturns() + { + _qosProviderFactory + .Setup(x => x.Get(It.IsAny())) + .Returns(_qosProvider.Object); + } + + private void TheQosProviderFactoryIsCalledCorrectly() + { + _qosProviderFactory + .Verify(x => x.Get(It.IsAny()), Times.Once); + } + + private void ThenTheQosProviderHouseIsCalledCorrectly() + { + _qosProviderHouse + .Verify(x => x.Add(It.IsAny(), _qosProvider.Object), Times.Once); + } } } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 81fc7022..2fb8f28f 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -35,6 +35,37 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder [Fact] public void should_return_route() + { + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher")) + .And(x =>x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("someUpstreamPath") + .Build() + })) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod("Get") + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_route_if_upstream_path_and_upstream_template_are_the_same() { this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) .And( @@ -61,7 +92,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .WithUpstreamHttpMethod("Get") .Build() ))) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .And(x => x.ThenTheUrlMatcherIsNotCalled()) .BDDfy(); } @@ -105,7 +136,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder [Fact] public void should_not_return_route() { - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("somePath")) + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("dontMatchPath")) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() @@ -148,6 +179,12 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .Verify(x => x.Match(_upstreamUrlPath, _reRoutesConfig[0].UpstreamPathTemplate.Value), Times.Once); } + private void ThenTheUrlMatcherIsNotCalled() + { + _mockMatcher + .Verify(x => x.Match(_upstreamUrlPath, _reRoutesConfig[0].UpstreamPathTemplate.Value), Times.Never); + } + private void GivenTheUrlMatcherReturns(Response match) { _match = match; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index ea069593..70dd747d 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -18,6 +18,26 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher _urlMatcher = new RegExUrlMatcher(); } + [Fact] + public void should_not_match_forward_slash_only_regex() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/working/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + [Fact] + public void should_match_forward_slash_only_regex() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + [Fact] public void should_find_match_when_template_smaller_than_valid_path() { diff --git a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs index da5d2031..b674b015 100644 --- a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs @@ -20,6 +20,7 @@ using Ocelot.Responses; using TestStack.BDDfy; using Xunit; using Ocelot.Configuration; +using Ocelot.Requester.QoS; namespace Ocelot.UnitTests.Request { @@ -27,6 +28,7 @@ namespace Ocelot.UnitTests.Request { private readonly Mock _requestBuilder; private readonly Mock _scopedRepository; + private readonly Mock _qosProviderHouse; private readonly string _url; private readonly TestServer _server; private readonly HttpClient _client; @@ -38,6 +40,7 @@ namespace Ocelot.UnitTests.Request public HttpRequestBuilderMiddlewareTests() { _url = "http://localhost:51879"; + _qosProviderHouse = new Mock(); _requestBuilder = new Mock(); _scopedRepository = new Mock(); var builder = new WebHostBuilder() @@ -45,6 +48,7 @@ namespace Ocelot.UnitTests.Request { x.AddSingleton(); x.AddLogging(); + x.AddSingleton(_qosProviderHouse.Object); x.AddSingleton(_requestBuilder.Object); x.AddSingleton(_scopedRepository.Object); }) @@ -72,15 +76,22 @@ namespace Ocelot.UnitTests.Request .WithUpstreamHttpMethod("Get") .Build()); - this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheQosProviderHouseReturns(new OkResponse(new NoQoSProvider()))) .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), new CookieContainer(), true, new QoSOptions(3, 8 ,5000, Polly.Timeout.TimeoutStrategy.Pessimistic)))) + .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), new CookieContainer(), true, new NoQoSProvider()))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) .BDDfy(); } + private void GivenTheQosProviderHouseReturns(Response qosProvider) + { + _qosProviderHouse + .Setup(x => x.Get(It.IsAny())) + .Returns(qosProvider); + } + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) { _downstreamRoute = new OkResponse(downstreamRoute); @@ -94,7 +105,7 @@ namespace Ocelot.UnitTests.Request _request = new OkResponse(request); _requestBuilder .Setup(x => x.Build(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny())) .ReturnsAsync(_request); } diff --git a/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs b/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs index c317c8d7..9d1c2ed5 100644 --- a/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs +++ b/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs @@ -11,6 +11,7 @@ using Shouldly; using TestStack.BDDfy; using Xunit; using Ocelot.Configuration; +using Ocelot.Requester.QoS; namespace Ocelot.UnitTests.Request { @@ -27,7 +28,7 @@ namespace Ocelot.UnitTests.Request private Response _result; private Ocelot.RequestId.RequestId _requestId; private bool _isQos; - private QoSOptions _qos; + private IQoSProvider _qoSProvider; public RequestBuilderTests() { @@ -40,7 +41,7 @@ namespace Ocelot.UnitTests.Request { this.Given(x => x.GivenIHaveHttpMethod("GET")) .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) - .And(x=> x.GivenTheQos(true,new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x=> x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectDownstreamUrlIsUsed("http://www.bbc.co.uk/")) .BDDfy(); @@ -51,7 +52,7 @@ namespace Ocelot.UnitTests.Request { this.Given(x => x.GivenIHaveHttpMethod("POST")) .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) - .And(x => x.GivenTheQos(true,new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectHttpMethodIsUsed(HttpMethod.Post)) @@ -65,7 +66,7 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) .And(x => x.GivenTheContentTypeIs("application/json")) - .And(x => x.GivenTheQos(true, new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectContentIsUsed(new StringContent("Hi from Tom"))) @@ -79,7 +80,7 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) .And(x => x.GivenTheContentTypeIs("application/json")) - .And(x => x.GivenTheQos(true, new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectContentHeadersAreUsed(new HeaderDictionary @@ -98,7 +99,7 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) .And(x => x.GivenTheContentTypeIs("application/json; charset=utf-8")) - .And(x => x.GivenTheQos(true, new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectContentHeadersAreUsed(new HeaderDictionary @@ -119,7 +120,7 @@ namespace Ocelot.UnitTests.Request { {"ChopSticks", "Bubbles" } })) - .And(x => x.GivenTheQos(true, new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary @@ -138,7 +139,7 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenTheHttpHeadersAre(new HeaderDictionary())) .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId("RequestId", requestId))) - .And(x => x.GivenTheQos(true, new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary { @@ -157,7 +158,7 @@ namespace Ocelot.UnitTests.Request {"RequestId", "534534gv54gv45g" } })) .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId("RequestId", Guid.NewGuid().ToString()))) - .And(x => x.GivenTheQos(true, new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary { @@ -177,7 +178,7 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenTheHttpHeadersAre(new HeaderDictionary())) .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId(requestIdKey, requestIdValue))) - .And(x => x.GivenTheQos(true, new QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic))) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheRequestIdIsNotInTheHeaders()) .BDDfy(); @@ -188,10 +189,10 @@ namespace Ocelot.UnitTests.Request _requestId = requestId; } - private void GivenTheQos(bool isQos, QoSOptions qos) + private void GivenTheQos(bool isQos, IQoSProvider qoSProvider) { _isQos = isQos; - _qos = qos; + _qoSProvider = qoSProvider; } [Fact] @@ -304,7 +305,7 @@ namespace Ocelot.UnitTests.Request private void WhenICreateARequest() { _result = _requestCreator.Build(_httpMethod, _downstreamUrl, _content?.ReadAsStreamAsync().Result, _headers, - _cookies, _query, _contentType, _requestId,_isQos,_qos).Result; + _cookies, _query, _contentType, _requestId,_isQos,_qoSProvider).Result; } diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index d2d62923..37815a64 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -13,6 +13,7 @@ using Ocelot.Logging; using Ocelot.QueryStrings.Middleware; using Ocelot.Requester; using Ocelot.Requester.Middleware; +using Ocelot.Requester.QoS; using Ocelot.Responder; using Ocelot.Responses; using TestStack.BDDfy; @@ -61,7 +62,7 @@ namespace Ocelot.UnitTests.Requester [Fact] public void should_call_scoped_data_repository_correctly() { - this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),new CookieContainer(),true, new Ocelot.Configuration.QoSOptions(3, 8, 5000, Polly.Timeout.TimeoutStrategy.Pessimistic)))) + this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),new CookieContainer(),true, new NoQoSProvider()))) .And(x => x.GivenTheRequesterReturns(new HttpResponseMessage())) .And(x => x.GivenTheScopedRepoReturns()) .When(x => x.WhenICallTheMiddleware()) diff --git a/test/Ocelot.UnitTests/Requester/QoSProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/QoSProviderFactoryTests.cs new file mode 100644 index 00000000..f4beb0f1 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/QoSProviderFactoryTests.cs @@ -0,0 +1,80 @@ +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Requester.QoS; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class QoSProviderFactoryTests + { + private readonly IQoSProviderFactory _factory; + private ReRoute _reRoute; + private IQoSProvider _result; + private Mock _loggerFactory; + private Mock _logger; + + public QoSProviderFactoryTests() + { + _logger = new Mock(); + _loggerFactory = new Mock(); + _loggerFactory + .Setup(x => x.CreateLogger()) + .Returns(_logger.Object); + _factory = new QoSProviderFactory(_loggerFactory.Object); + } + + [Fact] + public void should_return_no_qos_provider() + { + var reRoute = new ReRouteBuilder() + .WithUpstreamHttpMethod("get") + .WithIsQos(false) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .When(x => x.WhenIGetTheQoSProvider()) + .Then(x => x.ThenTheQoSProviderIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_polly_qos_provider() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithDurationOfBreak(100) + .WithExceptionsAllowedBeforeBreaking(100) + .Build(); + + var reRoute = new ReRouteBuilder() + .WithUpstreamHttpMethod("get") + .WithIsQos(true) + .WithQosOptions(qosOptions) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .When(x => x.WhenIGetTheQoSProvider()) + .Then(x => x.ThenTheQoSProviderIsReturned()) + .BDDfy(); + } + + private void GivenAReRoute(ReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenIGetTheQoSProvider() + { + _result = _factory.Get(_reRoute); + } + + private void ThenTheQoSProviderIsReturned() + { + _result.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs new file mode 100644 index 00000000..1e76a027 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs @@ -0,0 +1,117 @@ +using Ocelot.Requester.QoS; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class QosProviderHouseTests + { + private IQoSProvider _qoSProvider; + private readonly QosProviderHouse _qosProviderHouse; + private Response _addResult; + private Response _getResult; + private string _key; + + public QosProviderHouseTests() + { + _qosProviderHouse = new QosProviderHouse(); + } + + [Fact] + public void should_store_qos_provider() + { + var key = "test"; + + this.Given(x => x.GivenThereIsAQoSProvider(key, new FakeQoSProvider())) + .When(x => x.WhenIAddTheQoSProvider()) + .Then(x => x.ThenItIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_get_qos_provider() + { + var key = "test"; + + this.Given(x => x.GivenThereIsAQoSProvider(key, new FakeQoSProvider())) + .When(x => x.WhenWeGetTheQoSProvider(key)) + .Then(x => x.ThenItIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_store_qos_providers_by_key() + { + var key = "test"; + var keyTwo = "testTwo"; + + this.Given(x => x.GivenThereIsAQoSProvider(key, new FakeQoSProvider())) + .And(x => x.GivenThereIsAQoSProvider(keyTwo, new FakePollyQoSProvider())) + .When(x => x.WhenWeGetTheQoSProvider(key)) + .Then(x => x.ThenTheQoSProviderIs()) + .When(x => x.WhenWeGetTheQoSProvider(keyTwo)) + .Then(x => x.ThenTheQoSProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_no_qos_provider_with_key() + { + this.When(x => x.WhenWeGetTheQoSProvider("test")) + .Then(x => x.ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void ThenAnErrorIsReturned() + { + _getResult.IsError.ShouldBeTrue(); + _getResult.Errors[0].ShouldBeOfType(); + } + + private void ThenTheQoSProviderIs() + { + _getResult.Data.ShouldBeOfType(); + } + + private void ThenItIsAdded() + { + _addResult.IsError.ShouldBe(false); + _addResult.ShouldBeOfType(); + } + + private void WhenIAddTheQoSProvider() + { + _addResult = _qosProviderHouse.Add(_key, _qoSProvider); + } + + + private void GivenThereIsAQoSProvider(string key, IQoSProvider qoSProvider) + { + _key = key; + _qoSProvider = qoSProvider; + WhenIAddTheQoSProvider(); + } + + private void WhenWeGetTheQoSProvider(string key) + { + _getResult = _qosProviderHouse.Get(key); + } + + private void ThenItIsReturned() + { + _getResult.Data.ShouldBe(_qoSProvider); + } + + class FakeQoSProvider : IQoSProvider + { + public CircuitBreaker CircuitBreaker { get; } + } + + class FakePollyQoSProvider : IQoSProvider + { + public CircuitBreaker CircuitBreaker { get; } + } + } +} diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index d2b7e386..d2ac91e0 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -30,7 +30,7 @@ namespace Ocelot.UnitTests.Responder new RequestTimedOutError(new Exception()) })) .When(x => x.WhenIGetErrorStatusCode()) - .Then(x => x.ThenTheResponseIsStatusCodeIs(408)) + .Then(x => x.ThenTheResponseIsStatusCodeIs(503)) .BDDfy(); }