Skip to content

Commit cf14dcc

Browse files
authored
Merge pull request #408 from lutovich/1.7-resolver
Configurable server address resolver
2 parents c7c7123 + 1024a44 commit cf14dcc

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)