diff --git a/src/v1/driver.js b/src/v1/driver.js index 771120761..165a8ee7a 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -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; diff --git a/src/v1/index.js b/src/v1/index.js index 948259443..25a36f55e 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -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. + * // Note: 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" diff --git a/src/v1/internal/connection-providers.js b/src/v1/internal/connection-providers.js index 3a57c0624..7f1c4e32a 100644 --- a/src/v1/internal/connection-providers.js +++ b/src/v1/internal/connection-providers.js @@ -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 { @@ -62,7 +61,7 @@ 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]); @@ -70,7 +69,7 @@ export class LoadBalancer extends ConnectionProvider { this._connectionPool = connectionPool; this._driverOnErrorCallback = driverOnErrorCallback; this._hostNameResolver = LoadBalancer._createHostNameResolver(); - this._loadBalancingStrategy = new RoundRobinLoadBalancingStrategy(); + this._loadBalancingStrategy = loadBalancingStrategy; this._useSeedRouter = false; } diff --git a/src/v1/internal/least-connected-load-balancing-strategy.js b/src/v1/internal/least-connected-load-balancing-strategy.js new file mode 100644 index 000000000..ec6b84d28 --- /dev/null +++ b/src/v1/internal/least-connected-load-balancing-strategy.js @@ -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; + } +} diff --git a/src/v1/internal/load-balancing-strategy.js b/src/v1/internal/load-balancing-strategy.js index a04f9ea17..6d5c7eb52 100644 --- a/src/v1/internal/load-balancing-strategy.js +++ b/src/v1/internal/load-balancing-strategy.js @@ -17,7 +17,6 @@ * limitations under the License. */ - /** * A facility to select most appropriate reader or writer among the given addresses for request processing. */ diff --git a/src/v1/internal/round-robin-load-balancing-strategy.js b/src/v1/internal/round-robin-load-balancing-strategy.js index 011a4f9b4..32ac27590 100644 --- a/src/v1/internal/round-robin-load-balancing-strategy.js +++ b/src/v1/internal/round-robin-load-balancing-strategy.js @@ -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() { diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js index 7e5f2ff23..859f6bea9 100644 --- a/src/v1/routing-driver.js +++ b/src/v1/routing-driver.js @@ -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. @@ -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) { @@ -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 { diff --git a/test/internal/connection-providers.test.js b/test/internal/connection-providers.test.js index 8fe606c6b..51d45d43f 100644 --- a/test/internal/connection-providers.test.js +++ b/test/internal/connection-providers.test.js @@ -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 = () => { }; @@ -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'], @@ -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); diff --git a/test/internal/least-connected-load-balancing-strategy.test.js b/test/internal/least-connected-load-balancing-strategy.test.js new file mode 100644 index 000000000..17cf024a4 --- /dev/null +++ b/test/internal/least-connected-load-balancing-strategy.test.js @@ -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]; + } +} diff --git a/test/v1/routing-driver.test.js b/test/v1/routing-driver.test.js new file mode 100644 index 000000000..c8c7ac771 --- /dev/null +++ b/test/v1/routing-driver.test.js @@ -0,0 +1,42 @@ +/** + * 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 RoundRobinLoadBalancingStrategy from '../../src/v1/internal/round-robin-load-balancing-strategy'; +import LeastConnectedLoadBalancingStrategy from '../../src/v1/internal/least-connected-load-balancing-strategy'; +import RoutingDriver from '../../src/v1/routing-driver'; +import Pool from '../../src/v1/internal/pool'; + +describe('RoutingDriver', () => { + + it('should create least connected when nothing configured', () => { + const strategy = RoutingDriver._createLoadBalancingStrategy({}, new Pool()); + expect(strategy instanceof LeastConnectedLoadBalancingStrategy).toBeTruthy(); + }); + + it('should create least connected when it is configured', () => { + const strategy = RoutingDriver._createLoadBalancingStrategy({loadBalancingStrategy: 'least_connected'}, new Pool()); + expect(strategy instanceof LeastConnectedLoadBalancingStrategy).toBeTruthy(); + }); + + it('should create round robin when it is configured', () => { + const strategy = RoutingDriver._createLoadBalancingStrategy({loadBalancingStrategy: 'round_robin'}, new Pool()); + expect(strategy instanceof RoundRobinLoadBalancingStrategy).toBeTruthy(); + }); + +});