Skip to content

Commit aa56a98

Browse files
author
Zhen
committed
Adding url parsing support of routing context
1 parent 0375eb0 commit aa56a98

10 files changed

+177
-29
lines changed

src/v1/index.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import Record from './record';
2626
import {Driver, READ, WRITE} from './driver';
2727
import RoutingDriver from './routing-driver';
2828
import VERSION from '../version';
29-
import {parseScheme, parseUrl} from "./internal/connector";
30-
import {assertString} from "./internal/util";
29+
import {parseScheme, parseUrl, parseRoutingContext} from "./internal/connector";
30+
import {assertString, isEmptyObjectOrNull} from "./internal/util";
3131

3232

3333
const auth ={
@@ -120,13 +120,17 @@ let USER_AGENT = "neo4j-javascript/" + VERSION;
120120
function driver(url, authToken, config = {}) {
121121
assertString(url, 'Bolt URL');
122122
const scheme = parseScheme(url);
123+
const routingContext = parseRoutingContext(url);
123124
if (scheme === "bolt+routing://") {
124-
return new RoutingDriver(parseUrl(url), USER_AGENT, authToken, config);
125+
return new RoutingDriver(parseUrl(url), routingContext, USER_AGENT, authToken, config);
125126
} else if (scheme === "bolt://") {
127+
if(!isEmptyObjectOrNull(routingContext))
128+
{
129+
throw new Error("Routing context are not supported with scheme 'bolt'. Given URI: '" + url + "'");
130+
}
126131
return new Driver(parseUrl(url), USER_AGENT, authToken, config);
127132
} else {
128133
throw new Error("Unknown scheme: " + scheme);
129-
130134
}
131135
}
132136

src/v1/internal/connection-providers.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import RoutingTable from './routing-table';
2525
import Rediscovery from './rediscovery';
2626
import hasFeature from './features';
2727
import {DnsHostNameResolver, DummyHostNameResolver} from './host-name-resolvers';
28+
import GetServersUtil from './get-servers-util';
2829

2930
class ConnectionProvider {
3031

@@ -61,11 +62,11 @@ export class DirectConnectionProvider extends ConnectionProvider {
6162

6263
export class LoadBalancer extends ConnectionProvider {
6364

64-
constructor(address, connectionPool, driverOnErrorCallback) {
65+
constructor(address, routingContext, connectionPool, driverOnErrorCallback) {
6566
super();
6667
this._seedRouter = address;
6768
this._routingTable = new RoutingTable(new RoundRobinArray([this._seedRouter]));
68-
this._rediscovery = new Rediscovery();
69+
this._rediscovery = new Rediscovery(new GetServersUtil(routingContext));
6970
this._connectionPool = connectionPool;
7071
this._driverOnErrorCallback = driverOnErrorCallback;
7172
this._hostNameResolver = LoadBalancer._createHostNameResolver();

src/v1/internal/connector.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,12 @@ MAGIC_PREAMBLE = 0x6060B017,
6060
DEBUG = false;
6161

6262
let URLREGEX = new RegExp([
63-
"([^/]+//)?", // scheme
63+
"([^/]+//)?", // scheme
6464
"(([^:/?#]*)", // hostname
6565
"(?::([0-9]+))?)", // port (optional)
66-
".*"].join("")); // everything else
66+
"([^?]*)?", // everything else
67+
"(\\?(.+))?" // query
68+
].join(""));
6769

6870
function parseScheme( url ) {
6971
let scheme = url.match(URLREGEX)[1] || '';
@@ -82,6 +84,21 @@ function parsePort( url ) {
8284
return url.match( URLREGEX )[4];
8385
}
8486

87+
function parseRoutingContext(url) {
88+
const query = url.match(URLREGEX)[7] || '';
89+
const map = {};
90+
if (query.length !== 0) {
91+
query.split("&").forEach(val => {
92+
const keyValue = val.split("=");
93+
if (keyValue.length !== 2) {
94+
throw new Error("Invalid parameters: '" + keyValue + "' in url '" + url + "'.");
95+
}
96+
map[keyValue[0]] = keyValue[1];
97+
});
98+
}
99+
return map;
100+
}
101+
85102
/**
86103
* Very rudimentary log handling, should probably be replaced by something proper at some point.
87104
* @param actor the part that sent the message, 'S' for server and 'C' for client
@@ -495,5 +512,6 @@ export {
495512
parseUrl,
496513
parseHost,
497514
parsePort,
515+
parseRoutingContext,
498516
Connection
499517
}

src/v1/internal/get-servers-util.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,44 @@
2020
import RoundRobinArray from './round-robin-array';
2121
import {newError, PROTOCOL_ERROR, SERVICE_UNAVAILABLE} from '../error';
2222
import Integer, {int} from '../integer';
23+
import {ServerVersion, VERSION3_2} from './server-version-util'
2324

24-
const PROCEDURE_CALL = 'CALL dbms.cluster.routing.getServers';
25+
const CALL_GET_SERVERS = 'CALL dbms.cluster.routing.getServers';
26+
const GET_ROUTING_TABLE_PARAM = "context";
27+
const CALL_GET_ROUTING_TABLE = "CALL dbms.cluster.routing.getRoutingTable({"
28+
+ GET_ROUTING_TABLE_PARAM + "})";
2529
const PROCEDURE_NOT_FOUND_CODE = 'Neo.ClientError.Procedure.ProcedureNotFound';
2630

2731
export default class GetServersUtil {
2832

33+
constructor(routingContext={}) {
34+
this._routingContext = routingContext;
35+
}
36+
2937
callGetServers(session, routerAddress) {
30-
return session.run(PROCEDURE_CALL).then(result => {
31-
session.close();
32-
return result.records;
33-
}).catch(error => {
34-
if (error.code === PROCEDURE_NOT_FOUND_CODE) {
35-
// throw when getServers procedure not found because this is clearly a configuration issue
36-
throw newError('Server ' + routerAddress + ' could not perform routing. ' +
37-
'Make sure you are connecting to a causal cluster', SERVICE_UNAVAILABLE);
38+
session.run("RETURN 1").then(result=>{
39+
let statement = {text:CALL_GET_SERVERS};
40+
41+
if(ServerVersion.fromString(result.summary.server.version).compare(VERSION3_2)>=0)
42+
{
43+
statement = {
44+
text:CALL_GET_ROUTING_TABLE,
45+
parameters:{GET_ROUTING_TABLE_PARAM: this._routingContext}};
3846
}
39-
// return nothing when failed to connect because code higher in the callstack is still able to retry with a
40-
// different session towards a different router
41-
return null;
47+
48+
return session.run(statement).then(result => {
49+
session.close();
50+
return result.records;
51+
}).catch(error => {
52+
if (error.code === PROCEDURE_NOT_FOUND_CODE) {
53+
// throw when getServers procedure not found because this is clearly a configuration issue
54+
throw newError('Server ' + routerAddress + ' could not perform routing. ' +
55+
'Make sure you are connecting to a causal cluster', SERVICE_UNAVAILABLE);
56+
}
57+
// return nothing when failed to connect because code higher in the callstack is still able to retry with a
58+
// different session towards a different router
59+
return null;
60+
});
4261
});
4362
}
4463

src/v1/internal/rediscovery.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {newError, PROTOCOL_ERROR} from "../error";
2424
export default class Rediscovery {
2525

2626
constructor(getServersUtil) {
27-
this._getServersUtil = getServersUtil || new GetServersUtil();
27+
this._getServersUtil = getServersUtil;
2828
}
2929

3030
lookupRoutingTableOnRouter(session, routerAddress) {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
let SERVER_VERSION_REGEX = new RegExp("(Neo4j/)?(\\d+)\\.(\\d+)(?:\\.)?(\\d*)(\\.|-|\\+)?([0-9A-Za-z-.]*)?");
21+
22+
class ServerVersion {
23+
constructor(major, minor, patch) {
24+
this._major = major;
25+
this._minor = minor;
26+
this._patch = patch;
27+
}
28+
29+
static fromString(versionStr) {
30+
if (!versionStr) {
31+
return new ServerVersion(3, 0, 0);
32+
}
33+
else {
34+
const version = versionStr.match(SERVER_VERSION_REGEX);
35+
return new ServerVersion(version[2], version[3], version[4]);
36+
}
37+
}
38+
39+
compare(other) {
40+
const version = this._parseToNumber();
41+
const otherVersion = other._parseToNumber();
42+
43+
if (version == otherVersion) {
44+
return 0;
45+
}
46+
if (version > otherVersion) {
47+
return 1;
48+
}
49+
else {
50+
return -1;
51+
}
52+
}
53+
54+
_parseToNumber() {
55+
let value = 0;
56+
value += parseInt(this._major) * 100 + parseInt(this._minor) * 10;
57+
if (!isEmptyObjectOrNull(this._patch)) {
58+
value += parseInt(this._patch);
59+
}
60+
return value;
61+
}
62+
}
63+
64+
const VERSION3_2 = new ServerVersion(3, 2, 0);
65+
66+
export{
67+
ServerVersion,
68+
VERSION3_2
69+
}
70+
71+
72+
73+

src/v1/routing-driver.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ import {LoadBalancer} from './internal/connection-providers';
2727
*/
2828
class RoutingDriver extends Driver {
2929

30-
constructor(url, userAgent, token = {}, config = {}) {
30+
constructor(url, routingContext, userAgent, token = {}, config = {}) {
3131
super(url, userAgent, token, RoutingDriver._validateConfig(config));
32+
this._routingContext = routingContext;
3233
}
3334

3435
_createConnectionProvider(address, connectionPool, driverOnErrorCallback) {
35-
return new LoadBalancer(address, connectionPool, driverOnErrorCallback);
36+
return new LoadBalancer(address, this._routingContext, connectionPool, driverOnErrorCallback);
3637
}
3738

3839
_createSession(mode, connectionProvider, bookmark, config) {

test/internal/connection-providers.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe('LoadBalancer', () => {
135135
});
136136

137137
it('initializes routing table with the given router', () => {
138-
const loadBalancer = new LoadBalancer('server-ABC', newPool(), NO_OP_DRIVER_CALLBACK);
138+
const loadBalancer = new LoadBalancer('server-ABC', {}, newPool(), NO_OP_DRIVER_CALLBACK);
139139

140140
expectRoutingTable(loadBalancer,
141141
['server-ABC'],
@@ -1040,7 +1040,7 @@ function newLoadBalancerWithSeedRouter(seedRouter, seedRouterResolved,
10401040
expirationTime = Integer.MAX_VALUE,
10411041
routerToRoutingTable = {},
10421042
connectionPool = null) {
1043-
const loadBalancer = new LoadBalancer(seedRouter, connectionPool || newPool(), NO_OP_DRIVER_CALLBACK);
1043+
const loadBalancer = new LoadBalancer(seedRouter, {}, connectionPool || newPool(), NO_OP_DRIVER_CALLBACK);
10441044
loadBalancer._routingTable = new RoutingTable(
10451045
new RoundRobinArray(routers),
10461046
new RoundRobinArray(readers),

test/internal/get-servers-util.test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,10 @@ describe('get-servers-util', () => {
245245

246246
class FakeSession {
247247

248-
constructor(runResponse) {
249-
this._runResponse = runResponse;
248+
constructor(runResponses) {
249+
this._runResponses = runResponses;
250250
this._closed = false;
251+
this._runCounter = 0;
251252
}
252253

253254
static successful(result) {
@@ -259,7 +260,7 @@ describe('get-servers-util', () => {
259260
}
260261

261262
run() {
262-
return this._runResponse;
263+
return this._runResponses[this._runCounter ++];
263264
}
264265

265266
close() {

test/internal/host-name-resolvers.test.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,38 @@
1919

2020
import {DnsHostNameResolver, DummyHostNameResolver} from '../../src/v1/internal/host-name-resolvers';
2121
import hasFeature from '../../src/v1/internal/features';
22-
import {parseHost, parsePort, parseScheme} from '../../src/v1/internal/connector';
22+
import {parseHost, parsePort, parseScheme, parseRoutingContext} from '../../src/v1/internal/connector';
23+
24+
describe('RoutingContextParser', ()=>{
25+
26+
it('should parse routing context', done => {
27+
const url = "bolt://localhost:7687/cat?name=molly&age=1&color=white";
28+
const context = parseRoutingContext(url);
29+
expect(context).toEqual({name:"molly", age:"1", color:"white"});
30+
31+
done();
32+
});
33+
34+
it('should return empty routing context', done =>{
35+
const url1 = "bolt://localhost:7687/cat?";
36+
const context1 = parseRoutingContext(url1);
37+
expect(context1).toEqual({});
38+
39+
const url2 = "bolt://localhost:7687/lalala";
40+
const context2 = parseRoutingContext(url2);
41+
expect(context2).toEqual({});
42+
43+
done();
44+
});
45+
46+
it('should error for unmatched pair', done=>{
47+
const url = "bolt://localhost?cat";
48+
expect(()=>parseRoutingContext(url)).toThrow(
49+
new Error("Invalid parameters: 'cat' in url 'bolt://localhost?cat'."));
50+
51+
done();
52+
});
53+
});
2354

2455
describe('DummyHostNameResolver', () => {
2556

0 commit comments

Comments
 (0)