diff --git a/src/v1/internal/connection-providers.js b/src/v1/internal/connection-providers.js index c7dcf31b2..3b2fa25c5 100644 --- a/src/v1/internal/connection-providers.js +++ b/src/v1/internal/connection-providers.js @@ -23,18 +23,20 @@ import Session from '../session'; import RoundRobinArray from './round-robin-array'; import RoutingTable from './routing-table'; import Rediscovery from './rediscovery'; +import hasFeature from './features'; +import {DnsHostNameResolver, DummyHostNameResolver} from './host-name-resolvers'; class ConnectionProvider { acquireConnection(mode) { - throw new Error('Abstract method'); + throw new Error('Abstract function'); } _withAdditionalOnErrorCallback(connectionPromise, driverOnErrorCallback) { // install error handler from the driver on the connection promise; this callback is installed separately // so that it does not handle errors, instead it is just an additional error reporting facility. connectionPromise.catch(error => { - driverOnErrorCallback(error) + driverOnErrorCallback(error); }); // return the original connection promise return connectionPromise; @@ -61,10 +63,12 @@ export class LoadBalancer extends ConnectionProvider { constructor(address, connectionPool, driverOnErrorCallback) { super(); - this._routingTable = new RoutingTable(new RoundRobinArray([address])); + this._seedRouter = address; + this._routingTable = new RoutingTable(new RoundRobinArray([this._seedRouter])); this._rediscovery = new Rediscovery(); this._connectionPool = connectionPool; this._driverOnErrorCallback = driverOnErrorCallback; + this._hostNameResolver = LoadBalancer._createHostNameResolver(); } acquireConnection(mode) { @@ -109,7 +113,42 @@ export class LoadBalancer extends ConnectionProvider { _refreshRoutingTable(currentRoutingTable) { const knownRouters = currentRoutingTable.routers.toArray(); - const refreshedTablePromise = knownRouters.reduce((refreshedTablePromise, currentRouter, currentIndex) => { + return this._fetchNewRoutingTable(knownRouters, currentRoutingTable).then(newRoutingTable => { + if (LoadBalancer._isValidRoutingTable(newRoutingTable)) { + // one of the known routers returned a valid routing table - use it + return newRoutingTable; + } + + if (!newRoutingTable) { + // returned routing table was undefined, this means a connection error happened and the last known + // router did not return a valid routing table, so we need to forget it + const lastRouterIndex = knownRouters.length - 1; + LoadBalancer._forgetRouter(currentRoutingTable, knownRouters, lastRouterIndex); + } + + // none of the known routers returned a valid routing table - try to use seed router address for rediscovery + return this._fetchNewRoutingTableUsingSeedRouterAddress(knownRouters, this._seedRouter); + }).then(newRoutingTable => { + if (LoadBalancer._isValidRoutingTable(newRoutingTable)) { + this._updateRoutingTable(newRoutingTable); + return newRoutingTable; + } + + // none of the existing routers returned valid routing table, throw exception + throw newError('Could not perform discovery. No routing servers available.', SERVICE_UNAVAILABLE); + }); + } + + _fetchNewRoutingTableUsingSeedRouterAddress(knownRouters, seedRouter) { + return this._hostNameResolver.resolve(seedRouter).then(resolvedRouterAddresses => { + // filter out all addresses that we've already tried + const newAddresses = resolvedRouterAddresses.filter(address => knownRouters.indexOf(address) < 0); + return this._fetchNewRoutingTable(newAddresses, null); + }); + } + + _fetchNewRoutingTable(routerAddresses, routingTable) { + return routerAddresses.reduce((refreshedTablePromise, currentRouter, currentIndex) => { return refreshedTablePromise.then(newRoutingTable => { if (newRoutingTable) { if (!newRoutingTable.writers.isEmpty()) { @@ -120,7 +159,7 @@ export class LoadBalancer extends ConnectionProvider { // returned routing table was undefined, this means a connection error happened and we need to forget the // previous router and try the next one const previousRouterIndex = currentIndex - 1; - this._forgetRouter(currentRoutingTable, knownRouters, previousRouterIndex); + LoadBalancer._forgetRouter(routingTable, routerAddresses, previousRouterIndex); } // try next router @@ -128,20 +167,6 @@ export class LoadBalancer extends ConnectionProvider { return this._rediscovery.lookupRoutingTableOnRouter(session, currentRouter); }); }, Promise.resolve(null)); - - return refreshedTablePromise.then(newRoutingTable => { - if (newRoutingTable && !newRoutingTable.writers.isEmpty()) { - this._updateRoutingTable(newRoutingTable); - return newRoutingTable; - } - - // forget the last known router because it did not return a valid routing table - const lastRouterIndex = knownRouters.length - 1; - this._forgetRouter(currentRoutingTable, knownRouters, lastRouterIndex); - - // none of the existing routers returned valid routing table, throw exception - throw newError('Could not perform discovery. No routing servers available.', SERVICE_UNAVAILABLE); - }); } _createSessionForRediscovery(routerAddress) { @@ -162,12 +187,23 @@ export class LoadBalancer extends ConnectionProvider { this._routingTable = newRoutingTable; } - _forgetRouter(routingTable, routersArray, routerIndex) { + static _isValidRoutingTable(routingTable) { + return routingTable && !routingTable.writers.isEmpty(); + } + + static _forgetRouter(routingTable, routersArray, routerIndex) { const address = routersArray[routerIndex]; - if (address) { + if (routingTable && address) { routingTable.forgetRouter(address); } } + + static _createHostNameResolver() { + if (hasFeature('dns_lookup')) { + return new DnsHostNameResolver(); + } + return new DummyHostNameResolver(); + } } export class SingleConnectionProvider extends ConnectionProvider { diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index 7d0ed8dc2..9aae15831 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -500,5 +500,7 @@ export { connect, parseScheme, parseUrl, + parseHost, + parsePort, Connection } diff --git a/src/v1/internal/features.js b/src/v1/internal/features.js index d7711101f..9a1f622b4 100644 --- a/src/v1/internal/features.js +++ b/src/v1/internal/features.js @@ -40,6 +40,17 @@ const FEATURES = { } catch (e) { return false; } + }, + dns_lookup: () => { + try { + const lookupFunction = require('dns').lookup; + if (lookupFunction && typeof lookupFunction === 'function') { + return true; + } + return false; + } catch (e) { + return false; + } } }; diff --git a/src/v1/internal/host-name-resolvers.js b/src/v1/internal/host-name-resolvers.js new file mode 100644 index 000000000..e3b328f65 --- /dev/null +++ b/src/v1/internal/host-name-resolvers.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2002-2017 "Neo Technology,"," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {parseHost, parsePort} from './connector'; + +class HostNameResolver { + + resolve() { + throw new Error('Abstract function'); + } +} + +export class DummyHostNameResolver extends HostNameResolver { + + resolve(seedRouter) { + return resolveToItself(seedRouter); + } +} + +export class DnsHostNameResolver extends HostNameResolver { + + constructor() { + super(); + this._dns = require('dns'); + } + + resolve(seedRouter) { + const seedRouterHost = parseHost(seedRouter); + const seedRouterPort = parsePort(seedRouter); + + return new Promise((resolve) => { + this._dns.lookup(seedRouterHost, {all: true}, (error, addresses) => { + if (error) { + resolve(resolveToItself(seedRouter)); + } else { + const addressesWithPorts = addresses.map(address => addressWithPort(address, seedRouterPort)); + resolve(addressesWithPorts); + } + }); + }); + } +} + +function resolveToItself(address) { + return Promise.resolve([address]); +} + +function addressWithPort(addressObject, port) { + const address = addressObject.address; + if (port) { + return address + ':' + port; + } + return address; +} diff --git a/test/internal/connection-providers.test.js b/test/internal/connection-providers.test.js index 04329d6a1..d0216ef10 100644 --- a/test/internal/connection-providers.test.js +++ b/test/internal/connection-providers.test.js @@ -273,7 +273,7 @@ describe('LoadBalancer', () => { pool, int(0), // expired routing table { - 'server-1': null, // did not return any routing table + 'server-1': null, // returns no routing table 'server-2': updatedRoutingTable, } ); @@ -305,7 +305,7 @@ describe('LoadBalancer', () => { pool, int(0), // expired routing table { - 'server-1': null, // did not return any routing table + 'server-1': null, // returns no routing table 'server-2': updatedRoutingTable, } ); @@ -405,8 +405,8 @@ describe('LoadBalancer', () => { newPool(), int(0), // expired routing table { - 'server-1': null, // did not return any routing table - 'server-2': null // did not return any routing table + 'server-1': null, // returns no routing table + 'server-2': null // returns no routing table } ); @@ -424,8 +424,8 @@ describe('LoadBalancer', () => { newPool(), int(0), // expired routing table { - 'server-1': null, // did not return any routing table - 'server-2': null // did not return any routing table + 'server-1': null, // returns no routing table + 'server-2': null // returns no routing table } ); @@ -582,14 +582,447 @@ describe('LoadBalancer', () => { }); }); + it('uses seed router address when all existing routers failed', done => { + const illegalRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C', 'server-D'], + [] // no writers - table is illegal and should be skipped + ); + const updatedRoutingTable = newRoutingTable( + ['server-A', 'server-B', 'server-C'], + ['server-D', 'server-E'], + ['server-F', 'server-G'] + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-0'], // seed router address resolves just to itself + ['server-1', 'server-2', 'server-3'], + ['server-4', 'server-5'], + ['server-6', 'server-7'], + int(0), // expired routing table + { + 'server-1': null, // returns no routing table + 'server-2': illegalRoutingTable, + 'server-3': null, // returns no routing table + 'server-0': updatedRoutingTable + } + ); + + loadBalancer.acquireConnection(READ).then(connection1 => { + expect(connection1.address).toEqual('server-D'); + + loadBalancer.acquireConnection(WRITE).then(connection2 => { + expect(connection2.address).toEqual('server-F'); + + expectRoutingTable(loadBalancer, + ['server-A', 'server-B', 'server-C'], + ['server-D', 'server-E'], + ['server-F', 'server-G'] + ); + done(); + }); + }); + }); + + it('uses resolved seed router address when all existing routers failed', done => { + const illegalRoutingTable = newRoutingTable( + ['server-A'], + ['server-B'], + [] // no writers - table is illegal and should be skipped + ); + const updatedRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C', 'server-D'], + ['server-E', 'server-F'] + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-01'], // seed router address resolves to a different one + ['server-1', 'server-2', 'server-3'], + ['server-4', 'server-5'], + ['server-6', 'server-7'], + int(0), // expired routing table + { + 'server-1': illegalRoutingTable, + 'server-2': illegalRoutingTable, + 'server-3': null, // returns no routing table + 'server-01': updatedRoutingTable + } + ); + + loadBalancer.acquireConnection(WRITE).then(connection1 => { + expect(connection1.address).toEqual('server-E'); + + loadBalancer.acquireConnection(READ).then(connection2 => { + expect(connection2.address).toEqual('server-C'); + + expectRoutingTable(loadBalancer, + ['server-A', 'server-B'], + ['server-C', 'server-D'], + ['server-E', 'server-F'] + ); + done(); + }); + }); + }); + + it('uses resolved seed router address that returns correct routing table when all existing routers failed', done => { + const illegalRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C', 'server-D'], + [] // no writers - table is illegal and should be skipped + ); + const updatedRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C'], + ['server-D', 'server-E'] + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-01', 'server-02', 'server-03'], // seed router address resolves to 3 different addresses + ['server-1'], + ['server-2'], + ['server-3'], + int(0), // expired routing table + { + 'server-1': illegalRoutingTable, + 'server-01': null, // returns no routing table + 'server-02': illegalRoutingTable, + 'server-03': updatedRoutingTable + } + ); + + loadBalancer.acquireConnection(WRITE).then(connection1 => { + expect(connection1.address).toEqual('server-D'); + + loadBalancer.acquireConnection(WRITE).then(connection2 => { + expect(connection2.address).toEqual('server-E'); + + expectRoutingTable(loadBalancer, + ['server-A', 'server-B'], + ['server-C'], + ['server-D', 'server-E'] + ); + done(); + }); + }); + }); + + it('fails when existing routers fail and seed router returns an invalid routing table', done => { + const emptyIllegalRoutingTable = newRoutingTable([], [], []); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-0'], // seed router address resolves just to itself + ['server-1', 'server-2', 'server-3'], + ['server-4', 'server-5'], + ['server-6'], + int(0), // expired routing table + { + 'server-1': emptyIllegalRoutingTable, + 'server-2': null, // returns no routing table + 'server-3': null, // returns no routing table + 'server-0': emptyIllegalRoutingTable + } + ); + + loadBalancer.acquireConnection(READ).catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE); + + expectRoutingTable(loadBalancer, + ['server-1'], // only server-1 is in the table, it returned a routing table which turned out to be invalid + ['server-4', 'server-5'], + ['server-6'], + ); + + loadBalancer.acquireConnection(WRITE).catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE); + + expectRoutingTable(loadBalancer, + ['server-1'], // only server-1 is in the table, it returned a routing table which turned out to be invalid + ['server-4', 'server-5'], + ['server-6'], + ); + + done(); + }); + }); + }); + + it('fails when existing routers fail and resolved seed router returns an invalid routing table', done => { + const illegalRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C', 'server-D'], + [] // no writers - table is illegal and should be skipped + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-01'], // seed router address resolves to a different one + ['server-1', 'server-2'], + ['server-3'], + ['server-4'], + int(0), // expired routing table + { + 'server-1': null, // returns no routing table + 'server-2': illegalRoutingTable, + 'server-01': null // returns no routing table + } + ); + + loadBalancer.acquireConnection(WRITE).catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE); + + expectRoutingTable(loadBalancer, + ['server-2'], // only server-2 is in the table, it returned a routing table which turned out to be invalid + ['server-3'], + ['server-4'], + ); + + loadBalancer.acquireConnection(READ).catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE); + + expectRoutingTable(loadBalancer, + ['server-2'], // only server-2 is in the table, it returned a routing table which turned out to be invalid + ['server-3'], + ['server-4'], + ); + + done(); + }); + }); + }); + + it('fails when existing routers fail and all resolved seed routers return an invalid routing table', done => { + const illegalRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C', 'server-D'], + [] // no writers - table is illegal and should be skipped + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-02', 'server-01'], // seed router address resolves to 2 different addresses + ['server-1', 'server-2', 'server-3'], + ['server-4'], + ['server-5'], + int(0), // expired routing table + { + 'server-1': null, // returns no routing table + 'server-2': null, // returns no routing table + 'server-3': null, // returns no routing table + 'server-01': illegalRoutingTable, + 'server-02': null // returns no routing table + } + ); + + loadBalancer.acquireConnection(READ).catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE); + + expectRoutingTable(loadBalancer, + [], // all known seed servers failed to return routing tables and were forgotten + ['server-4'], + ['server-5'], + ); + + loadBalancer.acquireConnection(WRITE).catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE); + + expectRoutingTable(loadBalancer, + [], // all known seed servers failed to return routing tables and were forgotten + ['server-4'], + ['server-5'], + ); + + done(); + }); + }); + }); + + it('uses seed router when no existing routers', done => { + const updatedRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C'], + ['server-D'] + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-0'], // seed router address resolves just to itself + [], // no routers in the known routing table + ['server-1', 'server-2'], + ['server-3'], + Integer.MAX_VALUE, // not expired + { + 'server-0': updatedRoutingTable + } + ); + + loadBalancer.acquireConnection(WRITE).then(connection1 => { + expect(connection1.address).toEqual('server-D'); + + loadBalancer.acquireConnection(READ).then(connection2 => { + expect(connection2.address).toEqual('server-C'); + + expectRoutingTable(loadBalancer, + ['server-A', 'server-B'], + ['server-C'], + ['server-D'] + ); + done(); + }); + }); + }); + + it('uses resolved seed router when no existing routers', done => { + const updatedRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C', 'server-D'], + ['server-F', 'server-E'] + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-01'], // seed router address resolves to a different one + [], // no routers in the known routing table + ['server-1', 'server-2'], + ['server-3', 'server-4'], + Integer.MAX_VALUE, // not expired + { + 'server-01': updatedRoutingTable + } + ); + + loadBalancer.acquireConnection(READ).then(connection1 => { + expect(connection1.address).toEqual('server-C'); + + loadBalancer.acquireConnection(WRITE).then(connection2 => { + expect(connection2.address).toEqual('server-F'); + + expectRoutingTable(loadBalancer, + ['server-A', 'server-B'], + ['server-C', 'server-D'], + ['server-F', 'server-E'] + ); + done(); + }); + }); + }); + + it('uses resolved seed router that returns correct routing table when no existing routers exist', done => { + const illegalRoutingTable = newRoutingTable( + ['server-A'], + ['server-B'], + [] // no writers - table is illegal and should be skipped + ); + const updatedRoutingTable = newRoutingTable( + ['server-A', 'server-B', 'server-C'], + ['server-D', 'server-E'], + ['server-F'] + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-02', 'server-01', 'server-03'], // seed router address resolves to 3 different addresses + [], // no routers in the known routing table + ['server-1'], + ['server-2', 'server-3'], + Integer.MAX_VALUE, // not expired + { + 'server-01': null, + 'server-02': illegalRoutingTable, + 'server-03': updatedRoutingTable + } + ); + + loadBalancer.acquireConnection(WRITE).then(connection1 => { + expect(connection1.address).toEqual('server-F'); + + loadBalancer.acquireConnection(READ).then(connection2 => { + expect(connection2.address).toEqual('server-D'); + + expectRoutingTable(loadBalancer, + ['server-A', 'server-B', 'server-C'], + ['server-D', 'server-E'], + ['server-F'] + ); + done(); + }); + }); + }); + + it('ignores already probed routers after seed router resolution', done => { + const illegalRoutingTable = newRoutingTable( + ['server-A'], + ['server-B'], + [] // no writers - table is illegal and should be skipped + ); + const updatedRoutingTable = newRoutingTable( + ['server-A', 'server-B'], + ['server-C', 'server-D'], + ['server-E', 'server-F'] + ); + + const loadBalancer = newLoadBalancerWithSeedRouter( + 'server-0', ['server-1', 'server-01', 'server-2', 'server-02'], // seed router address resolves to 4 different addresses + ['server-1', 'server-2'], + ['server-3', 'server-4'], + ['server-5', 'server-6'], + int(0), // expired routing table + { + 'server-1': null, + 'server-01': null, + 'server-2': illegalRoutingTable, + 'server-02': updatedRoutingTable + } + ); + const usedRouterArrays = []; + setupLoadBalancerToRememberRouters(loadBalancer, usedRouterArrays); + + loadBalancer.acquireConnection(READ).then(connection1 => { + expect(connection1.address).toEqual('server-C'); + + loadBalancer.acquireConnection(WRITE).then(connection2 => { + expect(connection2.address).toEqual('server-E'); + + // two sets of routers probed: + // 1) existing routers 'server-1' & 'server-2' + // 2) resolved routers 'server-01' & 'server-02' + expect(usedRouterArrays.length).toEqual(2); + expect(usedRouterArrays[0]).toEqual(['server-1', 'server-2']); + expect(usedRouterArrays[1]).toEqual(['server-01', 'server-02']); + + expectRoutingTable(loadBalancer, + ['server-A', 'server-B'], + ['server-C', 'server-D'], + ['server-E', 'server-F'] + ); + done(); + }); + }); + }); + }); function newDirectConnectionProvider(address, pool) { return new DirectConnectionProvider(address, pool, NO_OP_DRIVER_CALLBACK); } -function newLoadBalancer(routers, readers, writers, pool = null, expirationTime = Integer.MAX_VALUE, routerToRoutingTable = {}) { - const loadBalancer = new LoadBalancer(null, pool || newPool(), NO_OP_DRIVER_CALLBACK); +function newLoadBalancer(routers, readers, writers, + pool = null, + expirationTime = Integer.MAX_VALUE, + routerToRoutingTable = {}) { + const seedRouter = 'server-non-existing-seed-router'; + const loadBalancer = new LoadBalancer(seedRouter, pool || newPool(), NO_OP_DRIVER_CALLBACK); + loadBalancer._routingTable = new RoutingTable( + new RoundRobinArray(routers), + new RoundRobinArray(readers), + new RoundRobinArray(writers), + expirationTime + ); + loadBalancer._rediscovery = new FakeRediscovery(routerToRoutingTable); + return loadBalancer; +} + +function newLoadBalancerWithSeedRouter(seedRouter, seedRouterResolved, + routers, readers, writers, + expirationTime = Integer.MAX_VALUE, + routerToRoutingTable = {}) { + const loadBalancer = new LoadBalancer(seedRouter, newPool(), NO_OP_DRIVER_CALLBACK); loadBalancer._routingTable = new RoutingTable( new RoundRobinArray(routers), new RoundRobinArray(readers), @@ -597,6 +1030,7 @@ function newLoadBalancer(routers, readers, writers, pool = null, expirationTime expirationTime ); loadBalancer._rediscovery = new FakeRediscovery(routerToRoutingTable); + loadBalancer._hostNameResolver = new FakeDnsResolver(seedRouterResolved); return loadBalancer; } @@ -609,6 +1043,15 @@ function newRoutingTable(routers, readers, writers, expirationTime = Integer.MAX ); } +function setupLoadBalancerToRememberRouters(loadBalancer, routersArray) { + const originalFetch = loadBalancer._fetchNewRoutingTable.bind(loadBalancer); + const rememberingFetch = (routerAddresses, routingTable) => { + routersArray.push(routerAddresses); + return originalFetch(routerAddresses, routingTable); + }; + loadBalancer._fetchNewRoutingTable = rememberingFetch; +} + function newPool() { return new Pool(FakeConnection.create); } @@ -653,3 +1096,14 @@ class FakeRediscovery { return this._routerToRoutingTable[router]; } } + +class FakeDnsResolver { + + constructor(addresses) { + this._addresses = addresses; + } + + resolve(seedRouter) { + return Promise.resolve(this._addresses ? this._addresses : [seedRouter]); + } +} diff --git a/test/internal/host-name-resolvers.test.js b/test/internal/host-name-resolvers.test.js new file mode 100644 index 000000000..2ca2a8149 --- /dev/null +++ b/test/internal/host-name-resolvers.test.js @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2002-2017 "Neo Technology,"," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {DnsHostNameResolver, DummyHostNameResolver} from '../../src/v1/internal/host-name-resolvers'; +import hasFeature from '../../src/v1/internal/features'; +import {parseHost, parsePort, parseScheme} from '../../src/v1/internal/connector'; + +describe('DummyHostNameResolver', () => { + + it('should resolve given address to itself', done => { + const seedRouter = 'localhost'; + const resolver = new DummyHostNameResolver(); + + resolver.resolve(seedRouter).then(addresses => { + expect(addresses.length).toEqual(1); + expect(addresses[0]).toEqual(seedRouter); + done(); + }); + }); + + it('should resolve given address with port to itself', done => { + const seedRouter = 'localhost:7474'; + const resolver = new DummyHostNameResolver(); + + resolver.resolve(seedRouter).then(addresses => { + expect(addresses.length).toEqual(1); + expect(addresses[0]).toEqual(seedRouter); + done(); + }); + }); + +}); + +describe('DnsHostNameResolver', () => { + + if (hasFeature('dns_lookup')) { + + let originalTimeout; + + beforeEach(() => { + // it sometimes takes couple seconds to perform dns lookup, increase the async test timeout + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000; + }); + + afterEach(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + it('should resolve address', done => { + const seedRouter = 'neo4j.com'; + const resolver = new DnsHostNameResolver(); + + resolver.resolve(seedRouter).then(addresses => { + expect(addresses.length).toBeGreaterThan(0); + + addresses.forEach(address => { + expectToBeDefined(address); + expect(parseScheme(address)).toEqual(''); + expectToBeDefined(parseHost(address)); + expect(parsePort(address)).not.toBeDefined(); + }); + + done(); + }); + }); + + it('should resolve address with port', done => { + const seedRouter = 'neo4j.com:7474'; + const resolver = new DnsHostNameResolver(); + + resolver.resolve(seedRouter).then(addresses => { + expect(addresses.length).toBeGreaterThan(0); + + addresses.forEach(address => { + expectToBeDefined(address); + expect(parseScheme(address)).toEqual(''); + expectToBeDefined(parseHost(address)); + expect(parsePort(address)).toEqual('7474'); + }); + + done(); + }); + }); + + it('should resolve unresolvable address to itself', done => { + const seedRouter = '127.0.0.1'; // IP can't be resolved + const resolver = new DnsHostNameResolver(); + + resolver.resolve(seedRouter).then(addresses => { + expect(addresses.length).toEqual(1); + expect(addresses[0]).toEqual(seedRouter); + done(); + }); + }); + + it('should resolve unresolvable address with port to itself', done => { + const seedRouter = '127.0.0.1:7474'; // IP can't be resolved + const resolver = new DnsHostNameResolver(); + + resolver.resolve(seedRouter).then(addresses => { + expect(addresses.length).toEqual(1); + expect(addresses[0]).toEqual(seedRouter); + done(); + }); + }); + + } +}); + +function expectToBeDefined(value) { + expect(value).toBeDefined(); + expect(value).not.toBeNull(); +} diff --git a/test/resources/boltkit/rediscover_using_initial_router.script b/test/resources/boltkit/rediscover_using_initial_router.script new file mode 100644 index 000000000..d91d3e510 --- /dev/null +++ b/test/resources/boltkit/rediscover_using_initial_router.script @@ -0,0 +1,18 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL +!: AUTO RUN "BEGIN" {} +!: AUTO RUN "COMMIT" {} +!: AUTO RUN "ROLLBACK" {} + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9001","127.0.0.1:9009","127.0.0.1:9010"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9011"], "role": "ROUTE"}]] + SUCCESS {} +C: RUN "MATCH (n) RETURN n.name AS name" {} + PULL_ALL +S: SUCCESS {"fields": ["name"]} + RECORD ["Bob"] + RECORD ["Alice"] + SUCCESS {} diff --git a/test/v1/routing.driver.boltkit.it.js b/test/v1/routing.driver.boltkit.it.js index 90d1d16a5..f17690f83 100644 --- a/test/v1/routing.driver.boltkit.it.js +++ b/test/v1/routing.driver.boltkit.it.js @@ -1459,6 +1459,79 @@ describe('routing driver', () => { }); }); + it('should use seed router for rediscovery when all other routers are dead', done => { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + + const kit = new boltkit.BoltKit(); + const router1 = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9010); + + kit.run(() => { + const driver = newDriver('bolt+routing://127.0.0.1:9010'); + const session = driver.session(); + + // restart router on the same port with different script that contains itself as reader + router1.exit(() => { + const router2 = kit.start('./test/resources/boltkit/rediscover_using_initial_router.script', 9010); + + session.readTransaction(tx => tx.run('MATCH (n) RETURN n.name AS name')).then(result => { + const records = result.records; + expect(records.length).toEqual(2); + expect(records[0].get('name')).toEqual('Bob'); + expect(records[1].get('name')).toEqual('Alice'); + + session.close(() => { + driver.close(); + router2.exit(code => { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + + it('should use resolved seed router addresses for rediscovery when all other routers are dead', done => { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + + const kit = new boltkit.BoltKit(); + const router1 = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9010); + + kit.run(() => { + const driver = newDriver('bolt+routing://127.0.0.1:9010'); + // make seed address resolve to 3 different addresses (only last one has backing stub server): + setupFakeHostNameResolution(driver, '127.0.0.1:9010', ['127.0.0.1:9011', '127.0.0.1:9012', '127.0.0.1:9009']); + const session = driver.session(); + + // start new router on a different port to emulate host name resolution + // this router uses different script that contains itself as reader + router1.exit(() => { + const router2 = kit.start('./test/resources/boltkit/rediscover_using_initial_router.script', 9009); + + session.readTransaction(tx => tx.run('MATCH (n) RETURN n.name AS name')).then(result => { + const records = result.records; + expect(records.length).toEqual(2); + expect(records[0].get('name')).toEqual('Bob'); + expect(records[1].get('name')).toEqual('Alice'); + + session.close(() => { + driver.close(); + router2.exit(code => { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + function moveNextDateNow30SecondsForward() { const currentTime = Date.now(); hijackNextDateNowCall(currentTime + 30 * 1000 + 1); @@ -1609,6 +1682,10 @@ describe('routing driver', () => { return memorizingRoutingTable; } + function setupFakeHostNameResolution(driver, seedRouter, resolvedAddresses) { + driver._connectionProvider._hostNameResolver = new FakeHostNameResolver(seedRouter, resolvedAddresses); + } + function getConnectionPool(driver) { return driver._connectionProvider._connectionPool; } @@ -1642,4 +1719,19 @@ describe('routing driver', () => { } } + class FakeHostNameResolver { + + constructor(seedRouter, resolvedAddresses) { + this._seedRouter = seedRouter; + this._resolvedAddresses = resolvedAddresses; + } + + resolve(seedRouter) { + if (seedRouter === this._seedRouter) { + return Promise.resolve(this._resolvedAddresses); + } + return Promise.reject(new Error('Unexpected seed router address ' + seedRouter)); + } + } + });