mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-22 06:42:50 +08:00
tests to handle some error cases and docs
This commit is contained in:
parent
b300ed9aec
commit
c9483cdad6
@ -108,3 +108,112 @@ subsequent requests. This means the sessions will be stuck across ReRoutes.
|
|||||||
Please note that if you give more than one DownstreamHostAndPort or you are using a Service Discovery provider such as Consul
|
Please note that if you give more than one DownstreamHostAndPort or you are using a Service Discovery provider such as Consul
|
||||||
and this returns more than one service then CookieStickySessions uses round robin to select the next server. This is hard coded at the
|
and this returns more than one service then CookieStickySessions uses round robin to select the next server. This is hard coded at the
|
||||||
moment but could be changed.
|
moment but could be changed.
|
||||||
|
|
||||||
|
Custom Load Balancers
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
`DavidLievrouw <https://github.com/DavidLievrouw`_ implemented a way to provide Ocelot with custom load balancer in `PR 1155 <https://github.com/ThreeMammals/Ocelot/pull/1155`_.
|
||||||
|
|
||||||
|
In order to create and use a custom load balancer you can do the following. Below we setup a basic load balancing config and not the Type is CustomLoadBalancer this is the name of a class we will
|
||||||
|
setup to do load balancing.
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"DownstreamPathTemplate": "/api/posts/{postId}",
|
||||||
|
"DownstreamScheme": "https",
|
||||||
|
"DownstreamHostAndPorts": [
|
||||||
|
{
|
||||||
|
"Host": "10.0.1.10",
|
||||||
|
"Port": 5000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Host": "10.0.1.11",
|
||||||
|
"Port": 5000,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"UpstreamPathTemplate": "/posts/{postId}",
|
||||||
|
"LoadBalancerOptions": {
|
||||||
|
"Type": "CustomLoadBalancer"
|
||||||
|
},
|
||||||
|
"UpstreamHttpMethod": [ "Put", "Delete" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Then you need to create a class that implements the ILoadBalancer interface. Below is a simple round robin example.
|
||||||
|
|
||||||
|
.. code-block:: csharp
|
||||||
|
|
||||||
|
private class CustomLoadBalancer : ILoadBalancer
|
||||||
|
{
|
||||||
|
private readonly Func<Task<List<Service>>> _services;
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
|
private int _last;
|
||||||
|
|
||||||
|
public CustomLoadBalancer(Func<Task<List<Service>>> services)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Response<ServiceHostAndPort>> Lease(DownstreamContext downstreamContext)
|
||||||
|
{
|
||||||
|
var services = await _services();
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_last >= services.Count)
|
||||||
|
{
|
||||||
|
_last = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = services[_last];
|
||||||
|
_last++;
|
||||||
|
return new OkResponse<ServiceHostAndPort>(next.HostAndPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Release(ServiceHostAndPort hostAndPort)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Finally you need to register this class with Ocelot. I have used the most complex example below to show all of the data / types that can be passed into the factory that creates load balancers.
|
||||||
|
|
||||||
|
.. code-block:: csharp
|
||||||
|
|
||||||
|
Func<IServiceProvider, DownstreamReRoute, IServiceDiscoveryProvider, CustomLoadBalancer> loadBalancerFactoryFunc = (serviceProvider, reRoute, serviceDiscoveryProvider) => new CustomLoadBalancer(serviceDiscoveryProvider.Get);
|
||||||
|
|
||||||
|
s.AddOcelot()
|
||||||
|
.AddCustomLoadBalancer(loadBalancerFactoryFunc);
|
||||||
|
|
||||||
|
However there is a much simpler example that will work the same.
|
||||||
|
|
||||||
|
.. code-block:: csharp
|
||||||
|
|
||||||
|
s.AddOcelot()
|
||||||
|
.AddCustomLoadBalancer<CustomLoadBalancer>();
|
||||||
|
|
||||||
|
There are numerous extension methods to add a custom load balancer and the interface is as follows.
|
||||||
|
|
||||||
|
.. code-block:: csharp
|
||||||
|
|
||||||
|
IOcelotBuilder AddCustomLoadBalancer<T>()
|
||||||
|
where T : ILoadBalancer, new();
|
||||||
|
|
||||||
|
IOcelotBuilder AddCustomLoadBalancer<T>(Func<T> loadBalancerFactoryFunc)
|
||||||
|
where T : ILoadBalancer;
|
||||||
|
|
||||||
|
IOcelotBuilder AddCustomLoadBalancer<T>(Func<IServiceProvider, T> loadBalancerFactoryFunc)
|
||||||
|
where T : ILoadBalancer;
|
||||||
|
|
||||||
|
IOcelotBuilder AddCustomLoadBalancer<T>(
|
||||||
|
Func<DownstreamReRoute, IServiceDiscoveryProvider, T> loadBalancerFactoryFunc)
|
||||||
|
where T : ILoadBalancer;
|
||||||
|
|
||||||
|
IOcelotBuilder AddCustomLoadBalancer<T>(
|
||||||
|
Func<IServiceProvider, DownstreamReRoute, IServiceDiscoveryProvider, T> loadBalancerFactoryFunc)
|
||||||
|
where T : ILoadBalancer;
|
||||||
|
|
||||||
|
When you enable custom load balancers Ocelot looks up your load balancer by its class name when it decides if it should do load balancing. If it finds a match it will load balance your request. If Ocelot cannot match the load balancer type in your configuration with the name of registered load balancer class then you will receive a HTTP 500 internal server error.
|
||||||
|
|
||||||
|
Remember if you specify no load balancer in your config Ocelot will not try and load balance.
|
@ -41,5 +41,6 @@
|
|||||||
QuotaExceededError = 36,
|
QuotaExceededError = 36,
|
||||||
RequestCanceled = 37,
|
RequestCanceled = 37,
|
||||||
ConnectionToDownstreamServiceError = 38,
|
ConnectionToDownstreamServiceError = 38,
|
||||||
|
CouldNotFindLoadBalancerCreator = 39,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
namespace Ocelot.LoadBalancer.LoadBalancers
|
||||||
|
{
|
||||||
|
using Errors;
|
||||||
|
|
||||||
|
public class CouldNotFindLoadBalancerCreator : Error
|
||||||
|
{
|
||||||
|
public CouldNotFindLoadBalancerCreator(string message)
|
||||||
|
: base(message, OcelotErrorCode.CouldNotFindLoadBalancerCreator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,13 @@
|
|||||||
|
|
||||||
var serviceProvider = serviceProviderFactoryResponse.Data;
|
var serviceProvider = serviceProviderFactoryResponse.Data;
|
||||||
var requestedType = reRoute.LoadBalancerOptions?.Type ?? nameof(NoLoadBalancer);
|
var requestedType = reRoute.LoadBalancerOptions?.Type ?? nameof(NoLoadBalancer);
|
||||||
var applicableCreator = _loadBalancerCreators.Single(c => c.Type == requestedType);
|
var applicableCreator = _loadBalancerCreators.SingleOrDefault(c => c.Type == requestedType);
|
||||||
|
|
||||||
|
if (applicableCreator == null)
|
||||||
|
{
|
||||||
|
return new ErrorResponse<ILoadBalancer>(new CouldNotFindLoadBalancerCreator($"Could not find load balancer creator for Type: {requestedType}, please check your config specified the correct load balancer and that you have registered a class with the same name."));
|
||||||
|
}
|
||||||
|
|
||||||
var createdLoadBalancer = applicableCreator.Create(reRoute, serviceProvider);
|
var createdLoadBalancer = applicableCreator.Create(reRoute, serviceProvider);
|
||||||
return new OkResponse<ILoadBalancer>(createdLoadBalancer);
|
return new OkResponse<ILoadBalancer>(createdLoadBalancer);
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,8 @@ namespace Ocelot.Responder
|
|||||||
return 502;
|
return 502;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.Any(e => e.Code == OcelotErrorCode.UnableToCompleteRequestError))
|
if (errors.Any(e => e.Code == OcelotErrorCode.UnableToCompleteRequestError
|
||||||
|
|| e.Code == OcelotErrorCode.CouldNotFindLoadBalancerCreator))
|
||||||
{
|
{
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,13 @@ namespace Ocelot.AcceptanceTests
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Configuration;
|
||||||
|
using Middleware;
|
||||||
|
using Responses;
|
||||||
|
using ServiceDiscovery.Providers;
|
||||||
using TestStack.BDDfy;
|
using TestStack.BDDfy;
|
||||||
|
using Values;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
public class LoadBalancerTests : IDisposable
|
public class LoadBalancerTests : IDisposable
|
||||||
@ -122,6 +128,88 @@ namespace Ocelot.AcceptanceTests
|
|||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_load_balance_request_with_custom_load_balancer()
|
||||||
|
{
|
||||||
|
var downstreamPortOne = RandomPortFinder.GetRandomPort();
|
||||||
|
var downstreamPortTwo = RandomPortFinder.GetRandomPort();
|
||||||
|
var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}";
|
||||||
|
var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}";
|
||||||
|
|
||||||
|
var configuration = new FileConfiguration
|
||||||
|
{
|
||||||
|
ReRoutes = new List<FileReRoute>
|
||||||
|
{
|
||||||
|
new FileReRoute
|
||||||
|
{
|
||||||
|
DownstreamPathTemplate = "/",
|
||||||
|
DownstreamScheme = "http",
|
||||||
|
UpstreamPathTemplate = "/",
|
||||||
|
UpstreamHttpMethod = new List<string> { "Get" },
|
||||||
|
LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(CustomLoadBalancer) },
|
||||||
|
DownstreamHostAndPorts = new List<FileHostAndPort>
|
||||||
|
{
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = downstreamPortOne,
|
||||||
|
},
|
||||||
|
new FileHostAndPort
|
||||||
|
{
|
||||||
|
Host = "localhost",
|
||||||
|
Port = downstreamPortTwo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GlobalConfiguration = new FileGlobalConfiguration(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Func<IServiceProvider, DownstreamReRoute, IServiceDiscoveryProvider, CustomLoadBalancer> loadBalancerFactoryFunc = (serviceProvider, reRoute, serviceDiscoveryProvider) => new CustomLoadBalancer(serviceDiscoveryProvider.Get);
|
||||||
|
|
||||||
|
this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200))
|
||||||
|
.And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200))
|
||||||
|
.And(x => _steps.GivenThereIsAConfiguration(configuration))
|
||||||
|
.And(x => _steps.GivenOcelotIsRunningWithCustomLoadBalancer(loadBalancerFactoryFunc))
|
||||||
|
.When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50))
|
||||||
|
.Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50))
|
||||||
|
.And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26))
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomLoadBalancer : ILoadBalancer
|
||||||
|
{
|
||||||
|
private readonly Func<Task<List<Service>>> _services;
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
|
private int _last;
|
||||||
|
|
||||||
|
public CustomLoadBalancer(Func<Task<List<Service>>> services)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Response<ServiceHostAndPort>> Lease(DownstreamContext downstreamContext)
|
||||||
|
{
|
||||||
|
var services = await _services();
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_last >= services.Count)
|
||||||
|
{
|
||||||
|
_last = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = services[_last];
|
||||||
|
_last++;
|
||||||
|
return new OkResponse<ServiceHostAndPort>(next.HostAndPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Release(ServiceHostAndPort hostAndPort)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top)
|
private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top)
|
||||||
{
|
{
|
||||||
_counterOne.ShouldBeInRange(bottom, top);
|
_counterOne.ShouldBeInRange(bottom, top);
|
||||||
|
@ -39,6 +39,9 @@ namespace Ocelot.AcceptanceTests
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Configuration;
|
||||||
|
using LoadBalancer.LoadBalancers;
|
||||||
|
using ServiceDiscovery.Providers;
|
||||||
using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests;
|
using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests;
|
||||||
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
|
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
|
||||||
using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue;
|
using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue;
|
||||||
@ -255,6 +258,39 @@ namespace Ocelot.AcceptanceTests
|
|||||||
_ocelotClient = _ocelotServer.CreateClient();
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step.
|
||||||
|
/// </summary>
|
||||||
|
public void GivenOcelotIsRunningWithCustomLoadBalancer<T>(Func<IServiceProvider, DownstreamReRoute, IServiceDiscoveryProvider, T> loadBalancerFactoryFunc)
|
||||||
|
where T : ILoadBalancer
|
||||||
|
{
|
||||||
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
|
||||||
|
_webHostBuilder
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
|
||||||
|
var env = hostingContext.HostingEnvironment;
|
||||||
|
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
|
||||||
|
config.AddJsonFile("ocelot.json", false, false);
|
||||||
|
config.AddEnvironmentVariables();
|
||||||
|
})
|
||||||
|
.ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddOcelot()
|
||||||
|
.AddCustomLoadBalancer(loadBalancerFactoryFunc);
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseOcelot().Wait();
|
||||||
|
});
|
||||||
|
|
||||||
|
_ocelotServer = new TestServer(_webHostBuilder);
|
||||||
|
|
||||||
|
_ocelotClient = _ocelotServer.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
public void GivenOcelotIsRunningWithConsul()
|
public void GivenOcelotIsRunningWithConsul()
|
||||||
{
|
{
|
||||||
_webHostBuilder = new WebHostBuilder();
|
_webHostBuilder = new WebHostBuilder();
|
||||||
|
@ -70,6 +70,23 @@ namespace Ocelot.UnitTests.LoadBalancer
|
|||||||
.BDDfy();
|
.BDDfy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void should_return_error_response_if_cannot_find_load_balancer_creator()
|
||||||
|
{
|
||||||
|
var reRoute = new DownstreamReRouteBuilder()
|
||||||
|
.WithLoadBalancerOptions(new LoadBalancerOptions("DoesntExistLoadBalancer", "", 0))
|
||||||
|
.WithUpstreamHttpMethod(new List<string> { "Get" })
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
this.Given(x => x.GivenAReRoute(reRoute))
|
||||||
|
.And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build()))
|
||||||
|
.And(x => x.GivenTheServiceProviderFactoryReturns())
|
||||||
|
.When(x => x.WhenIGetTheLoadBalancer())
|
||||||
|
.Then(x => x.ThenAnErrorResponseIsReturned())
|
||||||
|
.And(x => x.ThenTheErrorMessageIsCorrect())
|
||||||
|
.BDDfy();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void should_call_service_provider()
|
public void should_call_service_provider()
|
||||||
{
|
{
|
||||||
@ -147,6 +164,11 @@ namespace Ocelot.UnitTests.LoadBalancer
|
|||||||
_result.IsError.ShouldBeTrue();
|
_result.IsError.ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ThenTheErrorMessageIsCorrect()
|
||||||
|
{
|
||||||
|
_result.Errors[0].Message.ShouldBe("Could not find load balancer creator for Type: DoesntExistLoadBalancer, please check your config specified the correct load balancer and that you have registered a class with the same name.");
|
||||||
|
}
|
||||||
|
|
||||||
private class FakeLoadBalancerCreator<T> : ILoadBalancerCreator
|
private class FakeLoadBalancerCreator<T> : ILoadBalancerCreator
|
||||||
where T : ILoadBalancer, new()
|
where T : ILoadBalancer, new()
|
||||||
{
|
{
|
||||||
|
@ -47,6 +47,7 @@ namespace Ocelot.UnitTests.Responder
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(OcelotErrorCode.UnableToCompleteRequestError)]
|
[InlineData(OcelotErrorCode.UnableToCompleteRequestError)]
|
||||||
|
[InlineData(OcelotErrorCode.CouldNotFindLoadBalancerCreator)]
|
||||||
public void should_return_internal_server_error(OcelotErrorCode errorCode)
|
public void should_return_internal_server_error(OcelotErrorCode errorCode)
|
||||||
{
|
{
|
||||||
ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.InternalServerError);
|
ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.InternalServerError);
|
||||||
@ -120,7 +121,7 @@ namespace Ocelot.UnitTests.Responder
|
|||||||
var errors = new List<OcelotErrorCode>
|
var errors = new List<OcelotErrorCode>
|
||||||
{
|
{
|
||||||
OcelotErrorCode.CannotAddDataError,
|
OcelotErrorCode.CannotAddDataError,
|
||||||
OcelotErrorCode.RequestTimedOutError
|
OcelotErrorCode.RequestTimedOutError,
|
||||||
};
|
};
|
||||||
|
|
||||||
ShouldMapErrorsToStatusCode(errors, HttpStatusCode.ServiceUnavailable);
|
ShouldMapErrorsToStatusCode(errors, HttpStatusCode.ServiceUnavailable);
|
||||||
@ -132,7 +133,7 @@ namespace Ocelot.UnitTests.Responder
|
|||||||
// If this test fails then it's because the number of error codes has changed.
|
// If this test fails then it's because the number of error codes has changed.
|
||||||
// You should make the appropriate changes to the test cases here to ensure
|
// You should make the appropriate changes to the test cases here to ensure
|
||||||
// they cover all the error codes, and then modify this assertion.
|
// they cover all the error codes, and then modify this assertion.
|
||||||
Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(39, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?");
|
Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(40, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode)
|
private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user