From ce8da4c92d55f53ad4ed9204279003ef2b968291 Mon Sep 17 00:00:00 2001 From: TomPallister Date: Sat, 11 Feb 2017 13:14:25 +0000 Subject: [PATCH 1/3] removed qos by accident, added back in --- src/Ocelot/Configuration/Builder/ReRouteBuilder.cs | 7 ------- .../Creator/FileOcelotConfigurationCreator.cs | 4 +++- .../Configuration/FileConfigurationCreatorTests.cs | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index 42e4e5bf..382cf374 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -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; diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index 740a2b95..25888681 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -95,6 +95,7 @@ namespace Ocelot.Configuration.Creator var loadBalancerKey = BuildLoadBalancerKey(fileReRoute); var upstreamTemplatePattern = BuildUpstreamTemplate(fileReRoute); + var isQos = fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions.TimeoutValue >0; var serviceProviderConfiguration = BuildServiceProviderConfiguration(fileReRoute, globalConfiguration); @@ -103,7 +104,6 @@ namespace Ocelot.Configuration.Creator var claimsToHeaders = BuildAddThingsToRequest(fileReRoute.AddHeadersToRequest); - var claimsToClaims = BuildAddThingsToRequest(fileReRoute.AddClaimsToRequest); var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest); @@ -129,6 +129,8 @@ namespace Ocelot.Configuration.Creator .WithDownstreamPort(fileReRoute.DownstreamPort) .WithLoadBalancerKey(loadBalancerKey) .WithServiceProviderConfiguraion(serviceProviderConfiguration) + .WithIsQos(isQos) + .WithQosOptions(new QoSOptions(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking, fileReRoute.QoSOptions.DurationOfBreak, fileReRoute.QoSOptions.TimeoutValue)) .Build(); await SetupLoadBalancer(reRoute); diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 2d2a48ef..5080c9e0 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -164,11 +164,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() })) From 820673dda8b25ecb4ae3098e3bef5600d51fec93 Mon Sep 17 00:00:00 2001 From: TomPallister Date: Sat, 11 Feb 2017 15:11:10 +0000 Subject: [PATCH 2/3] added docs but qos acceptance test not working seems circuit never opens but not sure if it is meant to with timeouts..investigating --- README.md | 40 ++++-- src/Ocelot/Configuration/QoSOptions.cs | 6 +- src/Ocelot/Errors/OcelotErrorCode.cs | 3 +- .../CircuitBreakingDelegatingHandler.cs | 27 ++-- .../Requester/HttpClientHttpRequester.cs | 12 +- src/Ocelot/Requester/RequestTimedOutError.cs | 13 ++ .../Responder/ErrorsToHttpStatusCodeMapper.cs | 5 + src/Ocelot/Responses/ErrorResponseGeneric.cs | 8 +- test/Ocelot.AcceptanceTests/QoSTests.cs | 136 ++++++++++++++++++ .../ErrorsToHttpStatusCodeMapperTests.cs | 16 ++- 10 files changed, 235 insertions(+), 31 deletions(-) create mode 100644 src/Ocelot/Requester/RequestTimedOutError.cs create mode 100644 test/Ocelot.AcceptanceTests/QoSTests.cs diff --git a/README.md b/README.md index 3ef6d6b6..dfbdfd1e 100644 --- a/README.md +++ b/README.md @@ -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 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 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... diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 8584c57e..9deaaeda 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -8,7 +8,11 @@ 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); diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index d24988b9..e2fe4ada 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -25,6 +25,7 @@ ServicesAreNullError, ServicesAreEmptyError, UnableToFindServiceDiscoveryProviderError, - UnableToFindLoadBalancerError + UnableToFindLoadBalancerError, + RequestTimedOutError } } diff --git a/src/Ocelot/Requester/CircuitBreakingDelegatingHandler.cs b/src/Ocelot/Requester/CircuitBreakingDelegatingHandler.cs index 4adc0eeb..b4091296 100644 --- a/src/Ocelot/Requester/CircuitBreakingDelegatingHandler.cs +++ b/src/Ocelot/Requester/CircuitBreakingDelegatingHandler.cs @@ -18,11 +18,17 @@ namespace Ocelot.Requester private readonly Policy _circuitBreakerPolicy; private readonly TimeoutPolicy _timeoutPolicy; - public CircuitBreakingDelegatingHandler(int exceptionsAllowedBeforeBreaking, TimeSpan durationOfBreak,TimeSpan timeoutValue - ,TimeoutStrategy timeoutStrategy, IOcelotLogger logger, HttpMessageHandler innerHandler) + public CircuitBreakingDelegatingHandler( + int exceptionsAllowedBeforeBreaking, + TimeSpan durationOfBreak, + TimeSpan timeoutValue, + TimeoutStrategy timeoutStrategy, + IOcelotLogger logger, + HttpMessageHandler innerHandler) : base(innerHandler) { this._exceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; + this._durationOfBreak = durationOfBreak; _circuitBreakerPolicy = Policy @@ -39,30 +45,27 @@ namespace Ocelot.Requester 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 SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - Task responseTask = null; - try { - responseTask = Policy.WrapAsync(_circuitBreakerPolicy, _timeoutPolicy).ExecuteAsync(() => - { - return base.SendAsync(request,cancellationToken); - }); - return responseTask; + 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) + catch (HttpRequestException ex) { - return responseTask; + _logger.LogError($"Error in CircuitBreakingDelegatingHandler.SendAync", ex); + throw; } } diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index 77ab90cc..cc5332f3 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -27,6 +27,7 @@ namespace Ocelot.Requester { builder.WithCircuitBreaker(request.Qos, _logger, handler); } + using (var httpClient = builder.Build(handler)) { try @@ -34,13 +35,14 @@ namespace Ocelot.Requester var response = await httpClient.SendAsync(request.HttpRequestMessage); return new OkResponse(response); } - catch (Exception exception) + catch (Polly.Timeout.TimeoutRejectedException exception) { return - new ErrorResponse(new List - { - new UnableToCompleteRequestError(exception) - }); + new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (Exception exception) + { + return new ErrorResponse(new UnableToCompleteRequestError(exception)); } } } diff --git a/src/Ocelot/Requester/RequestTimedOutError.cs b/src/Ocelot/Requester/RequestTimedOutError.cs new file mode 100644 index 00000000..38afce1a --- /dev/null +++ b/src/Ocelot/Requester/RequestTimedOutError.cs @@ -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) + { + } + } +} diff --git a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs index 6ecc20eb..b9fcb299 100644 --- a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs +++ b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs @@ -22,6 +22,11 @@ namespace Ocelot.Responder return new OkResponse(403); } + if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError)) + { + return new OkResponse(408); + } + return new OkResponse(404); } } diff --git a/src/Ocelot/Responses/ErrorResponseGeneric.cs b/src/Ocelot/Responses/ErrorResponseGeneric.cs index 77230503..044d6f61 100644 --- a/src/Ocelot/Responses/ErrorResponseGeneric.cs +++ b/src/Ocelot/Responses/ErrorResponseGeneric.cs @@ -5,7 +5,13 @@ namespace Ocelot.Responses { public class ErrorResponse : Response { - public ErrorResponse(List errors) : base(errors) + public ErrorResponse(Error error) + : base(new List {error}) + { + + } + public ErrorResponse(List errors) + : base(errors) { } } diff --git a/test/Ocelot.AcceptanceTests/QoSTests.cs b/test/Ocelot.AcceptanceTests/QoSTests.cs new file mode 100644 index 00000000..50c1fb66 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/QoSTests.cs @@ -0,0 +1,136 @@ +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 _builder; + private readonly Steps _steps; + private int _requestCount; + + public QoSTests() + { + _steps = new Steps(); + } + + + [Fact] + public void should_open_circuit_breaker_then_close() + { + 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 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("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.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.RequestTimeout)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.RequestTimeout)) + .Given(x => x.GivenIWaitMilliSeconds(2000)) + .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) + { + _builder = 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 + if (_requestCount == 1) + { + _requestCount++; + await Task.Delay(1000); + context.Response.StatusCode = 200; + 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) + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + return; + } + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index cb8198dc..d2b7e386 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -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 + { + new RequestTimedOutError(new Exception()) + })) + .When(x => x.WhenIGetErrorStatusCode()) + .Then(x => x.ThenTheResponseIsStatusCodeIs(408)) + .BDDfy(); + } + [Fact] public void should_create_unauthenticated_response_code() { From 286c7f848813d58d4cc7ca3ee386c8e1af5a29d9 Mon Sep 17 00:00:00 2001 From: TomPallister Date: Sat, 11 Feb 2017 18:56:36 +0000 Subject: [PATCH 3/3] Fixed issue where qos was being created for each request so circuit breaker was never stopping traffic going to downstream service. --- .../Builder/QoSOptionsBuilder.cs | 34 ++++ .../Creator/FileOcelotConfigurationCreator.cs | 58 +++++-- src/Ocelot/Configuration/QoSOptions.cs | 7 +- src/Ocelot/Configuration/ReRoute.cs | 6 +- .../ServiceCollectionExtensions.cs | 3 + .../Finder/DownstreamRouteFinder.cs | 7 + src/Ocelot/Errors/OcelotErrorCode.cs | 3 +- .../Middleware/LoadBalancingMiddleware.cs | 3 +- .../Request/Builder/HttpRequestCreator.cs | 5 +- src/Ocelot/Request/Builder/IRequestCreator.cs | 4 +- src/Ocelot/Request/Builder/RequestBuilder.cs | 10 +- .../HttpRequestBuilderMiddleware.cs | 33 +++- src/Ocelot/Request/Request.cs | 11 +- .../CircuitBreakingDelegatingHandler.cs | 77 ---------- src/Ocelot/Requester/HttpClientBuilder.cs | 26 ++-- .../Requester/HttpClientHttpRequester.cs | 21 ++- .../PollyCircuitBreakingDelegatingHandler.cs | 47 ++++++ .../Requester/QoS/IQoSProviderFactory.cs | 145 ++++++++++++++++++ .../QoS/UnableToFindQoSProviderError.cs | 16 ++ .../Responder/ErrorsToHttpStatusCodeMapper.cs | 2 +- test/Ocelot.AcceptanceTests/QoSTests.cs | 116 +++++++++++--- .../FileConfigurationCreatorTests.cs | 62 +++++++- .../DownstreamRouteFinderTests.cs | 41 ++++- .../UrlMatcher/RegExUrlMatcherTests.cs | 20 +++ .../HttpRequestBuilderMiddlewareTests.cs | 17 +- .../Request/RequestBuilderTests.cs | 27 ++-- .../Requester/HttpRequesterMiddlewareTests.cs | 3 +- .../Requester/QoSProviderFactoryTests.cs | 80 ++++++++++ .../Requester/QosProviderHouseTests.cs | 117 ++++++++++++++ .../ErrorsToHttpStatusCodeMapperTests.cs | 2 +- 30 files changed, 819 insertions(+), 184 deletions(-) create mode 100644 src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs delete mode 100644 src/Ocelot/Requester/CircuitBreakingDelegatingHandler.cs create mode 100644 src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs create mode 100644 src/Ocelot/Requester/QoS/IQoSProviderFactory.cs create mode 100644 src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs create mode 100644 test/Ocelot.UnitTests/Requester/QoSProviderFactoryTests.cs create mode 100644 test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs 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(); }