From aa56a98cc888cfec5a9dcbd706ef99a9b9204b57 Mon Sep 17 00:00:00 2001 From: Zhen Date: Wed, 19 Apr 2017 18:02:10 +0200 Subject: [PATCH] Adding url parsing support of routing context --- src/v1/index.js | 12 ++-- src/v1/internal/connection-providers.js | 5 +- src/v1/internal/connector.js | 22 ++++++- src/v1/internal/get-servers-util.js | 43 +++++++++---- src/v1/internal/rediscovery.js | 2 +- src/v1/internal/server-version-util.js | 73 ++++++++++++++++++++++ src/v1/routing-driver.js | 5 +- test/internal/connection-providers.test.js | 4 +- test/internal/get-servers-util.test.js | 7 ++- test/internal/host-name-resolvers.test.js | 33 +++++++++- 10 files changed, 177 insertions(+), 29 deletions(-) create mode 100644 src/v1/internal/server-version-util.js diff --git a/src/v1/index.js b/src/v1/index.js index a9425642e..77d7705d3 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -26,8 +26,8 @@ import Record from './record'; import {Driver, READ, WRITE} from './driver'; import RoutingDriver from './routing-driver'; import VERSION from '../version'; -import {parseScheme, parseUrl} from "./internal/connector"; -import {assertString} from "./internal/util"; +import {parseScheme, parseUrl, parseRoutingContext} from "./internal/connector"; +import {assertString, isEmptyObjectOrNull} from "./internal/util"; const auth ={ @@ -120,13 +120,17 @@ let USER_AGENT = "neo4j-javascript/" + VERSION; function driver(url, authToken, config = {}) { assertString(url, 'Bolt URL'); const scheme = parseScheme(url); + const routingContext = parseRoutingContext(url); if (scheme === "bolt+routing://") { - return new RoutingDriver(parseUrl(url), USER_AGENT, authToken, config); + return new RoutingDriver(parseUrl(url), routingContext, USER_AGENT, authToken, config); } else if (scheme === "bolt://") { + if(!isEmptyObjectOrNull(routingContext)) + { + throw new Error("Routing context are not supported with scheme 'bolt'. Given URI: '" + url + "'"); + } return new Driver(parseUrl(url), USER_AGENT, authToken, config); } else { throw new Error("Unknown scheme: " + scheme); - } } diff --git a/src/v1/internal/connection-providers.js b/src/v1/internal/connection-providers.js index a2e5e94e7..5b6599772 100644 --- a/src/v1/internal/connection-providers.js +++ b/src/v1/internal/connection-providers.js @@ -25,6 +25,7 @@ import RoutingTable from './routing-table'; import Rediscovery from './rediscovery'; import hasFeature from './features'; import {DnsHostNameResolver, DummyHostNameResolver} from './host-name-resolvers'; +import GetServersUtil from './get-servers-util'; class ConnectionProvider { @@ -61,11 +62,11 @@ export class DirectConnectionProvider extends ConnectionProvider { export class LoadBalancer extends ConnectionProvider { - constructor(address, connectionPool, driverOnErrorCallback) { + constructor(address, routingContext, connectionPool, driverOnErrorCallback) { super(); this._seedRouter = address; this._routingTable = new RoutingTable(new RoundRobinArray([this._seedRouter])); - this._rediscovery = new Rediscovery(); + this._rediscovery = new Rediscovery(new GetServersUtil(routingContext)); this._connectionPool = connectionPool; this._driverOnErrorCallback = driverOnErrorCallback; this._hostNameResolver = LoadBalancer._createHostNameResolver(); diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index 9d19de16e..096063c51 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -60,10 +60,12 @@ MAGIC_PREAMBLE = 0x6060B017, DEBUG = false; let URLREGEX = new RegExp([ - "([^/]+//)?", // scheme + "([^/]+//)?", // scheme "(([^:/?#]*)", // hostname "(?::([0-9]+))?)", // port (optional) - ".*"].join("")); // everything else + "([^?]*)?", // everything else + "(\\?(.+))?" // query +].join("")); function parseScheme( url ) { let scheme = url.match(URLREGEX)[1] || ''; @@ -82,6 +84,21 @@ function parsePort( url ) { return url.match( URLREGEX )[4]; } +function parseRoutingContext(url) { + const query = url.match(URLREGEX)[7] || ''; + const map = {}; + if (query.length !== 0) { + query.split("&").forEach(val => { + const keyValue = val.split("="); + if (keyValue.length !== 2) { + throw new Error("Invalid parameters: '" + keyValue + "' in url '" + url + "'."); + } + map[keyValue[0]] = keyValue[1]; + }); + } + return map; +} + /** * Very rudimentary log handling, should probably be replaced by something proper at some point. * @param actor the part that sent the message, 'S' for server and 'C' for client @@ -495,5 +512,6 @@ export { parseUrl, parseHost, parsePort, + parseRoutingContext, Connection } diff --git a/src/v1/internal/get-servers-util.js b/src/v1/internal/get-servers-util.js index d94da81df..7d10faba9 100644 --- a/src/v1/internal/get-servers-util.js +++ b/src/v1/internal/get-servers-util.js @@ -20,25 +20,44 @@ import RoundRobinArray from './round-robin-array'; import {newError, PROTOCOL_ERROR, SERVICE_UNAVAILABLE} from '../error'; import Integer, {int} from '../integer'; +import {ServerVersion, VERSION3_2} from './server-version-util' -const PROCEDURE_CALL = 'CALL dbms.cluster.routing.getServers'; +const CALL_GET_SERVERS = 'CALL dbms.cluster.routing.getServers'; +const GET_ROUTING_TABLE_PARAM = "context"; +const CALL_GET_ROUTING_TABLE = "CALL dbms.cluster.routing.getRoutingTable({" + + GET_ROUTING_TABLE_PARAM + "})"; const PROCEDURE_NOT_FOUND_CODE = 'Neo.ClientError.Procedure.ProcedureNotFound'; export default class GetServersUtil { + constructor(routingContext={}) { + this._routingContext = routingContext; + } + callGetServers(session, routerAddress) { - return session.run(PROCEDURE_CALL).then(result => { - session.close(); - return result.records; - }).catch(error => { - if (error.code === PROCEDURE_NOT_FOUND_CODE) { - // throw when getServers procedure not found because this is clearly a configuration issue - throw newError('Server ' + routerAddress + ' could not perform routing. ' + - 'Make sure you are connecting to a causal cluster', SERVICE_UNAVAILABLE); + session.run("RETURN 1").then(result=>{ + let statement = {text:CALL_GET_SERVERS}; + + if(ServerVersion.fromString(result.summary.server.version).compare(VERSION3_2)>=0) + { + statement = { + text:CALL_GET_ROUTING_TABLE, + parameters:{GET_ROUTING_TABLE_PARAM: this._routingContext}}; } - // return nothing when failed to connect because code higher in the callstack is still able to retry with a - // different session towards a different router - return null; + + return session.run(statement).then(result => { + session.close(); + return result.records; + }).catch(error => { + if (error.code === PROCEDURE_NOT_FOUND_CODE) { + // throw when getServers procedure not found because this is clearly a configuration issue + throw newError('Server ' + routerAddress + ' could not perform routing. ' + + 'Make sure you are connecting to a causal cluster', SERVICE_UNAVAILABLE); + } + // return nothing when failed to connect because code higher in the callstack is still able to retry with a + // different session towards a different router + return null; + }); }); } diff --git a/src/v1/internal/rediscovery.js b/src/v1/internal/rediscovery.js index 08fb60bf0..a07977fde 100644 --- a/src/v1/internal/rediscovery.js +++ b/src/v1/internal/rediscovery.js @@ -24,7 +24,7 @@ import {newError, PROTOCOL_ERROR} from "../error"; export default class Rediscovery { constructor(getServersUtil) { - this._getServersUtil = getServersUtil || new GetServersUtil(); + this._getServersUtil = getServersUtil; } lookupRoutingTableOnRouter(session, routerAddress) { diff --git a/src/v1/internal/server-version-util.js b/src/v1/internal/server-version-util.js new file mode 100644 index 000000000..81dc980d5 --- /dev/null +++ b/src/v1/internal/server-version-util.js @@ -0,0 +1,73 @@ +/** + * 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. + */ + +let SERVER_VERSION_REGEX = new RegExp("(Neo4j/)?(\\d+)\\.(\\d+)(?:\\.)?(\\d*)(\\.|-|\\+)?([0-9A-Za-z-.]*)?"); + +class ServerVersion { + constructor(major, minor, patch) { + this._major = major; + this._minor = minor; + this._patch = patch; + } + + static fromString(versionStr) { + if (!versionStr) { + return new ServerVersion(3, 0, 0); + } + else { + const version = versionStr.match(SERVER_VERSION_REGEX); + return new ServerVersion(version[2], version[3], version[4]); + } + } + + compare(other) { + const version = this._parseToNumber(); + const otherVersion = other._parseToNumber(); + + if (version == otherVersion) { + return 0; + } + if (version > otherVersion) { + return 1; + } + else { + return -1; + } + } + + _parseToNumber() { + let value = 0; + value += parseInt(this._major) * 100 + parseInt(this._minor) * 10; + if (!isEmptyObjectOrNull(this._patch)) { + value += parseInt(this._patch); + } + return value; + } +} + +const VERSION3_2 = new ServerVersion(3, 2, 0); + +export{ + ServerVersion, + VERSION3_2 +} + + + + diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js index adc4548b5..3e895cf90 100644 --- a/src/v1/routing-driver.js +++ b/src/v1/routing-driver.js @@ -27,12 +27,13 @@ import {LoadBalancer} from './internal/connection-providers'; */ class RoutingDriver extends Driver { - constructor(url, userAgent, token = {}, config = {}) { + constructor(url, routingContext, userAgent, token = {}, config = {}) { super(url, userAgent, token, RoutingDriver._validateConfig(config)); + this._routingContext = routingContext; } _createConnectionProvider(address, connectionPool, driverOnErrorCallback) { - return new LoadBalancer(address, connectionPool, driverOnErrorCallback); + return new LoadBalancer(address, this._routingContext, connectionPool, driverOnErrorCallback); } _createSession(mode, connectionProvider, bookmark, config) { diff --git a/test/internal/connection-providers.test.js b/test/internal/connection-providers.test.js index 5a53812a8..7b78b5e64 100644 --- a/test/internal/connection-providers.test.js +++ b/test/internal/connection-providers.test.js @@ -135,7 +135,7 @@ describe('LoadBalancer', () => { }); it('initializes routing table with the given router', () => { - const loadBalancer = new LoadBalancer('server-ABC', newPool(), NO_OP_DRIVER_CALLBACK); + const loadBalancer = new LoadBalancer('server-ABC', {}, newPool(), NO_OP_DRIVER_CALLBACK); expectRoutingTable(loadBalancer, ['server-ABC'], @@ -1040,7 +1040,7 @@ function newLoadBalancerWithSeedRouter(seedRouter, seedRouterResolved, expirationTime = Integer.MAX_VALUE, routerToRoutingTable = {}, connectionPool = null) { - const loadBalancer = new LoadBalancer(seedRouter, connectionPool || newPool(), NO_OP_DRIVER_CALLBACK); + const loadBalancer = new LoadBalancer(seedRouter, {}, connectionPool || newPool(), NO_OP_DRIVER_CALLBACK); loadBalancer._routingTable = new RoutingTable( new RoundRobinArray(routers), new RoundRobinArray(readers), diff --git a/test/internal/get-servers-util.test.js b/test/internal/get-servers-util.test.js index 95855cf5a..e2323faf4 100644 --- a/test/internal/get-servers-util.test.js +++ b/test/internal/get-servers-util.test.js @@ -245,9 +245,10 @@ describe('get-servers-util', () => { class FakeSession { - constructor(runResponse) { - this._runResponse = runResponse; + constructor(runResponses) { + this._runResponses = runResponses; this._closed = false; + this._runCounter = 0; } static successful(result) { @@ -259,7 +260,7 @@ describe('get-servers-util', () => { } run() { - return this._runResponse; + return this._runResponses[this._runCounter ++]; } close() { diff --git a/test/internal/host-name-resolvers.test.js b/test/internal/host-name-resolvers.test.js index 2ca2a8149..a8a497dc3 100644 --- a/test/internal/host-name-resolvers.test.js +++ b/test/internal/host-name-resolvers.test.js @@ -19,7 +19,38 @@ 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'; +import {parseHost, parsePort, parseScheme, parseRoutingContext} from '../../src/v1/internal/connector'; + +describe('RoutingContextParser', ()=>{ + + it('should parse routing context', done => { + const url = "bolt://localhost:7687/cat?name=molly&age=1&color=white"; + const context = parseRoutingContext(url); + expect(context).toEqual({name:"molly", age:"1", color:"white"}); + + done(); + }); + + it('should return empty routing context', done =>{ + const url1 = "bolt://localhost:7687/cat?"; + const context1 = parseRoutingContext(url1); + expect(context1).toEqual({}); + + const url2 = "bolt://localhost:7687/lalala"; + const context2 = parseRoutingContext(url2); + expect(context2).toEqual({}); + + done(); + }); + + it('should error for unmatched pair', done=>{ + const url = "bolt://localhost?cat"; + expect(()=>parseRoutingContext(url)).toThrow( + new Error("Invalid parameters: 'cat' in url 'bolt://localhost?cat'.")); + + done(); + }); +}); describe('DummyHostNameResolver', () => {