From fd001b5eb8e922204ca2a42911e742357ff4f4e7 Mon Sep 17 00:00:00 2001 From: lutovich Date: Mon, 10 Jul 2017 18:54:18 +0200 Subject: [PATCH 1/4] Made pool expose active resource count It will now track count of active (checked out of the pool) resources per key. Where key is a server address in the driver. Counter is incremented when resource is acquired from the pool and decremented when resource is released back to the pool. Also added JSDocs in the `Pool` class. --- src/v1/internal/pool.js | 84 ++++++++++++++++++++++++++++++++------ test/internal/pool.test.js | 80 ++++++++++++++++++++++++++++++++++++ test/v1/session.test.js | 69 +++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 13 deletions(-) diff --git a/src/v1/internal/pool.js b/src/v1/internal/pool.js index 3d484e38b..9857cb78f 100644 --- a/src/v1/internal/pool.js +++ b/src/v1/internal/pool.js @@ -36,59 +36,117 @@ class Pool { this._validate = validate; this._maxIdle = maxIdle; this._pools = {}; + this._activeResourceCounts = {}; this._release = this._release.bind(this); } + /** + * Acquire and idle resource fom the pool or create a new one. + * @param {string} key the resource key. + * @return {object} resource that is ready to use. + */ acquire(key) { - let resource; let pool = this._pools[key]; if (!pool) { pool = []; this._pools[key] = pool; } while (pool.length) { - resource = pool.pop(); + const resource = pool.pop(); if (this._validate(resource)) { + // idle resource is valid and can be acquired + resourceAcquired(key, this._activeResourceCounts); return resource; } else { this._destroy(resource); } } + // there exist no idle valid resources, create a new one for acquisition + resourceAcquired(key, this._activeResourceCounts); return this._create(key, this._release); } + /** + * Destroy all idle resources for the given key. + * @param {string} key the resource key to purge. + */ purge(key) { - let resource; - let pool = this._pools[key] || []; + const pool = this._pools[key] || []; while (pool.length) { - resource = pool.pop(); + const resource = pool.pop(); this._destroy(resource) } delete this._pools[key] } + /** + * Destroy all idle resources in this pool. + */ purgeAll() { Object.keys(this._pools).forEach(key => this.purge(key)); } + /** + * Check if this pool contains resources for the given key. + * @param {string} key the resource key to check. + * @return {boolean} true when pool contains entries for the given key, false otherwise. + */ has(key) { return (key in this._pools); } + /** + * Get count of active (checked out of the pool) resources for the given key. + * @param {string} key the resource key to check. + * @return {number} count of resources acquired by clients. + */ + activeResourceCount(key) { + return this._activeResourceCounts[key] || 0; + } + _release(key, resource) { - let pool = this._pools[key]; - if (!pool) { + const pool = this._pools[key]; + + if (pool) { + // there exist idle connections for the given key + if (pool.length >= this._maxIdle || !this._validate(resource)) { + this._destroy(resource); + } else { + pool.push(resource); + } + } else { // key has been purged, don't put it back, just destroy the resource this._destroy(resource); - return; - } - if( pool.length >= this._maxIdle || !this._validate(resource) ) { - this._destroy(resource); - } else { - pool.push(resource); } + + resourceReleased(key, this._activeResourceCounts); + } +} + +/** + * Increment active (checked out of the pool) resource counter. + * @param {string} key the resource group identifier (server address for connections). + * @param {Object.} activeResourceCounts the object holding active counts per key. + */ +function resourceAcquired(key, activeResourceCounts) { + const currentCount = activeResourceCounts[key] || 0; + activeResourceCounts[key] = currentCount + 1; +} + +/** + * Decrement active (checked out of the pool) resource counter. + * @param {string} key the resource group identifier (server address for connections). + * @param {Object.} activeResourceCounts the object holding active counts per key. + */ +function resourceReleased(key, activeResourceCounts) { + const currentCount = activeResourceCounts[key] || 0; + const nextCount = currentCount - 1; + if (nextCount > 0) { + activeResourceCounts[key] = nextCount; + } else { + delete activeResourceCounts[key]; } } diff --git a/test/internal/pool.test.js b/test/internal/pool.test.js index 634c8d830..04e54e010 100644 --- a/test/internal/pool.test.js +++ b/test/internal/pool.test.js @@ -255,6 +255,86 @@ describe('Pool', () => { expect(pool.has(existingKey)).toBeTruthy(); expect(pool.has(absentKey)).toBeFalsy(); }); + + it('reports zero active resources when empty', () => { + const pool = new Pool((url, release) => new Resource(url, 42, release)); + + expect(pool.activeResourceCount('bolt://localhost:1')).toEqual(0); + expect(pool.activeResourceCount('bolt://localhost:2')).toEqual(0); + expect(pool.activeResourceCount('bolt://localhost:3')).toEqual(0); + }); + + it('reports active resources', () => { + const key = 'bolt://localhost:7687'; + const pool = new Pool((url, release) => new Resource(url, 42, release)); + + expect(pool.acquire(key)).toBeDefined(); + expect(pool.acquire(key)).toBeDefined(); + expect(pool.acquire(key)).toBeDefined(); + + expect(pool.activeResourceCount(key)).toEqual(3); + }); + + it('reports active resources when they are created', () => { + const key = 'bolt://localhost:7687'; + const pool = new Pool((url, release) => new Resource(url, 42, release)); + + // three new resources are created + expect(pool.acquire(key)).toBeDefined(); + expect(pool.acquire(key)).toBeDefined(); + expect(pool.acquire(key)).toBeDefined(); + + expect(pool.activeResourceCount(key)).toEqual(3); + }); + + it('reports active resources when they are acquired', () => { + const key = 'bolt://localhost:7687'; + const pool = new Pool((url, release) => new Resource(url, 42, release)); + + // three new resources are created and returned to the pool + const r0 = pool.acquire(key); + const r1 = pool.acquire(key); + const r2 = pool.acquire(key); + r0.close(); + r1.close(); + r2.close(); + + // three idle resources are acquired from the pool + const r3 = pool.acquire(key); + const r4 = pool.acquire(key); + const r5 = pool.acquire(key); + expect(r3).toBe(r2); + expect(r4).toBe(r1); + expect(r5).toBe(r0); + + expect(pool.activeResourceCount(key)).toEqual(3); + }); + + it('does not report resources that are returned to the pool', () => { + const key = 'bolt://localhost:7687'; + const pool = new Pool((url, release) => new Resource(url, 42, release)); + + const r0 = pool.acquire(key); + const r1 = pool.acquire(key); + const r2 = pool.acquire(key); + expect(pool.activeResourceCount(key)).toEqual(3); + + r0.close(); + expect(pool.activeResourceCount(key)).toEqual(2); + + r1.close(); + expect(pool.activeResourceCount(key)).toEqual(1); + + r2.close(); + expect(pool.activeResourceCount(key)).toEqual(0); + + const r3 = pool.acquire(key); + expect(pool.activeResourceCount(key)).toEqual(1); + + r3.close(); + expect(pool.activeResourceCount(key)).toEqual(0); + }); + }); class Resource { diff --git a/test/v1/session.test.js b/test/v1/session.test.js index daf6a0dc2..af08ef05b 100644 --- a/test/v1/session.test.js +++ b/test/v1/session.test.js @@ -965,6 +965,70 @@ describe('session', () => { }); }); + it('should acquire connection for transaction', done => { + expect(session.beginTransaction()).toBeDefined(); + + const otherSession1 = driver.session(); + expect(otherSession1.beginTransaction()).toBeDefined(); + + const otherSession2 = driver.session(); + expect(otherSession2.beginTransaction()).toBeDefined(); + + const otherSession3 = driver.session(); + expect(otherSession3.beginTransaction()).toBeDefined(); + + expect(numberOfAcquiredConnectionsFromPool()).toEqual(4); + + session.close(() => { + otherSession1.close(() => { + otherSession2.close(() => { + otherSession3.close(() => { + done(); + }); + }); + }); + }); + }); + + it('should acquire connection for query execution', done => { + session.run('RETURN 42 AS answer').subscribe({ + onNext: record => { + expect(record.get('answer').toInt()).toEqual(42); + expect(numberOfAcquiredConnectionsFromPool()).toEqual(1); + }, + onCompleted: () => { + session.close(() => { + done(); + }); + }, + onError: error => { + console.log(error); + } + }); + }); + + it('should acquire separate connections for transaction and query execution in different sessions', done => { + const otherSession = driver.session(); + expect(otherSession.beginTransaction()).toBeDefined(); + + session.run('RETURN 42 AS answer').subscribe({ + onNext: record => { + expect(record.get('answer').toInt()).toEqual(42); + expect(numberOfAcquiredConnectionsFromPool()).toEqual(2); + }, + onCompleted: () => { + otherSession.close(() => { + session.close(() => { + done(); + }); + }); + }, + onError: error => { + console.log(error); + } + }); + }); + function serverIs31OrLater(done) { // lazy way of checking the version number // if server has been set we know it is at least 3.1 @@ -1043,4 +1107,9 @@ describe('session', () => { }); } + function numberOfAcquiredConnectionsFromPool() { + const pool = driver._pool; + return pool.activeResourceCount('localhost'); + } + }); From bedaebb53bc9a4b9a029f180c603e24363a61ebf Mon Sep 17 00:00:00 2001 From: lutovich Date: Tue, 11 Jul 2017 09:57:39 +0200 Subject: [PATCH 2/4] Added least connected load balancing strategy 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'} ``` --- src/v1/driver.js | 6 +- src/v1/index.js | 6 + src/v1/internal/connection-providers.js | 5 +- ...least-connected-load-balancing-strategy.js | 85 +++++++++++ src/v1/internal/load-balancing-strategy.js | 1 - .../round-robin-load-balancing-strategy.js | 3 + src/v1/routing-driver.js | 22 ++- test/internal/connection-providers.test.js | 9 +- ...-connected-load-balancing-strategy.test.js | 135 ++++++++++++++++++ test/v1/routing-driver.test.js | 42 ++++++ 10 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 src/v1/internal/least-connected-load-balancing-strategy.js create mode 100644 test/internal/least-connected-load-balancing-strategy.test.js create mode 100644 test/v1/routing-driver.test.js 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(); + }); + +}); From c7281ce4d915a28ac3fd49a278f11a77e8b820e5 Mon Sep 17 00:00:00 2001 From: lutovich Date: Tue, 11 Jul 2017 18:20:43 +0200 Subject: [PATCH 3/4] Add TypeScript declaration for LB strategy --- test/types/v1/driver.test.ts | 3 +++ types/v1/driver.d.ts | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/test/types/v1/driver.test.ts b/test/types/v1/driver.test.ts index 1a05f15b6..c4b379760 100644 --- a/test/types/v1/driver.test.ts +++ b/test/types/v1/driver.test.ts @@ -21,6 +21,7 @@ import Driver, { AuthToken, Config, EncryptionLevel, + LoadBalancingStrategy, READ, SessionMode, TrustStrategy, @@ -54,6 +55,8 @@ const trustedCertificates: undefined | string[] = config.trustedCertificates; const knownHosts: undefined | string = config.knownHosts; const connectionPoolSize: undefined | number = config.connectionPoolSize; const maxTransactionRetryTime: undefined | number = config.maxTransactionRetryTime; +const loadBalancingStrategy1: undefined | LoadBalancingStrategy = config.loadBalancingStrategy; +const loadBalancingStrategy2: undefined | string = config.loadBalancingStrategy; const sessionMode: SessionMode = dummy; const sessionModeStr: string = sessionMode; diff --git a/types/v1/driver.d.ts b/types/v1/driver.d.ts index c669502f3..3bbc59fe4 100644 --- a/types/v1/driver.d.ts +++ b/types/v1/driver.d.ts @@ -36,6 +36,8 @@ declare type TrustStrategy = "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES" | "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES"; +declare type LoadBalancingStrategy = "least_connected" | "round_robin"; + declare interface Config { encrypted?: boolean | EncryptionLevel; trust?: TrustStrategy; @@ -43,6 +45,7 @@ declare interface Config { knownHosts?: string; connectionPoolSize?: number; maxTransactionRetryTime?: number; + loadBalancingStrategy?: LoadBalancingStrategy; } declare type SessionMode = "READ" | "WRITE"; @@ -56,6 +59,6 @@ declare interface Driver { close(): void; } -export {Driver, READ, WRITE, AuthToken, Config, EncryptionLevel, TrustStrategy, SessionMode} +export {Driver, READ, WRITE, AuthToken, Config, EncryptionLevel, TrustStrategy, LoadBalancingStrategy, SessionMode} export default Driver; From bcf95d1f7a0e356a998580d0f9c1e83c4158f184 Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 13 Jul 2017 12:27:18 +0200 Subject: [PATCH 4/4] Add test for wrong LB strategy configuration --- test/v1/routing-driver.test.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/v1/routing-driver.test.js b/test/v1/routing-driver.test.js index c8c7ac771..02ed07919 100644 --- a/test/v1/routing-driver.test.js +++ b/test/v1/routing-driver.test.js @@ -25,18 +25,26 @@ import Pool from '../../src/v1/internal/pool'; describe('RoutingDriver', () => { it('should create least connected when nothing configured', () => { - const strategy = RoutingDriver._createLoadBalancingStrategy({}, new Pool()); + const strategy = createStrategy({}); expect(strategy instanceof LeastConnectedLoadBalancingStrategy).toBeTruthy(); }); it('should create least connected when it is configured', () => { - const strategy = RoutingDriver._createLoadBalancingStrategy({loadBalancingStrategy: 'least_connected'}, new Pool()); + const strategy = createStrategy({loadBalancingStrategy: 'least_connected'}); expect(strategy instanceof LeastConnectedLoadBalancingStrategy).toBeTruthy(); }); it('should create round robin when it is configured', () => { - const strategy = RoutingDriver._createLoadBalancingStrategy({loadBalancingStrategy: 'round_robin'}, new Pool()); + const strategy = createStrategy({loadBalancingStrategy: 'round_robin'}); expect(strategy instanceof RoundRobinLoadBalancingStrategy).toBeTruthy(); }); + it('should fail when unknown strategy is configured', () => { + expect(() => createStrategy({loadBalancingStrategy: 'wrong'})).toThrow(); + }); + }); + +function createStrategy(config) { + return RoutingDriver._createLoadBalancingStrategy(config, new Pool()); +}