Skip to content

Commit 963aeae

Browse files
authored
Merge pull request #265 from lutovich/1.5-lb-infra
Extract load balancing logic in a separate class
2 parents 622e4ec + 094d25f commit 963aeae

17 files changed

+391
-368
lines changed

src/v1/internal/connection-providers.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from '../error';
2121
import {READ, WRITE} from '../driver';
2222
import Session from '../session';
23-
import RoundRobinArray from './round-robin-array';
2423
import RoutingTable from './routing-table';
2524
import Rediscovery from './rediscovery';
2625
import hasFeature from './features';
2726
import {DnsHostNameResolver, DummyHostNameResolver} from './host-name-resolvers';
2827
import RoutingUtil from './routing-util';
28+
import RoundRobinLoadBalancingStrategy from './round-robin-load-balancing-strategy';
2929

3030
class ConnectionProvider {
3131

@@ -65,20 +65,23 @@ export class LoadBalancer extends ConnectionProvider {
6565
constructor(address, routingContext, connectionPool, driverOnErrorCallback) {
6666
super();
6767
this._seedRouter = address;
68-
this._routingTable = new RoutingTable(new RoundRobinArray([this._seedRouter]));
68+
this._routingTable = new RoutingTable([this._seedRouter]);
6969
this._rediscovery = new Rediscovery(new RoutingUtil(routingContext));
7070
this._connectionPool = connectionPool;
7171
this._driverOnErrorCallback = driverOnErrorCallback;
7272
this._hostNameResolver = LoadBalancer._createHostNameResolver();
73+
this._loadBalancingStrategy = new RoundRobinLoadBalancingStrategy();
7374
this._useSeedRouter = false;
7475
}
7576

7677
acquireConnection(accessMode) {
7778
const connectionPromise = this._freshRoutingTable(accessMode).then(routingTable => {
7879
if (accessMode === READ) {
79-
return this._acquireConnectionToServer(routingTable.readers, 'read');
80+
const address = this._loadBalancingStrategy.selectReader(routingTable.readers);
81+
return this._acquireConnectionToServer(address, 'read');
8082
} else if (accessMode === WRITE) {
81-
return this._acquireConnectionToServer(routingTable.writers, 'write');
83+
const address = this._loadBalancingStrategy.selectWriter(routingTable.writers);
84+
return this._acquireConnectionToServer(address, 'write');
8285
} else {
8386
throw newError('Illegal mode ' + accessMode);
8487
}
@@ -95,8 +98,7 @@ export class LoadBalancer extends ConnectionProvider {
9598
this._routingTable.forgetWriter(address);
9699
}
97100

98-
_acquireConnectionToServer(serversRoundRobinArray, serverName) {
99-
const address = serversRoundRobinArray.next();
101+
_acquireConnectionToServer(address, serverName) {
100102
if (!address) {
101103
return Promise.reject(newError(
102104
`Failed to obtain connection towards ${serverName} server. Known routing table is: ${this._routingTable}`,
@@ -115,7 +117,7 @@ export class LoadBalancer extends ConnectionProvider {
115117
}
116118

117119
_refreshRoutingTable(currentRoutingTable) {
118-
const knownRouters = currentRoutingTable.routers.toArray();
120+
const knownRouters = currentRoutingTable.routers;
119121

120122
if (this._useSeedRouter) {
121123
return this._fetchRoutingTableFromSeedRouterFallbackToKnownRouters(knownRouters, currentRoutingTable);
@@ -215,7 +217,7 @@ export class LoadBalancer extends ConnectionProvider {
215217
SERVICE_UNAVAILABLE);
216218
}
217219

218-
if (newRoutingTable.writers.isEmpty()) {
220+
if (newRoutingTable.writers.length === 0) {
219221
// use seed router next time. this is important when cluster is partitioned. it tries to make sure driver
220222
// does not always get routing table without writers because it talks exclusively to a minority partition
221223
this._useSeedRouter = true;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
21+
/**
22+
* A facility to select most appropriate reader or writer among the given addresses for request processing.
23+
*/
24+
export default class LoadBalancingStrategy {
25+
26+
/**
27+
* Select next most appropriate reader from the list of given readers.
28+
* @param {string[]} knownReaders an array of currently known readers to select from.
29+
* @return {string} most appropriate reader or <code>null</code> if given array is empty.
30+
*/
31+
selectReader(knownReaders) {
32+
throw new Error('Abstract function');
33+
}
34+
35+
/**
36+
* Select next most appropriate writer from the list of given writers.
37+
* @param {string[]} knownWriters an array of currently known writers to select from.
38+
* @return {string} most appropriate writer or <code>null</code> if given array is empty.
39+
*/
40+
selectWriter(knownWriters) {
41+
throw new Error('Abstract function');
42+
}
43+
}

src/v1/internal/rediscovery.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ export default class Rediscovery {
6363
});
6464
}
6565

66-
static _assertNonEmpty(serversRoundRobinArray, serversName, routerAddress) {
67-
if (serversRoundRobinArray.isEmpty()) {
66+
static _assertNonEmpty(serverAddressesArray, serversName, routerAddress) {
67+
if (serverAddressesArray.length === 0) {
6868
throw newError('Received no ' + serversName + ' from router ' + routerAddress, PROTOCOL_ERROR);
6969
}
7070
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
export default class RoundRobinArrayIndex {
21+
22+
/**
23+
* @constructor
24+
* @param {number} [initialOffset=0] the initial offset for round robin.
25+
*/
26+
constructor(initialOffset) {
27+
this._offset = initialOffset || 0;
28+
}
29+
30+
/**
31+
* Get next index for an array with given length.
32+
* @param {number} arrayLength the array length.
33+
* @return {number} index in the array.
34+
*/
35+
next(arrayLength) {
36+
if (arrayLength === 0) {
37+
return -1;
38+
}
39+
40+
const nextOffset = this._offset;
41+
this._offset += 1;
42+
if (this._offset === Number.MAX_SAFE_INTEGER) {
43+
this._offset = 0;
44+
}
45+
46+
return nextOffset % arrayLength;
47+
}
48+
}

src/v1/internal/round-robin-array.js

Lines changed: 0 additions & 66 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 default class RoundRobinLoadBalancingStrategy extends LoadBalancingStrategy {
23+
24+
constructor() {
25+
super();
26+
this._readersIndex = new RoundRobinArrayIndex();
27+
this._writersIndex = new RoundRobinArrayIndex();
28+
}
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
selectReader(knownReaders) {
34+
return this._select(knownReaders, this._readersIndex);
35+
}
36+
37+
/**
38+
* @inheritDoc
39+
*/
40+
selectWriter(knownWriters) {
41+
return this._select(knownWriters, this._writersIndex);
42+
}
43+
44+
_select(addresses, roundRobinIndex) {
45+
const length = addresses.length;
46+
if (length === 0) {
47+
return null;
48+
}
49+
const index = roundRobinIndex.next(length);
50+
return addresses[index];
51+
}
52+
}

src/v1/internal/routing-table.js

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,34 @@
1717
* limitations under the License.
1818
*/
1919
import {int} from '../integer';
20-
import RoundRobinArray from './round-robin-array';
2120
import {READ, WRITE} from '../driver';
2221

2322
const MIN_ROUTERS = 1;
2423

2524
export default class RoutingTable {
2625

2726
constructor(routers, readers, writers, expirationTime) {
28-
this.routers = routers || new RoundRobinArray();
29-
this.readers = readers || new RoundRobinArray();
30-
this.writers = writers || new RoundRobinArray();
27+
this.routers = routers || [];
28+
this.readers = readers || [];
29+
this.writers = writers || [];
3130
this.expirationTime = expirationTime || int(0);
3231
}
3332

3433
forget(address) {
3534
// Don't remove it from the set of routers, since that might mean we lose our ability to re-discover,
3635
// just remove it from the set of readers and writers, so that we don't use it for actual work without
3736
// performing discovery first.
38-
this.readers.remove(address);
39-
this.writers.remove(address);
37+
38+
this.readers = removeFromArray(this.readers, address);
39+
this.writers = removeFromArray(this.writers, address);
4040
}
4141

4242
forgetRouter(address) {
43-
this.routers.remove(address);
43+
this.routers = removeFromArray(this.routers, address);
4444
}
4545

4646
forgetWriter(address) {
47-
this.writers.remove(address);
47+
this.writers = removeFromArray(this.writers, address);
4848
}
4949

5050
serversDiff(otherRoutingTable) {
@@ -62,20 +62,30 @@ export default class RoutingTable {
6262
*/
6363
isStaleFor(accessMode) {
6464
return this.expirationTime.lessThan(Date.now()) ||
65-
this.routers.size() < MIN_ROUTERS ||
66-
accessMode === READ && this.readers.isEmpty() ||
67-
accessMode === WRITE && this.writers.isEmpty();
65+
this.routers.length < MIN_ROUTERS ||
66+
accessMode === READ && this.readers.length === 0 ||
67+
accessMode === WRITE && this.writers.length === 0;
6868
}
6969

7070
_allServers() {
71-
return [...this.routers.toArray(), ...this.readers.toArray(), ...this.writers.toArray()];
71+
return [...this.routers, ...this.readers, ...this.writers];
7272
}
7373

7474
toString() {
7575
return `RoutingTable[` +
7676
`expirationTime=${this.expirationTime}, ` +
77-
`routers=${this.routers}, ` +
78-
`readers=${this.readers}, ` +
79-
`writers=${this.writers}]`;
77+
`routers=[${this.routers}], ` +
78+
`readers=[${this.readers}], ` +
79+
`writers=[${this.writers}]]`;
8080
}
8181
}
82+
83+
/**
84+
* Remove all occurrences of the element in the array.
85+
* @param {Array} array the array to filter.
86+
* @param {object} element the element to remove.
87+
* @return {Array} new filtered array.
88+
*/
89+
function removeFromArray(array, element) {
90+
return array.filter(item => item !== element);
91+
}

0 commit comments

Comments
 (0)