Merge pull request #39 from TomPallister/feature/qos-documentation

Feature/qos documentation
This commit is contained in:
Tom Pallister 2017-02-11 19:01:52 +00:00 committed by GitHub
commit dc8106c904
34 changed files with 1014 additions and 180 deletions

View File

@ -305,19 +305,25 @@ Below is an example configuration that will transforms claims to query string pa
This shows a transform where Ocelot looks at the users LocationId claim and add its as
a query string parameter to be forwarded onto the downstream service.
## Logging
## Quality of Service
Ocelot uses the standard logging interfaces ILoggerFactory / ILogger<T> at the moment.
This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation
for the standard asp.net core logging stuff at the moment.
Ocelot supports one QoS capability at the current time. You can set on a per ReRoute basis if you
want to use a circuit breaker when making requests to a downstream service. This uses the an awesome
.NET library called Polly check them out [here](https://github.com/App-vNext/Polly).
There are a bunch of debugging logs in the ocelot middlewares however I think the
system probably needs more logging in the code it calls into. Other than the debugging
there is a global error handler that should catch any errors thrown and log them as errors.
Add the following section to a ReRoute configuration.
The reason for not just using bog standard framework logging is that I could not
work out how to override the request id that get's logged when setting IncludeScopes
to true for logging settings. Nicely onto the next feature.
"QoSOptions": {
"ExceptionsAllowedBeforeBreaking":3,
"DurationOfBreak":5,
"TimeoutValue":5000
}
You must set a number greater than 0 against ExceptionsAllowedBeforeBreaking for this rule to be
implemented. Duration of break is how long the circuit breaker will stay open for after it is tripped.
TimeoutValue means ff a request takes more than 5 seconds it will automatically be timed out.
If you do not add a QoS section QoS will not be used.
## RequestId / CorrelationId
@ -404,6 +410,20 @@ http request before it is passed to Ocelots request creator.
Obviously you can just add middleware as normal before the call to app.UseOcelot() It cannot be added
after as Ocelot does not call the next middleware.
## Logging
Ocelot uses the standard logging interfaces ILoggerFactory / ILogger<T> at the moment.
This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation
for the standard asp.net core logging stuff at the moment.
There are a bunch of debugging logs in the ocelot middlewares however I think the
system probably needs more logging in the code it calls into. Other than the debugging
there is a global error handler that should catch any errors thrown and log them as errors.
The reason for not just using bog standard framework logging is that I could not
work out how to override the request id that get's logged when setting IncludeScopes
to true for logging settings. Nicely onto the next feature.
## Not supported
Ocelot does not support...

View File

@ -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);
}
}
}

View File

@ -22,7 +22,6 @@ namespace Ocelot.Configuration.Builder
private string _requestIdHeaderKey;
private bool _isCached;
private CacheOptions _fileCacheOptions;
private string _serviceName;
private string _downstreamScheme;
private string _downstreamHost;
private int _downstreamPort;
@ -49,12 +48,6 @@ namespace Ocelot.Configuration.Builder
return this;
}
public ReRouteBuilder WithServiceName(string serviceName)
{
_serviceName = serviceName;
return this;
}
public ReRouteBuilder WithDownstreamPathTemplate(string input)
{
_downstreamPathTemplate = input;

View File

@ -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<FileOcelotConfigurationCreator> _logger;
private readonly ILoadBalancerFactory _loadBalanceFactory;
private readonly ILoadBalancerHouse _loadBalancerHouse;
private readonly IQoSProviderFactory _qoSProviderFactory;
private readonly IQosProviderHouse _qosProviderHouse;
public FileOcelotConfigurationCreator(
IOptions<FileConfiguration> options,
@ -38,10 +40,14 @@ namespace Ocelot.Configuration.Creator
IClaimToThingConfigurationParser claimToThingConfigurationParser,
ILogger<FileOcelotConfigurationCreator> 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,16 +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 isQos = fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions.TimeoutValue >0;
var upstreamTemplatePattern = BuildUpstreamTemplatePattern(fileReRoute);
var isQos = IsQoS(fileReRoute);
var serviceProviderConfiguration = BuildServiceProviderConfiguration(fileReRoute, globalConfiguration);
@ -103,11 +110,12 @@ namespace Ocelot.Configuration.Creator
var claimsToHeaders = BuildAddThingsToRequest(fileReRoute.AddHeadersToRequest);
var claimsToClaims = BuildAddThingsToRequest(fileReRoute.AddClaimsToRequest);
var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest);
var qosOptions = BuildQoSOptions(fileReRoute);
var reRoute = new ReRouteBuilder()
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
.WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate)
@ -127,14 +135,31 @@ namespace Ocelot.Configuration.Creator
.WithLoadBalancer(fileReRoute.LoadBalancer)
.WithDownstreamHost(fileReRoute.DownstreamHost)
.WithDownstreamPort(fileReRoute.DownstreamPort)
.WithLoadBalancerKey(loadBalancerKey)
.WithLoadBalancerKey(reRouteKey)
.WithServiceProviderConfiguraion(serviceProviderConfiguration)
.WithIsQos(isQos)
.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);
@ -161,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}";
@ -183,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)
@ -204,7 +235,7 @@ namespace Ocelot.Configuration.Creator
.Build();
}
private string BuildUpstreamTemplate(FileReRoute reRoute)
private string BuildUpstreamTemplatePattern(FileReRoute reRoute)
{
var upstreamTemplate = reRoute.UpstreamPathTemplate;
@ -228,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}";

View File

@ -1,14 +1,15 @@
using Polly.Timeout;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
using Polly.Timeout;
namespace Ocelot.Configuration
{
public class QoSOptions
{
public QoSOptions(int exceptionsAllowedBeforeBreaking, int durationofBreak, int timeoutValue, TimeoutStrategy timeoutStrategy = TimeoutStrategy.Pessimistic)
public QoSOptions(
int exceptionsAllowedBeforeBreaking,
int durationofBreak,
int timeoutValue,
TimeoutStrategy timeoutStrategy = TimeoutStrategy.Pessimistic)
{
ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking;
DurationOfBreak = TimeSpan.FromMilliseconds(durationofBreak);

View File

@ -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; }

View File

@ -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<IQosProviderHouse, QosProviderHouse>();
services.AddSingleton<IQoSProviderFactory, QoSProviderFactory>();
services.AddSingleton<IServiceDiscoveryProviderFactory, ServiceDiscoveryProviderFactory>();
services.AddSingleton<ILoadBalancerFactory, LoadBalancerFactory>();
services.AddSingleton<ILoadBalancerHouse, LoadBalancerHouse>();

View File

@ -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<DownstreamRoute>(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute));
}
var urlMatch = _urlMatcher.Match(upstreamUrlPath, reRoute.UpstreamTemplatePattern);
if (urlMatch.Data.Match)

View File

@ -25,6 +25,8 @@
ServicesAreNullError,
ServicesAreEmptyError,
UnableToFindServiceDiscoveryProviderError,
UnableToFindLoadBalancerError
UnableToFindLoadBalancerError,
RequestTimedOutError,
UnableToFindQoSProviderError
}
}

View File

@ -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);

View File

@ -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>(request);

View File

@ -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);
}
}

View File

@ -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()

View File

@ -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<HttpRequestBuilderMiddleware>();
}
@ -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");

View File

@ -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; }
}
}

View File

@ -1,74 +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<HttpRequestException>()
.Or<TimeoutRejectedException>()
.Or<TimeoutException>()
.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 Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Task<HttpResponseMessage> responseTask = null;
try
{
responseTask = Policy.WrapAsync(_circuitBreakerPolicy, _timeoutPolicy).ExecuteAsync<HttpResponseMessage>(() =>
{
return base.SendAsync(request,cancellationToken);
});
return responseTask;
}
catch (BrokenCircuitException ex)
{
_logger.LogError($"Reached to allowed number of exceptions. Circuit is open. AllowedExceptionCount: {_exceptionsAllowedBeforeBreaking}, DurationOfBreak: {_durationOfBreak}",ex);
throw;
}
catch (HttpRequestException)
{
return responseTask;
}
}
private static bool IsTransientFailure(HttpResponseMessage result)
{
return result.StatusCode >= HttpStatusCode.InternalServerError;
}
}
}

View File

@ -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<int, Func<DelegatingHandler>> handlers = new Dictionary<int, Func<DelegatingHandler>>();
private readonly Dictionary<int, Func<DelegatingHandler>> _handlers = new Dictionary<int, Func<DelegatingHandler>>();
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;

View File

@ -1,10 +1,10 @@
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
{
@ -19,14 +19,15 @@ namespace Ocelot.Requester
public async Task<Response<HttpResponseMessage>> 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))
{
try
@ -34,13 +35,19 @@ namespace Ocelot.Requester
var response = await httpClient.SendAsync(request.HttpRequestMessage);
return new OkResponse<HttpResponseMessage>(response);
}
catch (Exception exception)
catch (TimeoutRejectedException exception)
{
return
new ErrorResponse<HttpResponseMessage>(new List<Error>
new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));
}
catch (BrokenCircuitException exception)
{
new UnableToCompleteRequestError(exception)
});
return
new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));
}
catch (Exception exception)
{
return new ErrorResponse<HttpResponseMessage>(new UnableToCompleteRequestError(exception));
}
}
}

View File

@ -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<HttpResponseMessage> 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;
}
}
}
}

View File

@ -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<PollyQoSProvider>();
_timeoutPolicy = Policy.TimeoutAsync(reRoute.QosOptions.TimeoutValue, reRoute.QosOptions.TimeoutStrategy);
_circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.Or<TimeoutException>()
.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<IQoSProvider> Get(string key);
Response Add(string key, IQoSProvider loadBalancer);
}
public class QosProviderHouse : IQosProviderHouse
{
private readonly Dictionary<string, IQoSProvider> _qoSProviders;
public QosProviderHouse()
{
_qoSProviders = new Dictionary<string, IQoSProvider>();
}
public Response<IQoSProvider> Get(string key)
{
IQoSProvider qoSProvider;
if (_qoSProviders.TryGetValue(key, out qoSProvider))
{
return new OkResponse<IQoSProvider>(_qoSProviders[key]);
}
return new ErrorResponse<IQoSProvider>(new List<Ocelot.Errors.Error>()
{
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();
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using Ocelot.Errors;
namespace Ocelot.Requester
{
public class RequestTimedOutError : Error
{
public RequestTimedOutError(Exception exception)
: base($"Timeout making http request, exception: {exception.Message}", OcelotErrorCode.RequestTimedOutError)
{
}
}
}

View File

@ -22,6 +22,11 @@ namespace Ocelot.Responder
return new OkResponse<int>(403);
}
if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError))
{
return new OkResponse<int>(503);
}
return new OkResponse<int>(404);
}
}

View File

@ -5,7 +5,13 @@ namespace Ocelot.Responses
{
public class ErrorResponse<T> : Response<T>
{
public ErrorResponse(List<Error> errors) : base(errors)
public ErrorResponse(Error error)
: base(new List<Error> {error})
{
}
public ErrorResponse(List<Error> errors)
: base(errors)
{
}
}

View File

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration.File;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.AcceptanceTests
{
public class QoSTests : IDisposable
{
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()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51879,
UpstreamPathTemplate = "/",
UpstreamHttpMethod = "Get",
QoSOptions = new FileQoSOptions
{
ExceptionsAllowedBeforeBreaking = 1,
TimeoutValue = 500,
DurationOfBreak = 1000
}
}
}
};
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.ServiceUnavailable))
.Given(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable))
.Given(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.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();
}
[Fact]
public void open_circuit_should_not_effect_different_reRoute()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
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 GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody)
{
_brokenService = new WebHostBuilder()
.UseUrls(url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(url)
.Configure(app =>
{
app.Run(async context =>
{
//circuit starts closed
if (_requestCount == 0)
{
_requestCount++;
context.Response.StatusCode = 200;
await context.Response.WriteAsync(responseBody);
return;
}
//request one times out and polly throws exception, circuit opens
if (_requestCount == 1)
{
_requestCount++;
await Task.Delay(1000);
context.Response.StatusCode = 200;
return;
}
//after break closes we return 200 OK
if (_requestCount == 2)
{
context.Response.StatusCode = 200;
await context.Response.WriteAsync(responseBody);
return;
}
});
})
.Build();
_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()
{
_workingService?.Dispose();
_brokenService?.Dispose();
_steps.Dispose();
}
}
}

View File

@ -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<ILoadBalancerFactory> _loadBalancerFactory;
private readonly Mock<ILoadBalancerHouse> _loadBalancerHouse;
private readonly Mock<ILoadBalancer> _loadBalancer;
private readonly Mock<IQoSProviderFactory> _qosProviderFactory;
private readonly Mock<IQosProviderHouse> _qosProviderHouse;
private readonly Mock<IQoSProvider> _qosProvider;
public FileConfigurationCreatorTests()
{
_qosProviderFactory = new Mock<IQoSProviderFactory>();
_qosProviderHouse = new Mock<IQosProviderHouse>();
_qosProvider = new Mock<IQoSProvider>();
_logger = new Mock<ILogger<FileOcelotConfigurationCreator>>();
_configParser = new Mock<IClaimToThingConfigurationParser>();
_validator = new Mock<IConfigurationValidator>();
@ -40,7 +47,8 @@ namespace Ocelot.UnitTests.Configuration
_loadBalancer = new Mock<ILoadBalancer>();
_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,7 +72,36 @@ 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<FileReRoute>
{
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();
}
@ -164,11 +201,11 @@ namespace Ocelot.UnitTests.Configuration
.WithUpstreamPathTemplate("/api/products/{productId}")
.WithUpstreamHttpMethod("Get")
.WithUpstreamTemplatePattern("(?i)/api/products/.*/$")
.WithServiceName("ProductService")
.WithServiceProviderConfiguraion(new ServiceProviderConfiguraionBuilder()
.WithUseServiceDiscovery(true)
.WithServiceDiscoveryProvider("consul")
.WithServiceDiscoveryProviderHost("127.0.0.1")
.WithServiceName("ProductService")
.Build())
.Build()
}))
@ -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<string>(), _loadBalancer.Object), Times.Once);
}
private void GivenTheQosProviderFactoryReturns()
{
_qosProviderFactory
.Setup(x => x.Get(It.IsAny<ReRoute>()))
.Returns(_qosProvider.Object);
}
private void TheQosProviderFactoryIsCalledCorrectly()
{
_qosProviderFactory
.Verify(x => x.Get(It.IsAny<ReRoute>()), Times.Once);
}
private void ThenTheQosProviderHouseIsCalledCorrectly()
{
_qosProviderHouse
.Verify(x => x.Add(It.IsAny<string>(), _qosProvider.Object), Times.Once);
}
}
}

View File

@ -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<List<UrlPathPlaceholderNameAndValue>>(
new List<UrlPathPlaceholderNameAndValue>())))
.And(x => x.GivenTheConfigurationIs(new List<ReRoute>
{
new ReRouteBuilder()
.WithDownstreamPathTemplate("someDownstreamPath")
.WithUpstreamPathTemplate("someUpstreamPath")
.WithUpstreamHttpMethod("Get")
.WithUpstreamTemplatePattern("someUpstreamPath")
.Build()
}))
.And(x => x.GivenTheUrlMatcherReturns(new OkResponse<UrlMatch>(new UrlMatch(true))))
.And(x => x.GivenTheUpstreamHttpMethodIs("Get"))
.When(x => x.WhenICallTheFinder())
.Then(
x => x.ThenTheFollowingIsReturned(new DownstreamRoute(
new List<UrlPathPlaceholderNameAndValue>(),
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<ReRoute>
{
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<UrlMatch> match)
{
_match = match;

View File

@ -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()
{

View File

@ -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<IRequestCreator> _requestBuilder;
private readonly Mock<IRequestScopedDataRepository> _scopedRepository;
private readonly Mock<IQosProviderHouse> _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<IQosProviderHouse>();
_requestBuilder = new Mock<IRequestCreator>();
_scopedRepository = new Mock<IRequestScopedDataRepository>();
var builder = new WebHostBuilder()
@ -45,6 +48,7 @@ namespace Ocelot.UnitTests.Request
{
x.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
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<IQoSProvider>(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<IQoSProvider> qosProvider)
{
_qosProviderHouse
.Setup(x => x.Get(It.IsAny<string>()))
.Returns(qosProvider);
}
private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute)
{
_downstreamRoute = new OkResponse<DownstreamRoute>(downstreamRoute);
@ -94,7 +105,7 @@ namespace Ocelot.UnitTests.Request
_request = new OkResponse<Ocelot.Request.Request>(request);
_requestBuilder
.Setup(x => x.Build(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<Stream>(), It.IsAny<IHeaderDictionary>(),
It.IsAny<IRequestCookieCollection>(), It.IsAny<QueryString>(), It.IsAny<string>(), It.IsAny<Ocelot.RequestId.RequestId>(),It.IsAny<bool>(), It.IsAny<QoSOptions>()))
It.IsAny<IRequestCookieCollection>(), It.IsAny<QueryString>(), It.IsAny<string>(), It.IsAny<Ocelot.RequestId.RequestId>(),It.IsAny<bool>(), It.IsAny<IQoSProvider>()))
.ReturnsAsync(_request);
}

View File

@ -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<Ocelot.Request.Request> _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;
}

View File

@ -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())

View File

@ -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<IOcelotLoggerFactory> _loggerFactory;
private Mock<IOcelotLogger> _logger;
public QoSProviderFactoryTests()
{
_logger = new Mock<IOcelotLogger>();
_loggerFactory = new Mock<IOcelotLoggerFactory>();
_loggerFactory
.Setup(x => x.CreateLogger<PollyQoSProvider>())
.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<NoQoSProvider>())
.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<PollyQoSProvider>())
.BDDfy();
}
private void GivenAReRoute(ReRoute reRoute)
{
_reRoute = reRoute;
}
private void WhenIGetTheQoSProvider()
{
_result = _factory.Get(_reRoute);
}
private void ThenTheQoSProviderIsReturned<T>()
{
_result.ShouldBeOfType<T>();
}
}
}

View File

@ -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<IQoSProvider> _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<FakeQoSProvider>())
.When(x => x.WhenWeGetTheQoSProvider(keyTwo))
.Then(x => x.ThenTheQoSProviderIs<FakePollyQoSProvider>())
.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<UnableToFindQoSProviderError>();
}
private void ThenTheQoSProviderIs<T>()
{
_getResult.Data.ShouldBeOfType<T>();
}
private void ThenItIsAdded()
{
_addResult.IsError.ShouldBe(false);
_addResult.ShouldBeOfType<OkResponse>();
}
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; }
}
}
}

View File

@ -1,6 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Ocelot.Errors;
using Ocelot.Middleware;
using Ocelot.Requester;
using Ocelot.Responder;
using Ocelot.Responses;
using Shouldly;
@ -20,6 +22,18 @@ namespace Ocelot.UnitTests.Responder
_codeMapper = new ErrorsToHttpStatusCodeMapper();
}
[Fact]
public void should_return_timeout()
{
this.Given(x => x.GivenThereAreErrors(new List<Error>
{
new RequestTimedOutError(new Exception())
}))
.When(x => x.WhenIGetErrorStatusCode())
.Then(x => x.ThenTheResponseIsStatusCodeIs(503))
.BDDfy();
}
[Fact]
public void should_create_unauthenticated_response_code()
{