Skip to content

Commit

Permalink
Round robin load balancing #72
Browse files Browse the repository at this point in the history
  • Loading branch information
Tratcher committed Apr 24, 2020
1 parent bee3eca commit c950ea1
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public enum LoadBalancingMode
/// </summary>
Random,
/// <summary>
/// Selects an endpoint by cycling through them in order.
/// </summary>
RoundRobin,
/// <summary>
/// Select the first endpoint without considering load. This is useful for dual endpoint fail-over systems.
/// </summary>
First,
Expand Down
3 changes: 2 additions & 1 deletion src/ReverseProxy.Core/Middleware/LoadBalancingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public Task Invoke(HttpContext context)
var endpoints = endpointsFeature?.Endpoints
?? throw new InvalidOperationException("The AvailableBackendEndpoints collection was not set.");

var loadBalancingOptions = backend.Config.Value?.LoadBalancingOptions ?? default;
var loadBalancingOptions = backend.Config.Value?.LoadBalancingOptions
?? new BackendConfig.BackendLoadBalancingOptions(default);

var endpoint = _operationLogger.Execute(
"ReverseProxy.PickEndpoint",
Expand Down
3 changes: 3 additions & 0 deletions src/ReverseProxy.Core/Service/Proxy/LoadBalancer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public EndpointInfo PickEndpoint(
{
case LoadBalancingMode.First:
return endpoints[0];
case LoadBalancingMode.RoundRobin:
var offset = loadBalancingOptions.RoundRobinCounter.Increment();
return endpoints[offset % endpoints.Count];
case LoadBalancingMode.Random:
var random = _randomFactory.CreateRandomInstance();
return endpoints[random.Next(endpointCount)];
Expand Down
5 changes: 5 additions & 0 deletions src/ReverseProxy.Core/Service/RuntimeModel/BackendConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using Microsoft.ReverseProxy.Core.Abstractions;
using Microsoft.ReverseProxy.Core.Util;

namespace Microsoft.ReverseProxy.Core.RuntimeModel
{
Expand Down Expand Up @@ -79,9 +80,13 @@ internal readonly struct BackendLoadBalancingOptions
public BackendLoadBalancingOptions(LoadBalancingMode mode)
{
Mode = mode;
// Increment returns the new value and we want the first return value to be 0.
RoundRobinCounter = new AtomicCounter() { Value = -1 };
}

public LoadBalancingMode Mode { get; }

public AtomicCounter RoundRobinCounter { get; }
}
}
}
13 changes: 8 additions & 5 deletions src/ReverseProxy.Core/Util/AtomicCounter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,25 @@ public class AtomicCounter
/// <summary>
/// Gets the current value of the counter.
/// </summary>
public int Value => Volatile.Read(ref _value);
public int Value {
get => Volatile.Read(ref _value);
set => Volatile.Write(ref _value, value);
}

/// <summary>
/// Atomically increments the counter value by 1.
/// </summary>
public void Increment()
public int Increment()
{
Interlocked.Increment(ref _value);
return Interlocked.Increment(ref _value);
}

/// <summary>
/// Atomically decrements the counter value by 1.
/// </summary>
public void Decrement()
public int Decrement()
{
Interlocked.Decrement(ref _value);
return Interlocked.Decrement(ref _value);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.ReverseProxy.Common.Abstractions.Telemetry;
using Microsoft.ReverseProxy.Common.Telemetry;
using Microsoft.ReverseProxy.Core.Abstractions;
using Microsoft.ReverseProxy.Core.RuntimeModel;
using Microsoft.ReverseProxy.Core.Service.Management;
using Microsoft.ReverseProxy.Core.Service.Proxy;
Expand Down Expand Up @@ -40,6 +41,7 @@ public async Task Invoke_Works()
backendId: "backend1",
endpointManager: new EndpointManager(),
proxyHttpClientFactory: proxyHttpClientFactoryMock.Object);
backend1.Config.Value = new BackendConfig(default, new BackendConfig.BackendLoadBalancingOptions(LoadBalancingMode.RoundRobin));
var endpoint1 = backend1.EndpointManager.GetOrCreateItem(
"endpoint1",
endpoint =>
Expand All @@ -66,10 +68,7 @@ public async Task Invoke_Works()
aspNetCoreEndpoints.Add(aspNetCoreEndpoint);
var httpContext = new DefaultHttpContext();
httpContext.SetEndpoint(aspNetCoreEndpoint);

Mock<ILoadBalancer>()
.Setup(l => l.PickEndpoint(It.IsAny<IReadOnlyList<EndpointInfo>>(), It.IsAny<BackendConfig.BackendLoadBalancingOptions>()))
.Returns(endpoint1);
Provide<ILoadBalancer, LoadBalancer>();

httpContext.Features.Set<IAvailableBackendEndpointsFeature>(
new AvailableBackendEndpointsFeature() { Endpoints = new List<EndpointInfo>() { endpoint1, endpoint2 }.AsReadOnly() });
Expand Down
23 changes: 23 additions & 0 deletions test/ReverseProxy.Core.Tests/Service/Proxy/LoadBalancerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,29 @@ public void PickEndpoint_LeastRequests_Works()
Assert.Same(result, endpoints[1]);
}

[Fact]
public void PickEndpoint_RoundRobin_Works()
{
var loadBalancer = Create<LoadBalancer>();
var endpoints = new[]
{
new EndpointInfo("ep1"),
new EndpointInfo("ep2"),
};
endpoints[0].ConcurrencyCounter.Increment();
var options = new BackendConfig.BackendLoadBalancingOptions(LoadBalancingMode.RoundRobin);

var result0 = loadBalancer.PickEndpoint(endpoints, in options);
var result1 = loadBalancer.PickEndpoint(endpoints, in options);
var result2 = loadBalancer.PickEndpoint(endpoints, in options);
var result3 = loadBalancer.PickEndpoint(endpoints, in options);

Assert.Same(result0, endpoints[0]);
Assert.Same(result1, endpoints[1]);
Assert.Same(result2, endpoints[0]);
Assert.Same(result3, endpoints[1]);
}

internal class TestRandomFactory : IRandomFactory
{
internal TestRandom Instance { get; set; }
Expand Down

0 comments on commit c950ea1

Please sign in to comment.