Skip to content

Commit

Permalink
Added least connected load balancing strategy
Browse files Browse the repository at this point in the history
This replaces the existing round-robin. It gives us better performance
with clusters composed of different machine capacities.
Lease connected load balancing strategy selects start index in
round-robin fashion to avoid always selecting same machine when cluster
does not have any running transactions or all machines have same number
of active connections.

It is possible to go back to previous round-robin load balancing
strategy using an experimental config setting:
```
  {loadBalancingStrategy: 'round_robin'}
```
  • Loading branch information
lutovich committed Jul 11, 2017
1 parent fd001b5 commit bedaebb
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 10 deletions.
6 changes: 3 additions & 3 deletions src/v1/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ class Driver {
* @constructor
* @param {string} url
* @param {string} userAgent
* @param {Object} token
* @param {Object} config
* @access private
* @param {object} token
* @param {object} config
* @protected
*/
constructor(url, userAgent, token = {}, config = {}) {
this._url = url;
Expand Down
6 changes: 6 additions & 0 deletions src/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
* // will retry the given unit of work on `ServiceUnavailable`, `SessionExpired` and transient errors with
* // exponential backoff using initial delay of 1 second. Default value is 30000 which is 30 seconds.
* maxTransactionRetryTime: 30000,
*
* // Provide an alternative load balancing strategy for the routing driver to use.
* // Driver uses "least_connected" by default.
* // <b>Note:</b> We are experimenting with different strategies. This could be removed in the next minor
* // version.
* loadBalancingStrategy: "least_connected" | "round_robin",
* }
*
* @param {string} url The URL for the Neo4j database, for instance "bolt://localhost"
Expand Down
5 changes: 2 additions & 3 deletions src/v1/internal/connection-providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import Rediscovery from './rediscovery';
import hasFeature from './features';
import {DnsHostNameResolver, DummyHostNameResolver} from './host-name-resolvers';
import RoutingUtil from './routing-util';
import RoundRobinLoadBalancingStrategy from './round-robin-load-balancing-strategy';

class ConnectionProvider {

Expand Down Expand Up @@ -62,15 +61,15 @@ export class DirectConnectionProvider extends ConnectionProvider {

export class LoadBalancer extends ConnectionProvider {

constructor(address, routingContext, connectionPool, driverOnErrorCallback) {
constructor(address, routingContext, connectionPool, loadBalancingStrategy, driverOnErrorCallback) {
super();
this._seedRouter = address;
this._routingTable = new RoutingTable([this._seedRouter]);
this._rediscovery = new Rediscovery(new RoutingUtil(routingContext));
this._connectionPool = connectionPool;
this._driverOnErrorCallback = driverOnErrorCallback;
this._hostNameResolver = LoadBalancer._createHostNameResolver();
this._loadBalancingStrategy = new RoundRobinLoadBalancingStrategy();
this._loadBalancingStrategy = loadBalancingStrategy;
this._useSeedRouter = false;
}

Expand Down
85 changes: 85 additions & 0 deletions src/v1/internal/least-connected-load-balancing-strategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2002-2017 "Neo Technology,","
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import RoundRobinArrayIndex from './round-robin-array-index';
import LoadBalancingStrategy from './load-balancing-strategy';

export const LEAST_CONNECTED_STRATEGY_NAME = 'least_connected';

export default class LeastConnectedLoadBalancingStrategy extends LoadBalancingStrategy {

/**
* @constructor
* @param {Pool} connectionPool the connection pool of this driver.
*/
constructor(connectionPool) {
super();
this._readersIndex = new RoundRobinArrayIndex();
this._writersIndex = new RoundRobinArrayIndex();
this._connectionPool = connectionPool;
}

/**
* @inheritDoc
*/
selectReader(knownReaders) {
return this._select(knownReaders, this._readersIndex);
}

/**
* @inheritDoc
*/
selectWriter(knownWriters) {
return this._select(knownWriters, this._writersIndex);
}

_select(addresses, roundRobinIndex) {
const length = addresses.length;
if (length === 0) {
return null;
}

// choose start index for iteration in round-rodin fashion
const startIndex = roundRobinIndex.next(length);
let index = startIndex;

let leastConnectedAddress = null;
let leastActiveConnections = Number.MAX_SAFE_INTEGER;

// iterate over the array to find least connected address
do {
const address = addresses[index];
const activeConnections = this._connectionPool.activeResourceCount(address);

if (activeConnections < leastActiveConnections) {
leastConnectedAddress = address;
leastActiveConnections = activeConnections;
}

// loop over to the start of the array when end is reached
if (index === length - 1) {
index = 0;
} else {
index++;
}
}
while (index !== startIndex);

return leastConnectedAddress;
}
}
1 change: 0 additions & 1 deletion src/v1/internal/load-balancing-strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
* limitations under the License.
*/


/**
* A facility to select most appropriate reader or writer among the given addresses for request processing.
*/
Expand Down
3 changes: 3 additions & 0 deletions src/v1/internal/round-robin-load-balancing-strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import RoundRobinArrayIndex from './round-robin-array-index';
import LoadBalancingStrategy from './load-balancing-strategy';

export const ROUND_ROBIN_STRATEGY_NAME = 'round_robin';

export default class RoundRobinLoadBalancingStrategy extends LoadBalancingStrategy {

constructor() {
Expand Down
22 changes: 21 additions & 1 deletion src/v1/routing-driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import Session from './session';
import {Driver} from './driver';
import {newError, SESSION_EXPIRED} from './error';
import {LoadBalancer} from './internal/connection-providers';
import LeastConnectedLoadBalancingStrategy, {LEAST_CONNECTED_STRATEGY_NAME} from './internal/least-connected-load-balancing-strategy';
import RoundRobinLoadBalancingStrategy, {ROUND_ROBIN_STRATEGY_NAME} from './internal/round-robin-load-balancing-strategy';

/**
* A driver that supports routing in a core-edge cluster.
Expand All @@ -34,7 +36,8 @@ class RoutingDriver extends Driver {
}

_createConnectionProvider(address, connectionPool, driverOnErrorCallback) {
return new LoadBalancer(address, this._routingContext, connectionPool, driverOnErrorCallback);
const loadBalancingStrategy = RoutingDriver._createLoadBalancingStrategy(this._config, connectionPool);
return new LoadBalancer(address, this._routingContext, connectionPool, loadBalancingStrategy, driverOnErrorCallback);
}

_createSession(mode, connectionProvider, bookmark, config) {
Expand Down Expand Up @@ -80,6 +83,23 @@ class RoutingDriver extends Driver {
return error.code === 'Neo.ClientError.Cluster.NotALeader' ||
error.code === 'Neo.ClientError.General.ForbiddenOnReadOnlyDatabase';
}

/**
* Create new load balancing strategy based on the config.
* @param {object} config the user provided config.
* @param {Pool} connectionPool the connection pool for this driver.
* @return {LoadBalancingStrategy} new strategy.
*/
static _createLoadBalancingStrategy(config, connectionPool) {
const configuredValue = config.loadBalancingStrategy;
if (!configuredValue || configuredValue === LEAST_CONNECTED_STRATEGY_NAME) {
return new LeastConnectedLoadBalancingStrategy(connectionPool);
} else if (configuredValue === ROUND_ROBIN_STRATEGY_NAME) {
return new RoundRobinLoadBalancingStrategy();
} else {
throw newError('Unknown load balancing strategy: ' + configuredValue);
}
}
}

class RoutingSession extends Session {
Expand Down
9 changes: 7 additions & 2 deletions test/internal/connection-providers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {SERVICE_UNAVAILABLE, SESSION_EXPIRED} from '../../src/v1/error';
import RoutingTable from '../../src/v1/internal/routing-table';
import {DirectConnectionProvider, LoadBalancer} from '../../src/v1/internal/connection-providers';
import Pool from '../../src/v1/internal/pool';
import LeastConnectedLoadBalancingStrategy from '../../src/v1/internal/least-connected-load-balancing-strategy';

const NO_OP_DRIVER_CALLBACK = () => {
};
Expand Down Expand Up @@ -134,7 +135,9 @@ describe('LoadBalancer', () => {
});

it('initializes routing table with the given router', () => {
const loadBalancer = new LoadBalancer('server-ABC', {}, newPool(), NO_OP_DRIVER_CALLBACK);
const connectionPool = newPool();
const loadBalancingStrategy = new LeastConnectedLoadBalancingStrategy(connectionPool);
const loadBalancer = new LoadBalancer('server-ABC', {}, connectionPool, loadBalancingStrategy, NO_OP_DRIVER_CALLBACK);

expectRoutingTable(loadBalancer,
['server-ABC'],
Expand Down Expand Up @@ -1068,7 +1071,9 @@ function newLoadBalancerWithSeedRouter(seedRouter, seedRouterResolved,
expirationTime = Integer.MAX_VALUE,
routerToRoutingTable = {},
connectionPool = null) {
const loadBalancer = new LoadBalancer(seedRouter, {}, connectionPool || newPool(), NO_OP_DRIVER_CALLBACK);
const pool = connectionPool || newPool();
const loadBalancingStrategy = new LeastConnectedLoadBalancingStrategy(pool);
const loadBalancer = new LoadBalancer(seedRouter, {}, pool, loadBalancingStrategy, NO_OP_DRIVER_CALLBACK);
loadBalancer._routingTable = new RoutingTable(routers, readers, writers, expirationTime);
loadBalancer._rediscovery = new FakeRediscovery(routerToRoutingTable);
loadBalancer._hostNameResolver = new FakeDnsResolver(seedRouterResolved);
Expand Down
135 changes: 135 additions & 0 deletions test/internal/least-connected-load-balancing-strategy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* Copyright (c) 2002-2017 "Neo Technology,","
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import LeastConnectedLoadBalancingStrategy from '../../src/v1/internal/least-connected-load-balancing-strategy';
import Pool from '../../src/v1/internal/pool';

describe('LeastConnectedLoadBalancingStrategy', () => {

it('should return null when no readers', () => {
const knownReaders = [];
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({}));

expect(strategy.selectReader(knownReaders)).toBeNull();
});

it('should return null when no writers', () => {
const knownWriters = [];
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({}));

expect(strategy.selectWriter(knownWriters)).toBeNull();
});

it('should return same reader when it is the only one available and has no connections', () => {
const knownReaders = ['reader-1'];
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({'reader-1': 0}));

expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
});

it('should return same writer when it is the only one available and has no connections', () => {
const knownWriters = ['writer-1'];
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({'writer-1': 0}));

expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
});

it('should return same reader when it is the only one available and has active connections', () => {
const knownReaders = ['reader-1'];
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({'reader-1': 14}));

expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
});

it('should return same writer when it is the only one available and has active connections', () => {
const knownWriters = ['writer-1'];
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({'writer-1': 3}));

expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
});

it('should return readers in round robin order when no active connections', () => {
const knownReaders = ['reader-1', 'reader-2', 'reader-3'];
const pool = new DummyPool({'reader-1': 0, 'reader-2': 0, 'reader-3': 0});
const strategy = new LeastConnectedLoadBalancingStrategy(pool);

expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
expect(strategy.selectReader(knownReaders)).toEqual('reader-3');
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
expect(strategy.selectReader(knownReaders)).toEqual('reader-3');
});

it('should return writers in round robin order when no active connections', () => {
const knownWriters = ['writer-1', 'writer-2', 'writer-3', 'writer-4'];
const pool = new DummyPool({'writer-1': 0, 'writer-2': 0, 'writer-3': 0, 'writer-4': 0});
const strategy = new LeastConnectedLoadBalancingStrategy(pool);

expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-2');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-3');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-2');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-3');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
});

it('should return least connected reader', () => {
const knownReaders = ['reader-1', 'reader-2', 'reader-3'];
const pool = new DummyPool({'reader-1': 7, 'reader-2': 3, 'reader-3': 8});
const strategy = new LeastConnectedLoadBalancingStrategy(pool);

expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
});

it('should return least connected writer', () => {
const knownWriters = ['writer-1', 'writer-2', 'writer-3', 'writer-4'];
const pool = new DummyPool({'writer-1': 5, 'writer-2': 4, 'writer-3': 6, 'writer-4': 2});
const strategy = new LeastConnectedLoadBalancingStrategy(pool);

expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
});

});

class DummyPool extends Pool {

constructor(activeConnections) {
super(() => 42);
this._activeConnections = activeConnections;
}

activeResourceCount(key) {
return this._activeConnections[key];
}
}
Loading

0 comments on commit bedaebb

Please sign in to comment.