From fb0f101732277e31134ce06a2d9d4e7ca04ecc1c Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Sun, 5 Feb 2017 21:08:16 +0000 Subject: [PATCH] wip fake consul provider --- .../LeastConnectionLoadBalancer.cs | 37 ++++--- .../LoadBalancers/LoadBalancerHouse.cs | 12 ++- .../Middleware/OcelotMiddlewareExtensions.cs | 16 +++ .../ServiceDiscoveryTests.cs | 98 +++++++++++++++---- .../TestConfiguration.cs | 2 +- test/Ocelot.AcceptanceTests/project.json | 3 +- .../LoadBalancer/LeastConnectionTests.cs | 45 +++++++++ 7 files changed, 174 insertions(+), 39 deletions(-) diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs index 38984567..bfb4817b 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs @@ -13,6 +13,7 @@ namespace Ocelot.LoadBalancer.LoadBalancers private readonly Func>> _services; private readonly List _leases; private readonly string _serviceName; + private static readonly object _syncLock = new object(); public LeastConnectionLoadBalancer(Func>> services, string serviceName) { @@ -35,32 +36,38 @@ namespace Ocelot.LoadBalancer.LoadBalancers return new ErrorResponse(new List() { new ServicesAreEmptyError($"services were empty for {_serviceName}") }); } - //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something? - UpdateServices(services); + lock(_syncLock) + { + //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something? + UpdateServices(services); - var leaseWithLeastConnections = GetLeaseWithLeastConnections(); + var leaseWithLeastConnections = GetLeaseWithLeastConnections(); - _leases.Remove(leaseWithLeastConnections); + _leases.Remove(leaseWithLeastConnections); - leaseWithLeastConnections = AddConnection(leaseWithLeastConnections); + leaseWithLeastConnections = AddConnection(leaseWithLeastConnections); - _leases.Add(leaseWithLeastConnections); - - return new OkResponse(new HostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort)); + _leases.Add(leaseWithLeastConnections); + + return new OkResponse(new HostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort)); + } } public Response Release(HostAndPort hostAndPort) { - var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost - && l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort); - - if (matchingLease != null) + lock(_syncLock) { - var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1); + var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost + && l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort); - _leases.Remove(matchingLease); + if (matchingLease != null) + { + var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1); - _leases.Add(replacementLease); + _leases.Remove(matchingLease); + + _leases.Add(replacementLease); + } } return new OkResponse(); diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs index 12c040c0..63ac3243 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -32,8 +32,16 @@ namespace Ocelot.LoadBalancer.LoadBalancers public Response Add(string key, ILoadBalancer loadBalancer) { - _loadBalancers[key] = loadBalancer; - return new OkResponse(); + try + { + _loadBalancers.Add(key, loadBalancer); + return new OkResponse(); + } + catch (System.Exception exception) + { + Console.WriteLine(exception.StackTrace); + throw; + } } } } diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index b0cd3f7e..352aa501 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -18,6 +18,7 @@ namespace Ocelot.Middleware using System.Threading.Tasks; using Authorisation.Middleware; using Microsoft.AspNetCore.Http; + using Ocelot.Configuration.Provider; using Ocelot.LoadBalancer.Middleware; public static class OcelotMiddlewareExtensions @@ -29,6 +30,7 @@ namespace Ocelot.Middleware /// public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder) { + CreateConfiguration(builder); builder.UseOcelot(new OcelotMiddlewareConfiguration()); return builder; } @@ -41,6 +43,8 @@ namespace Ocelot.Middleware /// public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration) { + CreateConfiguration(builder); + // This is registered to catch any global exceptions that are not handled builder.UseExceptionHandlerMiddleware(); @@ -118,6 +122,18 @@ namespace Ocelot.Middleware return builder; } + private static void CreateConfiguration(IApplicationBuilder builder) + { + var configProvider = (IOcelotConfigurationProvider)builder.ApplicationServices.GetService(typeof(IOcelotConfigurationProvider)); + + var config = configProvider.Get(); + + if(config == null) + { + throw new Exception("Unable to start Ocelot: configuration was null"); + } + } + private static void UseIfNotNull(this IApplicationBuilder builder, Func, Task> middleware) { if (middleware != null) diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index 06bee686..d8e5149d 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Net; +using Consul; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -14,13 +15,19 @@ namespace Ocelot.AcceptanceTests { public class ServiceDiscoveryTests : IDisposable { - private IWebHost _builder; + private IWebHost _builderOne; + private IWebHost _builderTwo; private IWebHost _fakeConsulBuilder; private readonly Steps _steps; + private List _serviceEntries; + private int _counterOne; + private int _counterTwo; + private static readonly object _syncLock = new object(); public ServiceDiscoveryTests() { _steps = new Steps(); + _serviceEntries = new List(); } [Fact] @@ -32,6 +39,28 @@ namespace Ocelot.AcceptanceTests var fakeConsulServiceDiscoveryUrl = "http://localhost:8500"; var downstreamServiceOneCounter = 0; var downstreamServiceTwoCounter = 0; + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = "localhost", + Port = 50879, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + var serviceEntryTwo = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = "localhost", + Port = 50880, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; var configuration = new FileConfiguration { @@ -52,38 +81,42 @@ namespace Ocelot.AcceptanceTests ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() { Provider = "Consul", - Host = "localhost" + Host = "localhost", + Port = 8500 } } }; - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, 200, downstreamServiceOneCounter)) - .And(x => x.GivenThereIsAServiceRunningOn(downstreamServiceTwoUrl, 200, downstreamServiceTwoCounter)) + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceName, downstreamServiceOneUrl, downstreamServiceTwoUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50, downstreamServiceOneCounter, downstreamServiceTwoCounter)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(downstreamServiceOneCounter,downstreamServiceTwoCounter)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes()) .BDDfy(); } - private void ThenBothServicesCalledRealisticAmountOfTimes(int counterOne, int counterTwo) + private void ThenBothServicesCalledRealisticAmountOfTimes() { - counterOne.ShouldBeGreaterThan(10); - counterTwo.ShouldBeGreaterThan(10); + _counterOne.ShouldBeGreaterThan(25); + _counterTwo.ShouldBeGreaterThan(25); } - private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected, int counterOne, int counterTwo) + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) { - var total = counterOne + counterTwo; + var total = _counterOne + _counterTwo; total.ShouldBe(expected); } - private void GivenTheServicesAreRegisteredWithConsul(string serviceName, params string[] urls) + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) { - //register these services with fake consul + foreach(var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } } private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) @@ -98,7 +131,10 @@ namespace Ocelot.AcceptanceTests { app.Run(async context => { - //do consul shit + if(context.Request.Path.Value == "/v1/health/service/product") + { + await context.Response.WriteJsonAsync(_serviceEntries); + } }); }) .Build(); @@ -106,9 +142,9 @@ namespace Ocelot.AcceptanceTests _fakeConsulBuilder.Start(); } - private void GivenThereIsAServiceRunningOn(string url, int statusCode, int counter) + private void GivenProductServiceOneIsRunning(string url, int statusCode) { - _builder = new WebHostBuilder() + _builderOne = new WebHostBuilder() .UseUrls(url) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) @@ -118,18 +154,40 @@ namespace Ocelot.AcceptanceTests { app.Run(async context => { - counter++; + _counterOne++; context.Response.StatusCode = statusCode; }); }) .Build(); - _builder.Start(); + _builderOne.Start(); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _builderTwo = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + _counterTwo++; + context.Response.StatusCode = statusCode; + }); + }) + .Build(); + + _builderTwo.Start(); } public void Dispose() { - _builder?.Dispose(); + _builderOne?.Dispose(); + _builderTwo?.Dispose(); _steps.Dispose(); } } diff --git a/test/Ocelot.AcceptanceTests/TestConfiguration.cs b/test/Ocelot.AcceptanceTests/TestConfiguration.cs index ce802efb..6784391c 100644 --- a/test/Ocelot.AcceptanceTests/TestConfiguration.cs +++ b/test/Ocelot.AcceptanceTests/TestConfiguration.cs @@ -28,7 +28,7 @@ { var runTime = $"{oSDescription}-{osArchitecture}".ToLower(); - var configPath = $"./bin/Debug/netcoreapp{Version}/{runTime}/configuration.json"; + var configPath = $"./test/Ocelot.AcceptanceTests/bin/Debug/netcoreapp{Version}/{runTime}/configuration.json"; return configPath; } diff --git a/test/Ocelot.AcceptanceTests/project.json b/test/Ocelot.AcceptanceTests/project.json index 2e5f9ee8..f1aa378b 100644 --- a/test/Ocelot.AcceptanceTests/project.json +++ b/test/Ocelot.AcceptanceTests/project.json @@ -32,7 +32,8 @@ "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", "Microsoft.NETCore.App": "1.1.0", "Shouldly": "2.8.2", - "TestStack.BDDfy": "4.3.2" + "TestStack.BDDfy": "4.3.2", + "Consul": "0.7.2.1" }, "runtimes": { "win10-x64": {}, diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs index 47b3a7d0..3896b68e 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; using Ocelot.LoadBalancer.LoadBalancers; @@ -15,6 +16,50 @@ namespace Ocelot.UnitTests.LoadBalancer private Response _result; private LeastConnectionLoadBalancer _leastConnection; private List _services; + private Random _random; + + public LeastConnectionTests() + { + _random = new Random(); + } + + [Fact] + public void should_be_able_to_lease_and_release_concurrently() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var tasks = new Task[100]; + try + { + for(var i = 0; i < tasks.Length; i++) + { + tasks[i] = LeaseDelayAndRelease(); + } + + Task.WaitAll(tasks); + } + catch (System.Exception exception) + { + Console.WriteLine(exception.StackTrace); + throw; + } + } + + private async Task LeaseDelayAndRelease() + { + var hostAndPort = await _leastConnection.Lease(); + await Task.Delay(_random.Next(1, 100)); + var response = _leastConnection.Release(hostAndPort.Data); + } [Fact] public void should_get_next_url()