Skip to content

Commit 1024a44

Browse files
committed
Configurable server address resolver
This commit makes it possible to configure a resolver function used by the routing driver. Such function is used during the initial discovery and when all known routers have failed. Driver already had an internal facility like this which performed a DNS lookup in NodeJS environment for the hostname of the initial address. This remains the default. In browser environment no resolution is performed and address is used as-is. Users are now able to provide a custom resolver in the config. Example: ``` var auth = neo4j.auth.basic('neo4j', 'neo4j'); var config = { resolver: function(address) { return ['fallback1.db.com:8987', 'fallback2.db.org:7687']; } }; var driver = neo4j.driver('bolt+routing://db.com', auth, config); ```
1 parent c7c7123 commit 1024a44

File tree

9 files changed

+229
-13
lines changed

9 files changed

+229
-13
lines changed

src/v1/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,20 @@ const logging = {
202202
* level: 'info',
203203
* logger: (level, message) => console.log(level + ' ' + message)
204204
* },
205+
*
206+
* // Specify a custom server address resolver function used by the routing driver to resolve the initial address used to create the driver.
207+
* // Such resolution happens:
208+
* // * during the very first rediscovery when driver is created
209+
* // * when all the known routers from the current routing table have failed and driver needs to fallback to the initial address
210+
* //
211+
* // In NodeJS environment driver defaults to performing a DNS resolution of the initial address using 'dns' module.
212+
* // In browser environment driver uses the initial address as-is.
213+
* // Value should be a function that takes a single string argument - the initial address. It should return an array of new addresses.
214+
* // Address is a string of shape '<host>:<port>'. Provided function can return either a Promise resolved with an array of addresses
215+
* // or array of addresses directly.
216+
* resolver: function(address) {
217+
* return ['127.0.0.1:8888', 'fallback.db.com:7687'];
218+
* },
205219
* }
206220
*
207221
* @param {string} url The URL for the Neo4j database, for instance "bolt://localhost"

src/v1/internal/connection-providers.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,15 @@ export class DirectConnectionProvider extends ConnectionProvider {
6262

6363
export class LoadBalancer extends ConnectionProvider {
6464

65-
constructor(hostPort, routingContext, connectionPool, loadBalancingStrategy, driverOnErrorCallback, log) {
65+
constructor(hostPort, routingContext, connectionPool, loadBalancingStrategy, hostNameResolver, driverOnErrorCallback, log) {
6666
super();
6767
this._seedRouter = hostPort;
6868
this._routingTable = new RoutingTable([this._seedRouter]);
6969
this._rediscovery = new Rediscovery(new RoutingUtil(routingContext));
7070
this._connectionPool = connectionPool;
7171
this._driverOnErrorCallback = driverOnErrorCallback;
72-
this._hostNameResolver = LoadBalancer._createHostNameResolver();
7372
this._loadBalancingStrategy = loadBalancingStrategy;
73+
this._hostNameResolver = hostNameResolver;
7474
this._log = log;
7575
this._useSeedRouter = false;
7676
}
@@ -175,7 +175,8 @@ export class LoadBalancer extends ConnectionProvider {
175175
}
176176

177177
_fetchRoutingTableUsingSeedRouter(seenRouters, seedRouter) {
178-
return this._hostNameResolver.resolve(seedRouter).then(resolvedRouterAddresses => {
178+
const resolvedAddresses = this._hostNameResolver.resolve(seedRouter);
179+
return resolvedAddresses.then(resolvedRouterAddresses => {
179180
// filter out all addresses that we've already tried
180181
const newAddresses = resolvedRouterAddresses.filter(address => seenRouters.indexOf(address) < 0);
181182
return this._fetchRoutingTable(newAddresses, null);

src/v1/internal/host-name-resolvers.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ export class DummyHostNameResolver extends HostNameResolver {
3333
}
3434
}
3535

36+
export class ConfiguredHostNameResolver extends HostNameResolver {
37+
38+
constructor(resolverFunction) {
39+
super();
40+
this._resolverFunction = resolverFunction;
41+
}
42+
43+
resolve(seedRouter) {
44+
return new Promise(resolve => resolve(this._resolverFunction(seedRouter)))
45+
.then(resolved => {
46+
if (!Array.isArray(resolved)) {
47+
throw new TypeError(`Configured resolver function should either return an array of addresses or a Promise resolved with an array of addresses.` +
48+
`Each address is '<host>:<port>'. Got: ${resolved}`);
49+
}
50+
return resolved;
51+
});
52+
}
53+
}
54+
3655
export class DnsHostNameResolver extends HostNameResolver {
3756

3857
constructor() {

src/v1/routing-driver.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {LoadBalancer} from './internal/connection-providers';
2323
import LeastConnectedLoadBalancingStrategy, {LEAST_CONNECTED_STRATEGY_NAME} from './internal/least-connected-load-balancing-strategy';
2424
import RoundRobinLoadBalancingStrategy, {ROUND_ROBIN_STRATEGY_NAME} from './internal/round-robin-load-balancing-strategy';
2525
import ConnectionErrorHandler from './internal/connection-error-handler';
26+
import hasFeature from './internal/features';
27+
import {ConfiguredHostNameResolver, DnsHostNameResolver, DummyHostNameResolver} from './internal/host-name-resolvers';
2628

2729
/**
2830
* A driver that supports routing in a causal cluster.
@@ -41,7 +43,8 @@ class RoutingDriver extends Driver {
4143

4244
_createConnectionProvider(hostPort, connectionPool, driverOnErrorCallback) {
4345
const loadBalancingStrategy = RoutingDriver._createLoadBalancingStrategy(this._config, connectionPool);
44-
return new LoadBalancer(hostPort, this._routingContext, connectionPool, loadBalancingStrategy, driverOnErrorCallback, this._log);
46+
const resolver = createHostNameResolver(this._config);
47+
return new LoadBalancer(hostPort, this._routingContext, connectionPool, loadBalancingStrategy, resolver, driverOnErrorCallback, this._log);
4548
}
4649

4750
_createConnectionErrorHandler() {
@@ -85,12 +88,31 @@ class RoutingDriver extends Driver {
8588

8689
/**
8790
* @private
91+
* @returns {HostNameResolver} new resolver.
92+
*/
93+
function createHostNameResolver(config) {
94+
if (config.resolver) {
95+
return new ConfiguredHostNameResolver(config.resolver);
96+
}
97+
if (hasFeature('dns_lookup')) {
98+
return new DnsHostNameResolver();
99+
}
100+
return new DummyHostNameResolver();
101+
}
102+
103+
/**
104+
* @private
105+
* @returns {object} the given config.
88106
*/
89107
function validateConfig(config) {
90108
if (config.trust === 'TRUST_ON_FIRST_USE') {
91109
throw newError('The chosen trust mode is not compatible with a routing driver');
92110
}
111+
const resolver = config.resolver;
112+
if (resolver && typeof resolver !== 'function') {
113+
throw new TypeError(`Configured resolver should be a function. Got: ${resolver}`);
114+
}
93115
return config;
94116
}
95117

96-
export default RoutingDriver
118+
export default RoutingDriver;

test/internal/bolt-stub.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,10 @@ class StubServer {
111111
}
112112
}
113113

114-
function newDriver(url) {
114+
function newDriver(url, config = {}) {
115115
// boltstub currently does not support encryption, create driver with encryption turned off
116-
const config = {
117-
encrypted: 'ENCRYPTION_OFF'
118-
};
119-
return neo4j.driver(url, sharedNeo4j.authToken, config);
116+
const newConfig = Object.assign({encrypted: 'ENCRYPTION_OFF'}, config);
117+
return neo4j.driver(url, sharedNeo4j.authToken, newConfig);
120118
}
121119

122120
const supportedStub = SupportedBoltStub.create();

test/internal/connection-providers.test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {DirectConnectionProvider, LoadBalancer} from '../../src/v1/internal/conn
2525
import Pool from '../../src/v1/internal/pool';
2626
import LeastConnectedLoadBalancingStrategy from '../../src/v1/internal/least-connected-load-balancing-strategy';
2727
import Logger from '../../src/v1/internal/logger';
28+
import {DummyHostNameResolver} from '../../src/v1/internal/host-name-resolvers';
2829

2930
const NO_OP_DRIVER_CALLBACK = () => {
3031
};
@@ -138,7 +139,8 @@ describe('LoadBalancer', () => {
138139
it('initializes routing table with the given router', () => {
139140
const connectionPool = newPool();
140141
const loadBalancingStrategy = new LeastConnectedLoadBalancingStrategy(connectionPool);
141-
const loadBalancer = new LoadBalancer('server-ABC', {}, connectionPool, loadBalancingStrategy, NO_OP_DRIVER_CALLBACK, Logger.noOp());
142+
const loadBalancer = new LoadBalancer('server-ABC', {}, connectionPool, loadBalancingStrategy, new DummyHostNameResolver(),
143+
NO_OP_DRIVER_CALLBACK, Logger.noOp());
142144

143145
expectRoutingTable(loadBalancer,
144146
['server-ABC'],
@@ -1074,7 +1076,8 @@ function newLoadBalancerWithSeedRouter(seedRouter, seedRouterResolved,
10741076
connectionPool = null) {
10751077
const pool = connectionPool || newPool();
10761078
const loadBalancingStrategy = new LeastConnectedLoadBalancingStrategy(pool);
1077-
const loadBalancer = new LoadBalancer(seedRouter, {}, pool, loadBalancingStrategy, NO_OP_DRIVER_CALLBACK, Logger.noOp());
1079+
const loadBalancer = new LoadBalancer(seedRouter, {}, pool, loadBalancingStrategy, new DummyHostNameResolver(),
1080+
NO_OP_DRIVER_CALLBACK, Logger.noOp());
10781081
loadBalancer._routingTable = new RoutingTable(routers, readers, writers, expirationTime);
10791082
loadBalancer._rediscovery = new FakeRediscovery(routerToRoutingTable);
10801083
loadBalancer._hostNameResolver = new FakeDnsResolver(seedRouterResolved);

test/resources/boltstub/get_routing_table.script

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ S: SUCCESS {"fields": ["name"]}
1515
RECORD ["Bob"]
1616
RECORD ["Eve"]
1717
SUCCESS {}
18+
S: <EXIT>

test/v1/routing-driver.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import RoundRobinLoadBalancingStrategy from '../../src/v1/internal/round-robin-l
2121
import LeastConnectedLoadBalancingStrategy from '../../src/v1/internal/least-connected-load-balancing-strategy';
2222
import RoutingDriver from '../../src/v1/routing-driver';
2323
import Pool from '../../src/v1/internal/pool';
24+
import neo4j from '../../src/v1';
2425

2526
describe('RoutingDriver', () => {
2627

@@ -43,6 +44,12 @@ describe('RoutingDriver', () => {
4344
expect(() => createStrategy({loadBalancingStrategy: 'wrong'})).toThrow();
4445
});
4546

47+
it('should fail when configured resolver is of illegal type', () => {
48+
expect(() => neo4j.driver('bolt+routing://localhost', {}, {resolver: 'string instead of a function'})).toThrowError(TypeError);
49+
expect(() => neo4j.driver('bolt+routing://localhost', {}, {resolver: []})).toThrowError(TypeError);
50+
expect(() => neo4j.driver('bolt+routing://localhost', {}, {resolver: {}})).toThrowError(TypeError);
51+
});
52+
4653
});
4754

4855
function createStrategy(config) {

test/v1/routing.driver.boltkit.test.js

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import neo4j from '../../src/v1';
2121
import {READ, WRITE} from '../../src/v1/driver';
2222
import boltStub from '../internal/bolt-stub';
2323
import RoutingTable from '../../src/v1/internal/routing-table';
24-
import {SESSION_EXPIRED} from '../../src/v1/error';
24+
import {SERVICE_UNAVAILABLE, SESSION_EXPIRED} from '../../src/v1/error';
2525
import lolex from 'lolex';
2626

2727
describe('routing driver with stub server', () => {
@@ -1915,6 +1915,89 @@ describe('routing driver with stub server', () => {
19151915
testAddressPurgeOnDatabaseError(`RETURN 1`, READ, done);
19161916
});
19171917

1918+
it('should use resolver function that returns array during first discovery', done => {
1919+
testResolverFunctionDuringFirstDiscovery(['127.0.0.1:9010'], done);
1920+
});
1921+
1922+
it('should use resolver function that returns promise during first discovery', done => {
1923+
testResolverFunctionDuringFirstDiscovery(Promise.resolve(['127.0.0.1:9010']), done);
1924+
});
1925+
1926+
it('should fail first discovery when configured resolver function throws', done => {
1927+
const failureFunction = () => {
1928+
throw new Error('Broken resolver');
1929+
};
1930+
testResolverFunctionFailureDuringFirstDiscovery(failureFunction, null, 'Broken resolver', done);
1931+
});
1932+
1933+
it('should fail first discovery when configured resolver function returns no addresses', done => {
1934+
const failureFunction = () => {
1935+
return [];
1936+
};
1937+
testResolverFunctionFailureDuringFirstDiscovery(failureFunction, SERVICE_UNAVAILABLE, 'No routing servers available', done);
1938+
});
1939+
1940+
it('should fail first discovery when configured resolver function returns a string instead of array of addresses', done => {
1941+
const failureFunction = () => {
1942+
return 'Hello';
1943+
};
1944+
testResolverFunctionFailureDuringFirstDiscovery(failureFunction, null, 'Configured resolver function should either return an array of addresses', done);
1945+
});
1946+
1947+
it('should use resolver function during rediscovery when existing routers fail', done => {
1948+
if (!boltStub.supported) {
1949+
done();
1950+
return;
1951+
}
1952+
1953+
const router1 = boltStub.start('./test/resources/boltstub/get_routing_table.script', 9001);
1954+
const router2 = boltStub.start('./test/resources/boltstub/acquire_endpoints.script', 9042);
1955+
const reader = boltStub.start('./test/resources/boltstub/read_server.script', 9005);
1956+
1957+
boltStub.run(() => {
1958+
const resolverFunction = address => {
1959+
if (address === '127.0.0.1:9001') {
1960+
return ['127.0.0.1:9010', '127.0.0.1:9011', '127.0.0.1:9042'];
1961+
}
1962+
throw new Error(`Unexpected address ${address}`);
1963+
};
1964+
1965+
const driver = boltStub.newDriver('bolt+routing://127.0.0.1:9001', {resolver: resolverFunction});
1966+
1967+
const session = driver.session(READ);
1968+
// run a query that should trigger discovery against 9001 and then read from it
1969+
session.run('MATCH (n) RETURN n.name AS name')
1970+
.then(result => {
1971+
expect(result.records.map(record => record.get(0))).toEqual(['Alice', 'Bob', 'Eve']);
1972+
1973+
// 9001 should now exit and read transaction should fail to read from all existing readers
1974+
// it should then rediscover using addresses from resolver, only 9042 of them works and can respond with table containing reader 9005
1975+
session.readTransaction(tx => tx.run('MATCH (n) RETURN n.name'))
1976+
.then(result => {
1977+
expect(result.records.map(record => record.get(0))).toEqual(['Bob', 'Alice', 'Tina']);
1978+
1979+
assertHasRouters(driver, ['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003']);
1980+
assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006']);
1981+
assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']);
1982+
1983+
session.close(() => {
1984+
driver.close();
1985+
router1.exit(code1 => {
1986+
router2.exit(code2 => {
1987+
reader.exit(code3 => {
1988+
expect(code1).toEqual(0);
1989+
expect(code2).toEqual(0);
1990+
expect(code3).toEqual(0);
1991+
done();
1992+
});
1993+
});
1994+
});
1995+
});
1996+
}).catch(done.fail);
1997+
}).catch(done.fail);
1998+
});
1999+
});
2000+
19182001
function testAddressPurgeOnDatabaseError(query, accessMode, done) {
19192002
if (!boltStub.supported) {
19202003
done();
@@ -2146,6 +2229,74 @@ describe('routing driver with stub server', () => {
21462229
return Object.keys(driver._openConnections).length;
21472230
}
21482231

2232+
function testResolverFunctionDuringFirstDiscovery(resolutionResult, done) {
2233+
if (!boltStub.supported) {
2234+
done();
2235+
return;
2236+
}
2237+
2238+
const router = boltStub.start('./test/resources/boltstub/acquire_endpoints.script', 9010);
2239+
const reader = boltStub.start('./test/resources/boltstub/read_server.script', 9005);
2240+
2241+
boltStub.run(() => {
2242+
const resolverFunction = address => {
2243+
if (address === 'neo4j.com:7687') {
2244+
return resolutionResult;
2245+
}
2246+
throw new Error(`Unexpected address ${address}`);
2247+
};
2248+
2249+
const driver = boltStub.newDriver('bolt+routing://neo4j.com', {resolver: resolverFunction});
2250+
2251+
const session = driver.session(READ);
2252+
session.run('MATCH (n) RETURN n.name')
2253+
.then(result => {
2254+
expect(result.records.map(record => record.get(0))).toEqual(['Bob', 'Alice', 'Tina']);
2255+
session.close(() => {
2256+
driver.close();
2257+
2258+
router.exit(code1 => {
2259+
reader.exit(code2 => {
2260+
expect(code1).toEqual(0);
2261+
expect(code2).toEqual(0);
2262+
done();
2263+
});
2264+
});
2265+
});
2266+
})
2267+
.catch(done.fail);
2268+
});
2269+
}
2270+
2271+
function testResolverFunctionFailureDuringFirstDiscovery(failureFunction, expectedCode, expectedMessage, done) {
2272+
if (!boltStub.supported) {
2273+
done();
2274+
return;
2275+
}
2276+
2277+
const resolverFunction = address => {
2278+
if (address === 'neo4j.com:8989') {
2279+
return failureFunction();
2280+
}
2281+
throw new Error('Unexpected address');
2282+
};
2283+
2284+
const driver = boltStub.newDriver('bolt+routing://neo4j.com:8989', {resolver: resolverFunction});
2285+
const session = driver.session();
2286+
2287+
session.run('RETURN 1')
2288+
.then(result => done.fail(result))
2289+
.catch(error => {
2290+
if (expectedCode) {
2291+
expect(error.code).toEqual(expectedCode);
2292+
}
2293+
if (expectedMessage) {
2294+
expect(error.message.indexOf(expectedMessage)).toBeGreaterThan(-1);
2295+
}
2296+
done();
2297+
});
2298+
}
2299+
21492300
class MemorizingRoutingTable extends RoutingTable {
21502301

21512302
constructor(initialTable) {

0 commit comments

Comments
 (0)