Skip to content

Commit

Permalink
Added least connected LB strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
lutovich committed Jul 11, 2017
1 parent 184f480 commit ec52f96
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 ec52f96

Please sign in to comment.