Skip to content

Commit ec52f96

Browse files
committed
Added least connected LB strategy
1 parent 184f480 commit ec52f96

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)