refactoring service discovery and load balancing approach into load balancing middleware

This commit is contained in:
TomPallister
2017-02-01 22:00:01 +00:00
54 changed files with 1160 additions and 547 deletions

View File

@ -1,14 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Ocelot.Errors;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Responses;
using Ocelot.Values;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests
namespace Ocelot.UnitTests.LoadBalancer
{
public class LeastConnectionTests
{
@ -17,10 +15,6 @@ namespace Ocelot.UnitTests
private LeastConnectionLoadBalancer _leastConnection;
private List<Service> _services;
public LeastConnectionTests()
{
}
[Fact]
public void should_get_next_url()
{
@ -202,161 +196,4 @@ namespace Ocelot.UnitTests
_result.Data.DownstreamPort.ShouldBe(_hostAndPort.DownstreamPort);
}
}
public class LeastConnectionLoadBalancer : ILoadBalancer
{
private Func<List<Service>> _services;
private List<Lease> _leases;
private string _serviceName;
public LeastConnectionLoadBalancer(Func<List<Service>> services, string serviceName)
{
_services = services;
_serviceName = serviceName;
_leases = new List<Lease>();
}
public Response<HostAndPort> Lease()
{
var services = _services();
if(services == null)
{
return new ErrorResponse<HostAndPort>(new List<Error>(){ new ServicesAreNullError($"services were null for {_serviceName}")});
}
if(!services.Any())
{
return new ErrorResponse<HostAndPort>(new List<Error>(){ 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);
var leaseWithLeastConnections = GetLeaseWithLeastConnections();
_leases.Remove(leaseWithLeastConnections);
leaseWithLeastConnections = AddConnection(leaseWithLeastConnections);
_leases.Add(leaseWithLeastConnections);
return new OkResponse<HostAndPort>(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)
{
var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1);
_leases.Remove(matchingLease);
_leases.Add(replacementLease);
}
return new OkResponse();
}
private Lease AddConnection(Lease lease)
{
return new Lease(lease.HostAndPort, lease.Connections + 1);
}
private Lease GetLeaseWithLeastConnections()
{
//now get the service with the least connections?
Lease leaseWithLeastConnections = null;
for(var i = 0; i < _leases.Count; i++)
{
if(i == 0)
{
leaseWithLeastConnections = _leases[i];
}
else
{
if(_leases[i].Connections < leaseWithLeastConnections.Connections)
{
leaseWithLeastConnections = _leases[i];
}
}
}
return leaseWithLeastConnections;
}
private Response UpdateServices(List<Service> services)
{
if(_leases.Count > 0)
{
var leasesToRemove = new List<Lease>();
foreach(var lease in _leases)
{
var match = services.FirstOrDefault(s => s.HostAndPort.DownstreamHost == lease.HostAndPort.DownstreamHost
&& s.HostAndPort.DownstreamPort == lease.HostAndPort.DownstreamPort);
if(match == null)
{
leasesToRemove.Add(lease);
}
}
foreach(var lease in leasesToRemove)
{
_leases.Remove(lease);
}
foreach(var service in services)
{
var exists = _leases.FirstOrDefault(l => l.HostAndPort.ToString() == service.HostAndPort.ToString());
if(exists == null)
{
_leases.Add(new Lease(service.HostAndPort, 0));
}
}
}
else
{
foreach(var service in services)
{
_leases.Add(new Lease(service.HostAndPort, 0));
}
}
return new OkResponse();
}
}
public class Lease
{
public Lease(HostAndPort hostAndPort, int connections)
{
HostAndPort = hostAndPort;
Connections = connections;
}
public HostAndPort HostAndPort {get;private set;}
public int Connections {get;private set;}
}
public class ServicesAreNullError : Error
{
public ServicesAreNullError(string message)
: base(message, OcelotErrorCode.ServicesAreNullError)
{
}
}
public class ServicesAreEmptyError : Error
{
public ServicesAreEmptyError(string message)
: base(message, OcelotErrorCode.ServicesAreEmptyError)
{
}
}
}

View File

@ -0,0 +1,97 @@
using Moq;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.ServiceDiscovery;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.LoadBalancer
{
public class LoadBalancerFactoryTests
{
private ReRoute _reRoute;
private LoadBalancerFactory _factory;
private ILoadBalancer _result;
private Mock<IServiceProvider> _serviceProvider;
public LoadBalancerFactoryTests()
{
_serviceProvider = new Mock<IServiceProvider>();
_factory = new LoadBalancerFactory(_serviceProvider.Object);
}
[Fact]
public void should_return_no_load_balancer()
{
var reRoute = new ReRouteBuilder()
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<NoLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_return_round_robin_load_balancer()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("RoundRobin")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<RoundRobinLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_return_round_least_connection_balancer()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("LeastConnection")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<LeastConnectionLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_call_service_provider()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("RoundRobin")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheServiceProviderIsCalledCorrectly())
.BDDfy();
}
private void ThenTheServiceProviderIsCalledCorrectly()
{
_serviceProvider
.Verify(x => x.Get(), Times.Once);
}
private void GivenAReRoute(ReRoute reRoute)
{
_reRoute = reRoute;
}
private void WhenIGetTheLoadBalancer()
{
_result = _factory.Get(_reRoute.ServiceName, _reRoute.LoadBalancer);
}
private void ThenTheLoadBalancerIsReturned<T>()
{
_result.ShouldBeOfType<T>();
}
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Responses;
using Ocelot.Values;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.LoadBalancer
{
public class NoLoadBalancerTests
{
private List<Service> _services;
private NoLoadBalancer _loadBalancer;
private Response<HostAndPort> _result;
[Fact]
public void should_return_host_and_port()
{
var hostAndPort = new HostAndPort("127.0.0.1", 80);
var services = new List<Service>
{
new Service("product", hostAndPort)
};
this.Given(x => x.GivenServices(services))
.When(x => x.WhenIGetTheNextHostAndPort())
.Then(x => x.ThenTheHostAndPortIs(hostAndPort))
.BDDfy();
}
private void GivenServices(List<Service> services)
{
_services = services;
}
private void WhenIGetTheNextHostAndPort()
{
_loadBalancer = new NoLoadBalancer(_services);
_result = _loadBalancer.Lease();
}
private void ThenTheHostAndPortIs(HostAndPort expected)
{
_result.Data.ShouldBe(expected);
}
}
}

View File

@ -1,12 +1,13 @@
using System.Collections.Generic;
using System.Diagnostics;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Responses;
using Ocelot.Values;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests
namespace Ocelot.UnitTests.LoadBalancer
{
public class RoundRobinTests
{
@ -64,38 +65,4 @@ namespace Ocelot.UnitTests
_hostAndPort.Data.ShouldBe(_services[index].HostAndPort);
}
}
public interface ILoadBalancer
{
Response<HostAndPort> Lease();
Response Release(HostAndPort hostAndPort);
}
public class RoundRobinLoadBalancer : ILoadBalancer
{
private readonly List<Service> _services;
private int _last;
public RoundRobinLoadBalancer(List<Service> services)
{
_services = services;
}
public Response<HostAndPort> Lease()
{
if (_last >= _services.Count)
{
_last = 0;
}
var next = _services[_last];
_last++;
return new OkResponse<HostAndPort>(next.HostAndPort);
}
public Response Release(HostAndPort hostAndPort)
{
return new OkResponse();
}
}
}

View File

@ -1,187 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.Responses;
using Ocelot.Values;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests
{
public class LoadBalancerFactoryTests
{
private ReRoute _reRoute;
private LoadBalancerFactory _factory;
private ILoadBalancer _result;
private Mock<Ocelot.ServiceDiscovery.IServiceProvider> _serviceProvider;
public LoadBalancerFactoryTests()
{
_serviceProvider = new Mock<Ocelot.ServiceDiscovery.IServiceProvider>();
_factory = new LoadBalancerFactory(_serviceProvider.Object);
}
[Fact]
public void should_return_no_load_balancer()
{
var reRoute = new ReRouteBuilder()
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<NoLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_return_round_robin_load_balancer()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("RoundRobin")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<RoundRobinLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_return_round_least_connection_balancer()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("LeastConnection")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheLoadBalancerIsReturned<LeastConnectionLoadBalancer>())
.BDDfy();
}
[Fact]
public void should_call_service_provider()
{
var reRoute = new ReRouteBuilder()
.WithLoadBalancer("RoundRobin")
.Build();
this.Given(x => x.GivenAReRoute(reRoute))
.When(x => x.WhenIGetTheLoadBalancer())
.Then(x => x.ThenTheServiceProviderIsCalledCorrectly(reRoute))
.BDDfy();
}
private void ThenTheServiceProviderIsCalledCorrectly(ReRoute reRoute)
{
_serviceProvider
.Verify(x => x.Get(), Times.Once);
}
private void GivenAReRoute(ReRoute reRoute)
{
_reRoute = reRoute;
}
private void WhenIGetTheLoadBalancer()
{
_result = _factory.Get(_reRoute);
}
private void ThenTheLoadBalancerIsReturned<T>()
{
_result.ShouldBeOfType<T>();
}
}
public class NoLoadBalancerTests
{
private List<Service> _services;
private NoLoadBalancer _loadBalancer;
private Response<HostAndPort> _result;
[Fact]
public void should_return_host_and_port()
{
var hostAndPort = new HostAndPort("127.0.0.1", 80);
var services = new List<Service>
{
new Service("product", hostAndPort)
};
this.Given(x => x.GivenServices(services))
.When(x => x.WhenIGetTheNextHostAndPort())
.Then(x => x.ThenTheHostAndPortIs(hostAndPort))
.BDDfy();
}
private void GivenServices(List<Service> services)
{
_services = services;
}
private void WhenIGetTheNextHostAndPort()
{
_loadBalancer = new NoLoadBalancer(_services);
_result = _loadBalancer.Lease();
}
private void ThenTheHostAndPortIs(HostAndPort expected)
{
_result.Data.ShouldBe(expected);
}
}
public class NoLoadBalancer : ILoadBalancer
{
private List<Service> _services;
public NoLoadBalancer(List<Service> services)
{
_services = services;
}
public Response<HostAndPort> Lease()
{
var service = _services.FirstOrDefault();
return new OkResponse<HostAndPort>(service.HostAndPort);
}
public Response Release(HostAndPort hostAndPort)
{
return new OkResponse();
}
}
public interface ILoadBalancerFactory
{
ILoadBalancer Get(ReRoute reRoute);
}
public class LoadBalancerFactory : ILoadBalancerFactory
{
private Ocelot.ServiceDiscovery.IServiceProvider _serviceProvider;
public LoadBalancerFactory(Ocelot.ServiceDiscovery.IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public ILoadBalancer Get(ReRoute reRoute)
{
switch (reRoute.LoadBalancer)
{
case "RoundRobin":
return new RoundRobinLoadBalancer(_serviceProvider.Get());
case "LeastConnection":
return new LeastConnectionLoadBalancer(() => _serviceProvider.Get(), reRoute.ServiceName);
default:
return new NoLoadBalancer(_serviceProvider.Get());
}
}
}
}

View File

@ -1,17 +1,15 @@
using System;
using System.Collections.Generic;
using Ocelot.Configuration;
using Ocelot.ServiceDiscovery;
using Ocelot.Values;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests
namespace Ocelot.UnitTests.ServiceDiscovery
{
public class NoServiceProviderTests
public class ConfigurationServiceProviderTests
{
private NoServiceProvider _serviceProvider;
private ConfigurationServiceProvider _serviceProvider;
private HostAndPort _hostAndPort;
private List<Service> _result;
private List<Service> _expected;
@ -26,20 +24,20 @@ namespace Ocelot.UnitTests
new Service("product", hostAndPort)
};
this.Given(x => x.GivenAHostAndPort(services))
this.Given(x => x.GivenServices(services))
.When(x => x.WhenIGetTheService())
.Then(x => x.ThenTheFollowingIsReturned(services))
.BDDfy();
}
private void GivenAHostAndPort(List<Service> services)
private void GivenServices(List<Service> services)
{
_expected = services;
}
private void WhenIGetTheService()
{
_serviceProvider = new NoServiceProvider(_expected);
_serviceProvider = new ConfigurationServiceProvider(_expected);
_result = _serviceProvider.Get();
}

View File

@ -1,17 +1,15 @@
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.ServiceDiscovery;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests
namespace Ocelot.UnitTests.ServiceDiscovery
{
public class ServiceProviderFactoryTests
{
private ReRoute _reRote;
private ServiceConfiguraion _serviceConfig;
private IServiceProvider _result;
private ServiceProviderFactory _factory;
private readonly ServiceProviderFactory _factory;
public ServiceProviderFactoryTests()
{
@ -21,25 +19,22 @@ namespace Ocelot.UnitTests
[Fact]
public void should_return_no_service_provider()
{
var reRoute = new ReRouteBuilder()
.WithDownstreamHost("127.0.0.1")
.WithDownstreamPort(80)
.Build();
var serviceConfig = new ServiceConfiguraion("product", "127.0.0.1", 80, false);
this.Given(x => x.GivenTheReRoute(reRoute))
this.Given(x => x.GivenTheReRoute(serviceConfig))
.When(x => x.WhenIGetTheServiceProvider())
.Then(x => x.ThenTheServiceProviderIs<NoServiceProvider>())
.Then(x => x.ThenTheServiceProviderIs<ConfigurationServiceProvider>())
.BDDfy();
}
private void GivenTheReRoute(ReRoute reRoute)
private void GivenTheReRoute(ServiceConfiguraion serviceConfig)
{
_reRote = reRoute;
_serviceConfig = serviceConfig;
}
private void WhenIGetTheServiceProvider()
{
_result = _factory.Get(_reRote);
_result = _factory.Get(_serviceConfig);
}
private void ThenTheServiceProviderIs<T>()

View File

@ -4,7 +4,7 @@ using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests
namespace Ocelot.UnitTests.ServiceDiscovery
{
public class ServiceRegistryTests
{

View File

@ -1,5 +1,5 @@
{
"version": "1.0.0-*",
"version": "0.0.0-dev",
"testRunner": "xunit",
@ -13,7 +13,7 @@
"Microsoft.Extensions.Logging.Debug": "1.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0",
"Ocelot": "1.0.0-*",
"Ocelot": "0.0.0-dev",
"xunit": "2.2.0-beta2-build3300",
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"Moq": "4.6.38-alpha",