Skip to content

Commit 6131fa2

Browse files
committed
Remember seed uri and DNS-lookup during rediscovery
Routing driver now memorizes seed router uri that was used on driver creation. It is later used as a fallback in case no existing routers respond during rediscovery. Also DNS resolution of this seed uri is performed if platform supports DNS lookup (only NodeJS, functionality not available in browser). This functionality is useful when seed address exists in DNS record and does not correspond to any causal cluster member. Such setup allows driver to switch to a completely different causal cluster using DNS record manipulations.
1 parent 9b1d2f8 commit 6131fa2

File tree

8 files changed

+842
-29
lines changed

8 files changed

+842
-29
lines changed

src/v1/internal/connection-providers.js

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,20 @@ import Session from '../session';
2323
import RoundRobinArray from './round-robin-array';
2424
import RoutingTable from './routing-table';
2525
import Rediscovery from './rediscovery';
26+
import hasFeature from './features';
27+
import {DnsHostNameResolver, DummyHostNameResolver} from './host-name-resolvers';
2628

2729
class ConnectionProvider {
2830

2931
acquireConnection(mode) {
30-
throw new Error('Abstract method');
32+
throw new Error('Abstract function');
3133
}
3234

3335
_withAdditionalOnErrorCallback(connectionPromise, driverOnErrorCallback) {
3436
// install error handler from the driver on the connection promise; this callback is installed separately
3537
// so that it does not handle errors, instead it is just an additional error reporting facility.
3638
connectionPromise.catch(error => {
37-
driverOnErrorCallback(error)
39+
driverOnErrorCallback(error);
3840
});
3941
// return the original connection promise
4042
return connectionPromise;
@@ -61,10 +63,12 @@ export class LoadBalancer extends ConnectionProvider {
6163

6264
constructor(address, connectionPool, driverOnErrorCallback) {
6365
super();
64-
this._routingTable = new RoutingTable(new RoundRobinArray([address]));
66+
this._seedRouter = address;
67+
this._routingTable = new RoutingTable(new RoundRobinArray([this._seedRouter]));
6568
this._rediscovery = new Rediscovery();
6669
this._connectionPool = connectionPool;
6770
this._driverOnErrorCallback = driverOnErrorCallback;
71+
this._hostNameResolver = LoadBalancer._createHostNameResolver();
6872
}
6973

7074
acquireConnection(mode) {
@@ -109,7 +113,42 @@ export class LoadBalancer extends ConnectionProvider {
109113
_refreshRoutingTable(currentRoutingTable) {
110114
const knownRouters = currentRoutingTable.routers.toArray();
111115

112-
const refreshedTablePromise = knownRouters.reduce((refreshedTablePromise, currentRouter, currentIndex) => {
116+
return this._fetchNewRoutingTable(knownRouters, currentRoutingTable).then(newRoutingTable => {
117+
if (LoadBalancer._isValidRoutingTable(newRoutingTable)) {
118+
// one of the known routers returned a valid routing table - use it
119+
return newRoutingTable;
120+
}
121+
122+
if (!newRoutingTable) {
123+
// returned routing table was undefined, this means a connection error happened and the last known
124+
// router did not return a valid routing table, so we need to forget it
125+
const lastRouterIndex = knownRouters.length - 1;
126+
LoadBalancer._forgetRouter(currentRoutingTable, knownRouters, lastRouterIndex);
127+
}
128+
129+
// none of the known routers returned a valid routing table - try to use seed router address for rediscovery
130+
return this._fetchNewRoutingTableUsingSeedRouterAddress(knownRouters, this._seedRouter);
131+
}).then(newRoutingTable => {
132+
if (LoadBalancer._isValidRoutingTable(newRoutingTable)) {
133+
this._updateRoutingTable(newRoutingTable);
134+
return newRoutingTable;
135+
}
136+
137+
// none of the existing routers returned valid routing table, throw exception
138+
throw newError('Could not perform discovery. No routing servers available.', SERVICE_UNAVAILABLE);
139+
});
140+
}
141+
142+
_fetchNewRoutingTableUsingSeedRouterAddress(knownRouters, seedRouter) {
143+
return this._hostNameResolver.resolve(seedRouter).then(resolvedRouterAddresses => {
144+
// filter out all addresses that we've already tried
145+
const newAddresses = resolvedRouterAddresses.filter(address => knownRouters.indexOf(address) < 0);
146+
return this._fetchNewRoutingTable(newAddresses, null);
147+
});
148+
}
149+
150+
_fetchNewRoutingTable(routerAddresses, routingTable) {
151+
return routerAddresses.reduce((refreshedTablePromise, currentRouter, currentIndex) => {
113152
return refreshedTablePromise.then(newRoutingTable => {
114153
if (newRoutingTable) {
115154
if (!newRoutingTable.writers.isEmpty()) {
@@ -120,28 +159,14 @@ export class LoadBalancer extends ConnectionProvider {
120159
// returned routing table was undefined, this means a connection error happened and we need to forget the
121160
// previous router and try the next one
122161
const previousRouterIndex = currentIndex - 1;
123-
this._forgetRouter(currentRoutingTable, knownRouters, previousRouterIndex);
162+
LoadBalancer._forgetRouter(routingTable, routerAddresses, previousRouterIndex);
124163
}
125164

126165
// try next router
127166
const session = this._createSessionForRediscovery(currentRouter);
128167
return this._rediscovery.lookupRoutingTableOnRouter(session, currentRouter);
129168
});
130169
}, Promise.resolve(null));
131-
132-
return refreshedTablePromise.then(newRoutingTable => {
133-
if (newRoutingTable && !newRoutingTable.writers.isEmpty()) {
134-
this._updateRoutingTable(newRoutingTable);
135-
return newRoutingTable;
136-
}
137-
138-
// forget the last known router because it did not return a valid routing table
139-
const lastRouterIndex = knownRouters.length - 1;
140-
this._forgetRouter(currentRoutingTable, knownRouters, lastRouterIndex);
141-
142-
// none of the existing routers returned valid routing table, throw exception
143-
throw newError('Could not perform discovery. No routing servers available.', SERVICE_UNAVAILABLE);
144-
});
145170
}
146171

147172
_createSessionForRediscovery(routerAddress) {
@@ -162,12 +187,23 @@ export class LoadBalancer extends ConnectionProvider {
162187
this._routingTable = newRoutingTable;
163188
}
164189

165-
_forgetRouter(routingTable, routersArray, routerIndex) {
190+
static _isValidRoutingTable(routingTable) {
191+
return routingTable && !routingTable.writers.isEmpty();
192+
}
193+
194+
static _forgetRouter(routingTable, routersArray, routerIndex) {
166195
const address = routersArray[routerIndex];
167-
if (address) {
196+
if (routingTable && address) {
168197
routingTable.forgetRouter(address);
169198
}
170199
}
200+
201+
static _createHostNameResolver() {
202+
if (hasFeature('dns_lookup')) {
203+
return new DnsHostNameResolver();
204+
}
205+
return new DummyHostNameResolver();
206+
}
171207
}
172208

173209
export class SingleConnectionProvider extends ConnectionProvider {

src/v1/internal/connector.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,5 +500,7 @@ export {
500500
connect,
501501
parseScheme,
502502
parseUrl,
503+
parseHost,
504+
parsePort,
503505
Connection
504506
}

src/v1/internal/features.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ const FEATURES = {
4040
} catch (e) {
4141
return false;
4242
}
43+
},
44+
dns_lookup: () => {
45+
try {
46+
const lookupFunction = require('dns').lookup;
47+
if (lookupFunction && typeof lookupFunction === 'function') {
48+
return true;
49+
}
50+
return false;
51+
} catch (e) {
52+
return false;
53+
}
4354
}
4455
};
4556

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 {parseHost, parsePort} from './connector';
21+
22+
class HostNameResolver {
23+
24+
resolve() {
25+
throw new Error('Abstract function');
26+
}
27+
}
28+
29+
export class DummyHostNameResolver extends HostNameResolver {
30+
31+
resolve(seedRouter) {
32+
return resolveToItself(seedRouter);
33+
}
34+
}
35+
36+
export class DnsHostNameResolver extends HostNameResolver {
37+
38+
constructor() {
39+
super();
40+
this._dns = require('dns');
41+
}
42+
43+
resolve(seedRouter) {
44+
const seedRouterHost = parseHost(seedRouter);
45+
const seedRouterPort = parsePort(seedRouter);
46+
47+
return new Promise((resolve) => {
48+
this._dns.lookup(seedRouterHost, {all: true}, (error, addresses) => {
49+
if (error) {
50+
resolve(resolveToItself(seedRouter));
51+
} else {
52+
const addressesWithPorts = addresses.map(address => addressWithPort(address, seedRouterPort));
53+
resolve(addressesWithPorts);
54+
}
55+
});
56+
});
57+
}
58+
}
59+
60+
function resolveToItself(address) {
61+
return Promise.resolve([address]);
62+
}
63+
64+
function addressWithPort(addressObject, port) {
65+
const address = addressObject.address;
66+
if (port) {
67+
return address + ':' + port;
68+
}
69+
return address;
70+
}

0 commit comments

Comments
 (0)