Skip to content

Commit bedaebb

Browse files
committed
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'} ```
1 parent fd001b5 commit bedaebb

10 files changed

+304
-10
lines changed

src/v1/driver.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ class Driver {
4242
* @constructor
4343
* @param {string} url
4444
* @param {string} userAgent
45-
* @param {Object} token
46-
* @param {Object} config
47-
* @access private
45+
* @param {object} token
46+
* @param {object} config
47+
* @protected
4848
*/
4949
constructor(url, userAgent, token = {}, config = {}) {
5050
this._url = url;

src/v1/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
117117
* // will retry the given unit of work on `ServiceUnavailable`, `SessionExpired` and transient errors with
118118
* // exponential backoff using initial delay of 1 second. Default value is 30000 which is 30 seconds.
119119
* maxTransactionRetryTime: 30000,
120+
*
121+
* // Provide an alternative load balancing strategy for the routing driver to use.
122+
* // Driver uses "least_connected" by default.
123+
* // <b>Note:</b> We are experimenting with different strategies. This could be removed in the next minor
124+
* // version.
125+
* loadBalancingStrategy: "least_connected" | "round_robin",
120126
* }
121127
*
122128
* @param {string} url The URL for the Neo4j database, for instance "bolt://localhost"

src/v1/internal/connection-providers.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import Rediscovery from './rediscovery';
2525
import hasFeature from './features';
2626
import {DnsHostNameResolver, DummyHostNameResolver} from './host-name-resolvers';
2727
import RoutingUtil from './routing-util';
28-
import RoundRobinLoadBalancingStrategy from './round-robin-load-balancing-strategy';
2928

3029
class ConnectionProvider {
3130

@@ -62,15 +61,15 @@ export class DirectConnectionProvider extends ConnectionProvider {
6261

6362
export class LoadBalancer extends ConnectionProvider {
6463

65-
constructor(address, routingContext, connectionPool, driverOnErrorCallback) {
64+
constructor(address, routingContext, connectionPool, loadBalancingStrategy, driverOnErrorCallback) {
6665
super();
6766
this._seedRouter = address;
6867
this._routingTable = new RoutingTable([this._seedRouter]);
6968
this._rediscovery = new Rediscovery(new RoutingUtil(routingContext));
7069
this._connectionPool = connectionPool;
7170
this._driverOnErrorCallback = driverOnErrorCallback;
7271
this._hostNameResolver = LoadBalancer._createHostNameResolver();
73-
this._loadBalancingStrategy = new RoundRobinLoadBalancingStrategy();
72+
this._loadBalancingStrategy = loadBalancingStrategy;
7473
this._useSeedRouter = false;
7574
}
7675

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright (c) 2002-2017 "Neo Technology,","
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
import RoundRobinArrayIndex from './round-robin-array-index';
20+
import LoadBalancingStrategy from './load-balancing-strategy';
21+
22+
export const LEAST_CONNECTED_STRATEGY_NAME = 'least_connected';
23+
24+
export default class LeastConnectedLoadBalancingStrategy extends LoadBalancingStrategy {
25+
26+
/**
27+
* @constructor
28+
* @param {Pool} connectionPool the connection pool of this driver.
29+
*/
30+
constructor(connectionPool) {
31+
super();
32+
this._readersIndex = new RoundRobinArrayIndex();
33+
this._writersIndex = new RoundRobinArrayIndex();
34+
this._connectionPool = connectionPool;
35+
}
36+
37+
/**
38+
* @inheritDoc
39+
*/
40+
selectReader(knownReaders) {
41+
return this._select(knownReaders, this._readersIndex);
42+
}
43+
44+
/**
45+
* @inheritDoc
46+
*/
47+
selectWriter(knownWriters) {
48+
return this._select(knownWriters, this._writersIndex);
49+
}
50+
51+
_select(addresses, roundRobinIndex) {
52+
const length = addresses.length;
53+
if (length === 0) {
54+
return null;
55+
}
56+
57+
// choose start index for iteration in round-rodin fashion
58+
const startIndex = roundRobinIndex.next(length);
59+
let index = startIndex;
60+
61+
let leastConnectedAddress = null;
62+
let leastActiveConnections = Number.MAX_SAFE_INTEGER;
63+
64+
// iterate over the array to find least connected address
65+
do {
66+
const address = addresses[index];
67+
const activeConnections = this._connectionPool.activeResourceCount(address);
68+
69+
if (activeConnections < leastActiveConnections) {
70+
leastConnectedAddress = address;
71+
leastActiveConnections = activeConnections;
72+
}
73+
74+
// loop over to the start of the array when end is reached
75+
if (index === length - 1) {
76+
index = 0;
77+
} else {
78+
index++;
79+
}
80+
}
81+
while (index !== startIndex);
82+
83+
return leastConnectedAddress;
84+
}
85+
}

src/v1/internal/load-balancing-strategy.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
* limitations under the License.
1818
*/
1919

20-
2120
/**
2221
* A facility to select most appropriate reader or writer among the given addresses for request processing.
2322
*/

src/v1/internal/round-robin-load-balancing-strategy.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19+
1920
import RoundRobinArrayIndex from './round-robin-array-index';
2021
import LoadBalancingStrategy from './load-balancing-strategy';
2122

23+
export const ROUND_ROBIN_STRATEGY_NAME = 'round_robin';
24+
2225
export default class RoundRobinLoadBalancingStrategy extends LoadBalancingStrategy {
2326

2427
constructor() {

src/v1/routing-driver.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import Session from './session';
2121
import {Driver} from './driver';
2222
import {newError, SESSION_EXPIRED} from './error';
2323
import {LoadBalancer} from './internal/connection-providers';
24+
import LeastConnectedLoadBalancingStrategy, {LEAST_CONNECTED_STRATEGY_NAME} from './internal/least-connected-load-balancing-strategy';
25+
import RoundRobinLoadBalancingStrategy, {ROUND_ROBIN_STRATEGY_NAME} from './internal/round-robin-load-balancing-strategy';
2426

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

3638
_createConnectionProvider(address, connectionPool, driverOnErrorCallback) {
37-
return new LoadBalancer(address, this._routingContext, connectionPool, driverOnErrorCallback);
39+
const loadBalancingStrategy = RoutingDriver._createLoadBalancingStrategy(this._config, connectionPool);
40+
return new LoadBalancer(address, this._routingContext, connectionPool, loadBalancingStrategy, driverOnErrorCallback);
3841
}
3942

4043
_createSession(mode, connectionProvider, bookmark, config) {
@@ -80,6 +83,23 @@ class RoutingDriver extends Driver {
8083
return error.code === 'Neo.ClientError.Cluster.NotALeader' ||
8184
error.code === 'Neo.ClientError.General.ForbiddenOnReadOnlyDatabase';
8285
}
86+
87+
/**
88+
* Create new load balancing strategy based on the config.
89+
* @param {object} config the user provided config.
90+
* @param {Pool} connectionPool the connection pool for this driver.
91+
* @return {LoadBalancingStrategy} new strategy.
92+
*/
93+
static _createLoadBalancingStrategy(config, connectionPool) {
94+
const configuredValue = config.loadBalancingStrategy;
95+
if (!configuredValue || configuredValue === LEAST_CONNECTED_STRATEGY_NAME) {
96+
return new LeastConnectedLoadBalancingStrategy(connectionPool);
97+
} else if (configuredValue === ROUND_ROBIN_STRATEGY_NAME) {
98+
return new RoundRobinLoadBalancingStrategy();
99+
} else {
100+
throw newError('Unknown load balancing strategy: ' + configuredValue);
101+
}
102+
}
83103
}
84104

85105
class RoutingSession extends Session {

test/internal/connection-providers.test.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {SERVICE_UNAVAILABLE, SESSION_EXPIRED} from '../../src/v1/error';
2323
import RoutingTable from '../../src/v1/internal/routing-table';
2424
import {DirectConnectionProvider, LoadBalancer} from '../../src/v1/internal/connection-providers';
2525
import Pool from '../../src/v1/internal/pool';
26+
import LeastConnectedLoadBalancingStrategy from '../../src/v1/internal/least-connected-load-balancing-strategy';
2627

2728
const NO_OP_DRIVER_CALLBACK = () => {
2829
};
@@ -134,7 +135,9 @@ describe('LoadBalancer', () => {
134135
});
135136

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

139142
expectRoutingTable(loadBalancer,
140143
['server-ABC'],
@@ -1068,7 +1071,9 @@ function newLoadBalancerWithSeedRouter(seedRouter, seedRouterResolved,
10681071
expirationTime = Integer.MAX_VALUE,
10691072
routerToRoutingTable = {},
10701073
connectionPool = null) {
1071-
const loadBalancer = new LoadBalancer(seedRouter, {}, connectionPool || newPool(), NO_OP_DRIVER_CALLBACK);
1074+
const pool = connectionPool || newPool();
1075+
const loadBalancingStrategy = new LeastConnectedLoadBalancingStrategy(pool);
1076+
const loadBalancer = new LoadBalancer(seedRouter, {}, pool, loadBalancingStrategy, NO_OP_DRIVER_CALLBACK);
10721077
loadBalancer._routingTable = new RoutingTable(routers, readers, writers, expirationTime);
10731078
loadBalancer._rediscovery = new FakeRediscovery(routerToRoutingTable);
10741079
loadBalancer._hostNameResolver = new FakeDnsResolver(seedRouterResolved);
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* Copyright (c) 2002-2017 "Neo Technology,","
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import LeastConnectedLoadBalancingStrategy from '../../src/v1/internal/least-connected-load-balancing-strategy';
21+
import Pool from '../../src/v1/internal/pool';
22+
23+
describe('LeastConnectedLoadBalancingStrategy', () => {
24+
25+
it('should return null when no readers', () => {
26+
const knownReaders = [];
27+
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({}));
28+
29+
expect(strategy.selectReader(knownReaders)).toBeNull();
30+
});
31+
32+
it('should return null when no writers', () => {
33+
const knownWriters = [];
34+
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({}));
35+
36+
expect(strategy.selectWriter(knownWriters)).toBeNull();
37+
});
38+
39+
it('should return same reader when it is the only one available and has no connections', () => {
40+
const knownReaders = ['reader-1'];
41+
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({'reader-1': 0}));
42+
43+
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
44+
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
45+
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
46+
});
47+
48+
it('should return same writer when it is the only one available and has no connections', () => {
49+
const knownWriters = ['writer-1'];
50+
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({'writer-1': 0}));
51+
52+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
53+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
54+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
55+
});
56+
57+
it('should return same reader when it is the only one available and has active connections', () => {
58+
const knownReaders = ['reader-1'];
59+
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({'reader-1': 14}));
60+
61+
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
62+
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
63+
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
64+
});
65+
66+
it('should return same writer when it is the only one available and has active connections', () => {
67+
const knownWriters = ['writer-1'];
68+
const strategy = new LeastConnectedLoadBalancingStrategy(new DummyPool({'writer-1': 3}));
69+
70+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
71+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
72+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
73+
});
74+
75+
it('should return readers in round robin order when no active connections', () => {
76+
const knownReaders = ['reader-1', 'reader-2', 'reader-3'];
77+
const pool = new DummyPool({'reader-1': 0, 'reader-2': 0, 'reader-3': 0});
78+
const strategy = new LeastConnectedLoadBalancingStrategy(pool);
79+
80+
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
81+
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
82+
expect(strategy.selectReader(knownReaders)).toEqual('reader-3');
83+
expect(strategy.selectReader(knownReaders)).toEqual('reader-1');
84+
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
85+
expect(strategy.selectReader(knownReaders)).toEqual('reader-3');
86+
});
87+
88+
it('should return writers in round robin order when no active connections', () => {
89+
const knownWriters = ['writer-1', 'writer-2', 'writer-3', 'writer-4'];
90+
const pool = new DummyPool({'writer-1': 0, 'writer-2': 0, 'writer-3': 0, 'writer-4': 0});
91+
const strategy = new LeastConnectedLoadBalancingStrategy(pool);
92+
93+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
94+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-2');
95+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-3');
96+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
97+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-1');
98+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-2');
99+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-3');
100+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
101+
});
102+
103+
it('should return least connected reader', () => {
104+
const knownReaders = ['reader-1', 'reader-2', 'reader-3'];
105+
const pool = new DummyPool({'reader-1': 7, 'reader-2': 3, 'reader-3': 8});
106+
const strategy = new LeastConnectedLoadBalancingStrategy(pool);
107+
108+
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
109+
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
110+
expect(strategy.selectReader(knownReaders)).toEqual('reader-2');
111+
});
112+
113+
it('should return least connected writer', () => {
114+
const knownWriters = ['writer-1', 'writer-2', 'writer-3', 'writer-4'];
115+
const pool = new DummyPool({'writer-1': 5, 'writer-2': 4, 'writer-3': 6, 'writer-4': 2});
116+
const strategy = new LeastConnectedLoadBalancingStrategy(pool);
117+
118+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
119+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
120+
expect(strategy.selectWriter(knownWriters)).toEqual('writer-4');
121+
});
122+
123+
});
124+
125+
class DummyPool extends Pool {
126+
127+
constructor(activeConnections) {
128+
super(() => 42);
129+
this._activeConnections = activeConnections;
130+
}
131+
132+
activeResourceCount(key) {
133+
return this._activeConnections[key];
134+
}
135+
}

0 commit comments

Comments
 (0)