From 49fe1cd7bfd83d453a2808e4f1910b36000d40ee Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Tue, 20 Sep 2016 15:48:43 +0200 Subject: [PATCH 01/18] Extended pool to handle multiple keys The connection pool now handles connections to multiple URI, this work is a precursor to handling multiple connections in the driver. --- src/v1/driver.js | 4 +-- src/v1/internal/connector.js | 6 ++-- src/v1/internal/pool.js | 20 +++++++----- test/internal/pool.test.js | 59 +++++++++++++++++++++++++----------- 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/v1/driver.js b/src/v1/driver.js index f8e612ba8..b30aa33d9 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -69,7 +69,7 @@ class Driver { let conn = connect(this._url, this._config); conn.initialize(this._userAgent, this._token, streamObserver); conn._id = sessionId; - conn._release = () => release(conn); + conn._release = () => release(this._url, conn); this._openSessions[sessionId] = conn; return conn; @@ -108,7 +108,7 @@ class Driver { * @return {Session} new session. */ session() { - let conn = this._pool.acquire(); + let conn = this._pool.acquire(this._url); return new Session( conn, (cb) => { // This gets called on Session#close(), and is where we return // the pooled 'connection' instance. diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index 80db8ab26..d4c19d599 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -177,13 +177,15 @@ class Connection { * @constructor * @param channel - channel with a 'write' function and a 'onmessage' * callback property + * @param url - url to connect to */ - constructor (channel) { + constructor (channel, url) { /** * An ordered queue of observers, each exchange response (zero or more * RECORD messages followed by a SUCCESS message) we recieve will be routed * to the next pending observer. */ + this.url = url; this._pendingObservers = []; this._currentObserver = undefined; this._ch = channel; @@ -464,7 +466,7 @@ function connect( url, config = {}) { trust : config.trust || (hasFeature("trust_on_first_use") ? "TRUST_ON_FIRST_USE" : "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES"), trustedCertificates : config.trustedCertificates || [], knownHosts : config.knownHosts - })); + }), url); } export default { diff --git a/src/v1/internal/pool.js b/src/v1/internal/pool.js index 37d898900..ae9b20cfa 100644 --- a/src/v1/internal/pool.js +++ b/src/v1/internal/pool.js @@ -35,14 +35,15 @@ class Pool { this._destroy = destroy; this._validate = validate; this._maxIdle = maxIdle; - this._pool = []; + this._pools = {}; this._release = this._release.bind(this); } - acquire() { + acquire(key) { let resource; - while( this._pool.length ) { - resource = this._pool.pop(); + let pool = this._pools[key] || []; + while( pool.length ) { + resource = pool.pop(); if( this._validate(resource) ) { return resource; @@ -54,11 +55,16 @@ class Pool { return this._create(this._release); } - _release(resource) { - if( this._pool.length >= this._maxIdle || !this._validate(resource) ) { + _release(key, resource) { + let pool = this._pools[key]; + if (!pool) { + pool = []; + this._pools[key] = pool; + } + if( pool.length >= this._maxIdle || !this._validate(resource) ) { this._destroy(resource); } else { - this._pool.push(resource); + pool.push(resource); } } } diff --git a/test/internal/pool.test.js b/test/internal/pool.test.js index f9a17b5aa..82ea50a9d 100644 --- a/test/internal/pool.test.js +++ b/test/internal/pool.test.js @@ -23,11 +23,12 @@ describe('Pool', function() { it('allocates if pool is empty', function() { // Given var counter = 0; + var key = "bolt://localhost:7687"; var pool = new Pool( function (release) { return new Resource(counter++, release) } ); // When - var r0 = pool.acquire(); - var r1 = pool.acquire(); + var r0 = pool.acquire(key); + var r1 = pool.acquire(key); // Then expect( r0.id ).toBe( 0 ); @@ -37,22 +38,45 @@ describe('Pool', function() { it('pools if resources are returned', function() { // Given a pool that allocates var counter = 0; + var key = "bolt://localhost:7687"; var pool = new Pool( function (release) { return new Resource(counter++, release) } ); // When - var r0 = pool.acquire(); - r0.close(); - var r1 = pool.acquire(); + var r0 = pool.acquire(key); + r0.close(key); + var r1 = pool.acquire(key); // Then expect( r0.id ).toBe( 0 ); expect( r1.id ).toBe( 0 ); }); + it('handles multiple keys', function() { + // Given a pool that allocates + var counter = 0; + var key1 = "bolt://localhost:7687"; + var key2 = "bolt://localhost:7688"; + var pool = new Pool( function (release) { return new Resource(counter++, release) } ); + + // When + var r0 = pool.acquire(key1); + var r1 = pool.acquire(key2); + r0.close(key1); + var r2 = pool.acquire(key1); + var r3 = pool.acquire(key2); + + // Then + expect( r0.id ).toBe( 0 ); + expect( r1.id ).toBe( 1 ); + expect( r2.id ).toBe( 0 ); + expect( r3.id ).toBe( 2 ); + }); + it('frees if pool reaches max size', function() { // Given a pool that tracks destroyed resources var counter = 0, destroyed = []; + var key = "bolt://localhost:7687"; var pool = new Pool( function (release) { return new Resource(counter++, release) }, function (resource) { destroyed.push(resource); }, @@ -61,12 +85,12 @@ describe('Pool', function() { ); // When - var r0 = pool.acquire(); - var r1 = pool.acquire(); - var r2 = pool.acquire(); - r0.close(); - r1.close(); - r2.close(); + var r0 = pool.acquire(key); + var r1 = pool.acquire(key); + var r2 = pool.acquire(key); + r0.close(key); + r1.close(key); + r2.close(key); // Then expect( destroyed.length ).toBe( 1 ); @@ -77,6 +101,7 @@ describe('Pool', function() { // Given a pool that allocates var counter = 0, destroyed = []; + var key = "bolt://localhost:7687"; var pool = new Pool( function (release) { return new Resource(counter++, release) }, function (resource) { destroyed.push(resource); }, @@ -85,10 +110,10 @@ describe('Pool', function() { ); // When - var r0 = pool.acquire(); - var r1 = pool.acquire(); - r0.close(); - r1.close(); + var r0 = pool.acquire(key); + var r1 = pool.acquire(key); + r0.close(key); + r1.close(key); // Then expect( destroyed.length ).toBe( 2 ); @@ -97,8 +122,8 @@ describe('Pool', function() { }); }); -function Resource( id, release ) { +function Resource( id, release) { var self = this; this.id = id; - this.close = function() { release(self); }; + this.close = function(key) { release(key, self); }; } \ No newline at end of file From cd7f8d5c9ea192dd5daccee7b88ceb531e563c49 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Fri, 30 Sep 2016 11:37:38 +0200 Subject: [PATCH 02/18] Added ability to purge item in pool We need purging of item in the connection pool since we need a way of removing connections that are no longer online or in other ways have been removed from a cluster. --- src/v1/internal/pool.js | 18 ++++++++++++++++-- test/internal/pool.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/v1/internal/pool.js b/src/v1/internal/pool.js index ae9b20cfa..85a00816f 100644 --- a/src/v1/internal/pool.js +++ b/src/v1/internal/pool.js @@ -42,10 +42,10 @@ class Pool { acquire(key) { let resource; let pool = this._pools[key] || []; - while( pool.length ) { + while (pool.length) { resource = pool.pop(); - if( this._validate(resource) ) { + if (this._validate(resource)) { return resource; } else { this._destroy(resource); @@ -55,6 +55,20 @@ class Pool { return this._create(this._release); } + purge(key) { + let resource; + let pool = this._pools[key] || []; + while (pool.length) { + resource = pool.pop(); + this._destroy(resource) + } + delete this._pools[key] + } + + has(key) { + return (key in this._pools); + } + _release(key, resource) { let pool = this._pools[key]; if (!pool) { diff --git a/test/internal/pool.test.js b/test/internal/pool.test.js index 82ea50a9d..06471b6ce 100644 --- a/test/internal/pool.test.js +++ b/test/internal/pool.test.js @@ -120,6 +120,38 @@ describe('Pool', function() { expect( destroyed[0].id ).toBe( r0.id ); expect( destroyed[1].id ).toBe( r1.id ); }); + + + it('purges keys', function() { + // Given a pool that allocates + var counter = 0; + var key1 = "bolt://localhost:7687"; + var key2 = "bolt://localhost:7688"; + var pool = new Pool( function (release) { return new Resource(counter++, release) }, + function (res) {res.destroyed = true; return true} + ); + + // When + var r0 = pool.acquire(key1); + var r1 = pool.acquire(key2); + r0.close(key1); + r1.close(key2); + expect(pool.has(key1)).toBe(true); + expect(pool.has(key2)).toBe(true); + pool.purge(key1); + expect(pool.has(key1)).toBe(false); + expect(pool.has(key2)).toBe(true); + + var r2 = pool.acquire(key1); + var r3 = pool.acquire(key2); + + // Then + expect( r0.id ).toBe( 0 ); + expect( r0.destroyed ).toBe( true ); + expect( r1.id ).toBe( 1 ); + expect( r2.id ).toBe( 2 ); + expect( r3.id ).toBe( 1 ); + }); }); function Resource( id, release) { From e7588a1ac95bbb6275c6128b17d255c69a72d04b Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Fri, 30 Sep 2016 17:37:31 +0200 Subject: [PATCH 03/18] Added utilities for running boltkit in tests --- .../resources/boltkit/discover_servers.script | 10 +++ test/resources/boltkit/return_x.script | 10 +++ test/v1/boltkit.js | 63 +++++++++++++++++++ test/v1/direct.driver.test.js | 47 ++++++++++++++ test/v1/routing.driver.test.js | 46 ++++++++++++++ 5 files changed, 176 insertions(+) create mode 100644 test/resources/boltkit/discover_servers.script create mode 100644 test/resources/boltkit/return_x.script create mode 100644 test/v1/boltkit.js create mode 100644 test/v1/direct.driver.test.js create mode 100644 test/v1/routing.driver.test.js diff --git a/test/resources/boltkit/discover_servers.script b/test/resources/boltkit/discover_servers.script new file mode 100644 index 000000000..50b3d815d --- /dev/null +++ b/test/resources/boltkit/discover_servers.script @@ -0,0 +1,10 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO RUN "RETURN 1 // JavaDriver poll to test connection" {} +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002","127.0.0.1:9003"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} diff --git a/test/resources/boltkit/return_x.script b/test/resources/boltkit/return_x.script new file mode 100644 index 000000000..929e73bf3 --- /dev/null +++ b/test/resources/boltkit/return_x.script @@ -0,0 +1,10 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO RUN "RETURN 1 // JavaDriver poll to test connection" {} +!: AUTO PULL_ALL + +C: RUN "RETURN {x}" {"x": 1} + PULL_ALL +S: SUCCESS {"fields": ["x"]} + RECORD [1] + SUCCESS {} diff --git a/test/v1/boltkit.js b/test/v1/boltkit.js new file mode 100644 index 000000000..f91d21d70 --- /dev/null +++ b/test/v1/boltkit.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2002-2016 "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. + */ + +var childProcess = require("child_process"); + +var BoltKit = function () {}; + +BoltKit.prototype.start = function(script, port) { + var spawn = childProcess.spawn, server, code = -1; + + server = spawn('/usr/local/bin/boltstub', ['-v', port, script]); + server.stdout.on('data', (data) => { + console.log(`${data}`); + }); + server.stderr.on('data', (data) => { + console.log(`${data}`); + }); + + server.on('close', function (c) { + code = c; + }); + + server.on('end', function (data) { + console.log(data); + }); + + server.on('error', function (err) { + console.log('Failed to start child process:' + err); + }); + + var Server = function(){}; + //give process some time to exit + Server.prototype.exit = function(callback) {setTimeout(function(){callback(code);}, 500)}; + + return new Server(); +}; + +//Make sure boltstub is started before running +//user code +BoltKit.prototype.run = function(callback) { + setTimeout(callback, 500); +}; + +module.exports = { + BoltKit: BoltKit +}; + diff --git a/test/v1/direct.driver.test.js b/test/v1/direct.driver.test.js new file mode 100644 index 000000000..614dea6d8 --- /dev/null +++ b/test/v1/direct.driver.test.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2002-2016 "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. + */ + +var neo4j = require("../../lib/v1"); +var boltkit = require('./boltkit'); + +describe('direct driver', function() { + + it('should run query', function (done) { + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/return_x.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt://localhost:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + // Then + session.run("RETURN {x}", {'x': 1}).then(function (res) { + expect(res.records[0].get('x').toInt()).toEqual(1); + session.close(); + driver.close(); + server.exit(function(code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); +}); + diff --git a/test/v1/routing.driver.test.js b/test/v1/routing.driver.test.js new file mode 100644 index 000000000..7b0a0db8b --- /dev/null +++ b/test/v1/routing.driver.test.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2002-2016 "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. + */ + +var neo4j = require("../../lib/v1"); +var boltkit = require('./boltkit'); +xdescribe('routing driver ', function() { + + it('should discover server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/discover_servers.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://localhost:9001", neo4j.auth.basic("neo4j", "neo4j")); + + setTimeout(function () { + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }, 1000); + }); + }); +}); + From 6d16d6042f5917ef0207ca32685c53bec21dde02 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Wed, 5 Oct 2016 19:29:33 +0200 Subject: [PATCH 04/18] Update to babel 6 Updating to babel 6 required significant changes mostly on how we do export and import since babel 6 is more strict than babel 5. --- .babelrc | 3 + gulpfile.js => gulpfile.babel.js | 10 +- package.json | 11 +- src/v1/driver.js | 232 ++++++++++++++++++++-- src/v1/graph-types.js | 2 +- src/v1/index.js | 72 ++++--- src/v1/integer.js | 7 +- src/v1/internal/buf.js | 2 +- src/v1/internal/ch-dummy.js | 9 +- src/v1/internal/ch-websocket.js | 2 +- src/v1/internal/chunking.js | 17 +- src/v1/internal/connector.js | 51 +++-- src/v1/internal/log.js | 8 +- src/v1/internal/packstream.js | 9 +- src/v1/internal/pool.js | 5 +- src/v1/internal/stream-observer.js | 2 +- src/v1/internal/utf8.js | 14 +- src/v1/record.js | 2 +- src/v1/result-summary.js | 5 +- src/v1/result.js | 5 +- src/v1/session.js | 3 +- src/v1/transaction.js | 2 +- test/internal/buf.test.js | 2 +- test/internal/packstream.test.js | 7 +- test/internal/pool.test.js | 2 +- test/internal/tls.test.js | 2 +- test/internal/utf8.test.js | 2 +- test/v1/boltkit.js | 37 ++-- test/v1/direct.driver.test.js | 7 +- test/v1/examples.test.js | 70 ++++--- test/v1/record.test.js | 2 +- test/v1/session.test.js | 2 +- test/v1/tck/steps/matchacceptencesteps.js | 13 +- test/v1/tck/steps/resultapisteps.js | 2 +- test/v1/tck/steps/tyepesystemsteps.js | 24 +-- test/v1/tck/steps/util.js | 60 +++--- 36 files changed, 491 insertions(+), 214 deletions(-) create mode 100644 .babelrc rename gulpfile.js => gulpfile.babel.js (96%) diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..26a660ba5 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "stage-3"] +} diff --git a/gulpfile.js b/gulpfile.babel.js similarity index 96% rename from gulpfile.js rename to gulpfile.babel.js index be6530db4..e48dcc07e 100644 --- a/gulpfile.js +++ b/gulpfile.babel.js @@ -16,6 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +require("babel-polyfill"); var browserify = require('browserify'); var source = require('vinyl-source-stream'); @@ -52,7 +53,8 @@ gulp.task('browser', function(cb){ }); /** Build all-in-one files for use in the browser */ -gulp.task('build-browser', function () { +gulp.task('bu' + + 'ild-browser', function () { var browserOutput = 'lib/browser'; // Our app bundler var appBundler = browserify({ @@ -61,7 +63,7 @@ gulp.task('build-browser', function () { standalone: 'neo4j', packageCache: {} }).transform(babelify.configure({ - ignore: /external/ + presets: ['es2015', 'stage-3'], ignore: /external/ })).bundle(); // Un-minified browser package @@ -98,7 +100,7 @@ gulp.task('build-browser-test', function(){ cache: {}, debug: true }).transform(babelify.configure({ - ignore: /external/ + presets: ['es2015', 'stage-3'], ignore: /external/ })) .bundle(function(err, res){ cb(); @@ -115,7 +117,7 @@ gulp.task('build-browser-test', function(){ var buildNode = function(options) { return gulp.src(options.src) - .pipe(babel({ignore: ['src/external/**/*.js']})) + .pipe(babel({presets: ['es2015', 'stage-3'], ignore: ['src/external/**/*.js']})) .pipe(gulp.dest(options.dest)) }; diff --git a/package.json b/package.json index 35ad5e3a8..8d288f98a 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,17 @@ }, "main": "lib/index.js", "devDependencies": { - "babel": "^5.8.23", - "babelify": "^6.3.0", - "browserify": "^11.0.0", + "babel-core": "^6.17.0", + "babel-polyfill": "^6.16.0", + "babel-preset-es2015": "^6.16.0", + "babel-preset-stage-3": "^6.17.0", + "babelify": "^7.3.0", + "browserify": "^13.1.0", "esdoc": "^0.4.0", "esdoc-importpath-plugin": "0.0.1", "glob": "^5.0.14", "gulp": "^3.9.1", - "gulp-babel": "^5.2.1", + "gulp-babel": "^6.1.2", "gulp-batch": "^1.0.5", "gulp-concat": "^2.6.0", "gulp-cucumber": "0.0.14", diff --git a/src/v1/driver.js b/src/v1/driver.js index b30aa33d9..50d0deb72 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -18,11 +18,14 @@ */ import Session from './session'; -import {Pool} from './internal/pool'; -import {connect} from "./internal/connector"; +import Pool from './internal/pool'; +import Integer from './integer'; +import {connect, scheme} from "./internal/connector"; import StreamObserver from './internal/stream-observer'; -import {VERSION} from '../version'; +import VERSION from '../version'; +import "babel-polyfill"; +let READ = 'READ', WRITE = 'WRITE'; /** * A driver maintains one or more {@link Session sessions} with a remote * Neo4j instance. Through the {@link Session sessions} you can send statements @@ -53,7 +56,7 @@ class Driver { this._pool = new Pool( this._createConnection.bind(this), this._destroyConnection.bind(this), - this._validateConnection.bind(this), + Driver._validateConnection.bind(this), config.connectionPoolSize ); } @@ -63,7 +66,7 @@ class Driver { * @return {Connection} new connector-api session instance, a low level session API. * @access private */ - _createConnection( release ) { + _createConnection(release) { let sessionId = this._sessionIdGenerator++; let streamObserver = new _ConnectionStreamObserver(this); let conn = connect(this._url, this._config); @@ -80,7 +83,7 @@ class Driver { * @return {boolean} true if the connection is open * @access private **/ - _validateConnection( conn ) { + static _validateConnection(conn) { return conn.isOpen(); } @@ -89,7 +92,7 @@ class Driver { * @return {Session} new session. * @access private */ - _destroyConnection( conn ) { + _destroyConnection(conn) { delete this._openSessions[conn._id]; conn.close(); } @@ -109,7 +112,11 @@ class Driver { */ session() { let conn = this._pool.acquire(this._url); - return new Session( conn, (cb) => { + return this._createSession(conn); + } + + _createSession(conn) { + return new Session(conn, (cb) => { // This gets called on Session#close(), and is where we return // the pooled 'connection' instance. @@ -126,7 +133,9 @@ class Driver { conn._release(); // Call user callback - if(cb) { cb(); } + if (cb) { + cb(); + } }); } @@ -144,6 +153,191 @@ class Driver { } } +class RoundRobinArray { + constructor(items) { + this._items = items || []; + this._index = 0; + } + + hop() { + let elem = this._items[this._index]; + this._index = (this._index + 1) % (this._items.length - 1); + return elem; + } + + push(elem) { + this._items.push(elem); + } + + pushAll(elems) { + Array.prototype.push.apply(this._items, elems); + } + + empty() { + return this._items.length === 0; + } + + clear() { + this._items = []; + this._index = 0; + } + + size() { + return this._items.length; + } + + toArray() { + return this._items; + } + + remove(item) { + let index = this._items.indexOf(item); + while (index != -1) { + this._items.splice(index, 1); + if (index < this._index) { + this._index -= 1; + } + //make sure we are in range + this._index %= (this._items.length - 1); + } + } +} + +let GET_SERVERS = "CALL dbms.cluster.routing.getServers"; +class RoutingDriver extends Driver { + + constructor(url, userAgent = 'neo4j-javascript/0.0', token = {}, config = {}) { + super(url, userAgent, token, config); + this._routers = new RoundRobinArray(); + this._routers.push(url); + this._readers = new RoundRobinArray(); + this._writers = new RoundRobinArray(); + this._expires = Date.now(); + this._checkServers(); + } + + _checkServers() { + if (this._expires < Date.now() || + this._routers.empty() || + this._readers.empty() || + this._writers.empty()) { + this._callServers(); + } + } + + async _callServers() { + let seen = this._allServers(); + //clear writers and readers + this._writers.clear(); + this._readers.clear(); + //we have to wait to clear routers until + //we have discovered new ones + let newRouters = new RoundRobinArray(); + let success = false; + + while (!this._routers.empty() && !success) { + let url = this._routers.hop(); + try { + let res = await this._call(url); + if (res.records.length != 1) continue; + let record = res.records[0]; + //Note we are loosing precision here but we are not + //terribly worried since it is only + //for dates more than 140000 years into the future. + this._expires += record.get('ttl').toNumber(); + let servers = record.get('servers'); + for (let i = 0; i <= servers.length; i++) { + let server = servers[i]; + seen.delete(server); + let role = server['role']; + let addresses = server['addresses']; + if (role === 'ROUTE') { + newRouters.push(server); + } else if (role === 'WRITE') { + this._writers.push(server); + } else if (role === 'READ') { + this._readers.push(server); + } + } + + if (newRouters.empty()) continue; + //we have results + this._routers = newRouters(); + //these are no longer valid according to server + let self = this; + seen.forEach((key) => { + self._pool.purge(key); + }); + success = true; + return; + } catch (error) { + //continue + this._forget(url); + console.log(error); + } + } + + if (this.onError) { + this.onError("Server could not perform discovery, please open a new driver with a different seed address."); + } + this.close(); + } + + //TODO make nice, expose constants? + session(mode) { + let conn = this._aquireConnection(mode); + return this._createSession(conn); + } + + _aquireConnection(mode) { + //make sure we have enough servers + this._checkServers(); + + let m = mode || WRITE; + if (m === READ) { + return this._pools.acquire(this._readers.hop()); + } else if (m === WRITE) { + return this._pools.acquire(this._writers.hop()); + } else { + //TODO fail + } + } + + _allServers() { + let seen = new Set(this._routers.toArray()); + let writers = this._writers.toArray() + let readers = this._readers.toArray() + for (let i = 0; i < writers.length; i++) { + seen.add(writers[i]); + } + for (let i = 0; i < readers.length; i++) { + seen.add(writers[i]); + } + return seen; + } + + async _call(url) { + let conn = this._pool.acquire(url); + let session = this._createSession(conn); + console.log("calling " + GET_SERVERS); + return session.run(GET_SERVERS) + .then((res) => { + session.close(); + return res; + }).catch((err) => { + this._forget(url); + return Promise.reject(err); + }); + } + + _forget(url) { + this._pools.purge(url); + this._routers.remove(url); + this._readers.remove(url); + this._writers.remove(url); + } +} + /** Internal stream observer used for connection state */ class _ConnectionStreamObserver extends StreamObserver { constructor(driver) { @@ -151,18 +345,20 @@ class _ConnectionStreamObserver extends StreamObserver { this._driver = driver; this._hasFailed = false; } + onError(error) { if (!this._hasFailed) { super.onError(error); - if(this._driver.onError) { + if (this._driver.onError) { this._driver.onError(error); } this._hasFailed = true; } } + onCompleted(message) { - if(this._driver.onCompleted) { - this._driver.onCompleted(message); + if (this._driver.onCompleted) { + this._driver.onCompleted(message); } } } @@ -205,7 +401,8 @@ let USER_AGENT = "neo4j-javascript/" + VERSION; * // * // TRUST_SYSTEM_CA_SIGNED_CERTIFICATES meand that you trust whatever certificates * // are in the default certificate chain of th - * trust: "TRUST_ON_FIRST_USE" | "TRUST_SIGNED_CERTIFICATES" | TRUST_CUSTOM_CA_SIGNED_CERTIFICATES | TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, + * trust: "TRUST_ON_FIRST_USE" | "TRUST_SIGNED_CERTIFICATES" | TRUST_CUSTOM_CA_SIGNED_CERTIFICATES | + * TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, * * // List of one or more paths to trusted encryption certificates. This only * // works in the NodeJS bundle, and only matters if you use "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES". @@ -226,8 +423,13 @@ let USER_AGENT = "neo4j-javascript/" + VERSION; * @param {Object} config Configuration object. See the configuration section above for details. * @returns {Driver} */ -function driver(url, authToken, config={}) { - return new Driver(url, USER_AGENT, authToken, config); +function driver(url, authToken, config = {}) { + let sch = scheme(url); + if (sch === "bolt+routing://") { + return new RoutingDriver(url, USER_AGENT, authToken, config); + } else { + return new Driver(url, USER_AGENT, authToken, config); + } } export {Driver, driver} diff --git a/src/v1/graph-types.js b/src/v1/graph-types.js index 3c7523a23..5acd9f71a 100644 --- a/src/v1/graph-types.js +++ b/src/v1/graph-types.js @@ -171,7 +171,7 @@ class Path { } } -export default { +export { Node, Relationship, UnboundRelationship, diff --git a/src/v1/index.js b/src/v1/index.js index bf02c05f3..413067fd8 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -23,38 +23,52 @@ import {Node, Relationship, UnboundRelationship, PathSegment, Path} from './grap import {Neo4jError} from './error'; import Result from './result'; import ResultSummary from './result-summary'; -import {Record} from './record'; +import Record from './record'; -export default { +const auth ={ + basic: (username, password, realm = undefined) => { + if (realm) { + return {scheme: "basic", principal: username, credentials: password, realm: realm}; + } else { + return {scheme: "basic", principal: username, credentials: password}; + } + }, + custom: (principal, credentials, realm, scheme, parameters = undefined ) => { + if (parameters) { + return {scheme: scheme, principal: principal, credentials: credentials, realm: realm, + parameters: parameters} + } else { + return {scheme: scheme, principal: principal, credentials: credentials, realm: realm} + } + } +}; + +const types ={ + Node, + Relationship, + UnboundRelationship, + PathSegment, + Path, + Result, + ResultSummary, + Record + }; + +const forExport = { driver, int, isInt, Neo4jError, - auth: { - basic: (username, password, realm = undefined) => { - if (realm) { - return {scheme: "basic", principal: username, credentials: password, realm: realm}; - } else { - return {scheme: "basic", principal: username, credentials: password}; - } - }, - custom: (principal, credentials, realm, scheme, parameters = undefined ) => { - if (parameters) { - return {scheme: scheme, principal: principal, credentials: credentials, realm: realm, - parameters: parameters} - } else { - return {scheme: scheme, principal: principal, credentials: credentials, realm: realm} - } - } - }, - types: { - Node, - Relationship, - UnboundRelationship, - PathSegment, - Path, - Result, - ResultSummary, - Record - } + auth, + types +}; + +export { + driver, + int, + isInt, + Neo4jError, + auth, + types } +export default forExport diff --git a/src/v1/integer.js b/src/v1/integer.js index 70b7250a9..3a20d9e52 100644 --- a/src/v1/integer.js +++ b/src/v1/integer.js @@ -817,8 +817,9 @@ let int = Integer.fromValue; */ let isInt = Integer.isInteger; -export default { - Integer, +export { int, - isInt + isInt, } + +export default Integer diff --git a/src/v1/internal/buf.js b/src/v1/internal/buf.js index 95d93bf6e..d8800e125 100644 --- a/src/v1/internal/buf.js +++ b/src/v1/internal/buf.js @@ -579,7 +579,7 @@ function alloc (size) { return new _DefaultBuffer(size); } -export default { +export { BaseBuffer, HeapBuffer, SliceBuffer, diff --git a/src/v1/internal/ch-dummy.js b/src/v1/internal/ch-dummy.js index 6fb55c711..f08fe6a81 100644 --- a/src/v1/internal/ch-dummy.js +++ b/src/v1/internal/ch-dummy.js @@ -23,7 +23,7 @@ const observer = { updateInstance: (instance) => { observer.instance = instance } -} +}; class DummyChannel { constructor(opts) { @@ -48,7 +48,6 @@ class DummyChannel { } } -export default { - channel: DummyChannel, - observer: observer -} +const channel = DummyChannel; + +export { channel, observer } diff --git a/src/v1/internal/ch-websocket.js b/src/v1/internal/ch-websocket.js index 277a70fc0..684ba48db 100644 --- a/src/v1/internal/ch-websocket.js +++ b/src/v1/internal/ch-websocket.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import {debug} from "./log"; +import debug from "./log"; import {HeapBuffer} from "./buf"; import {newError} from './../error'; import {isLocalHost, ENCRYPTION_NON_LOCAL, ENCRYPTION_ON, ENCRYPTION_OFF} from './util'; diff --git a/src/v1/internal/chunking.js b/src/v1/internal/chunking.js index 9d58b07c7..85141026d 100644 --- a/src/v1/internal/chunking.js +++ b/src/v1/internal/chunking.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import buf from './buf'; +import {alloc, BaseBuffer, CombinedBuffer} from './buf'; let _CHUNK_HEADER_SIZE = 2, @@ -28,12 +28,12 @@ let * Looks like a writable buffer, chunks output transparently into a channel below. * @access private */ -class Chunker extends buf.BaseBuffer { +class Chunker extends BaseBuffer { constructor(channel, bufferSize) { super(0); this._bufferSize = bufferSize || _DEFAULT_BUFFER_SIZE; this._ch = channel; - this._buffer = buf.alloc(this._bufferSize); + this._buffer = alloc(this._bufferSize); this._currentChunkStart = 0; this._chunkOpen = false; } @@ -80,7 +80,7 @@ class Chunker extends buf.BaseBuffer { this._ch.write(out.getSlice(0, out.position)); // Alloc a new output buffer. We assume we're using NodeJS's buffer pooling under the hood here! - this._buffer = buf.alloc(this._bufferSize); + this._buffer = alloc(this._bufferSize); this._chunkOpen = false; } return this; @@ -180,7 +180,7 @@ class Dechunker { if (this._currentMessage.length == 1) { message = this._currentMessage[0]; } else { - message = new buf.CombinedBuffer( this._currentMessage ); + message = new CombinedBuffer( this._currentMessage ); } this._currentMessage = []; this.onmessage(message); @@ -198,8 +198,7 @@ class Dechunker { } } - -export default { - Chunker: Chunker, - Dechunker: Dechunker +export { + Chunker, + Dechunker } \ No newline at end of file diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index d4c19d599..60c367a88 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -19,11 +19,11 @@ import WebSocketChannel from "./ch-websocket"; import NodeChannel from "./ch-node"; -import chunking from "./chunking"; +import {Dechunker, Chunker} from "./chunking"; import hasFeature from "./features"; -import packstream from "./packstream"; +import {Packer,Unpacker} from "./packstream"; import {alloc, CombinedBuffer} from "./buf"; -import GraphType from '../graph-types'; +import {Node, Relationship, UnboundRelationship, Path, PathSegment} from '../graph-types'; import {int, isInt} from '../integer'; import {newError} from './../error'; import {ENCRYPTION_NON_LOCAL, ENCRYPTION_OFF, shouldEncrypt} from './util'; @@ -63,13 +63,26 @@ MAGIC_PREAMBLE = 0x6060B017, DEBUG = false; let URLREGEX = new RegExp([ - "[^/]+//", // scheme + "([^/]+//)", // scheme "(([^:/?#]*)", // hostname "(?::([0-9]+))?)", // port (optional) ".*"].join("")); // everything else function host( url ) { - return url.match( URLREGEX )[2]; + return url.match( URLREGEX )[3]; +} + +function port( url ) { + return url.match( URLREGEX )[4]; +} + +function scheme( url ) { + let scheme = url.match( URLREGEX )[1]; + if (scheme) { + return scheme.toLowerCase(); + } + + return scheme; } /** @@ -86,9 +99,6 @@ function log(actor, msg) { } } -function port( url ) { - return url.match( URLREGEX )[3]; -} function NO_OP(){} @@ -101,14 +111,14 @@ let NO_OP_OBSERVER = { /** Maps from packstream structures to Neo4j domain objects */ let _mappers = { node : ( unpacker, buf ) => { - return new GraphType.Node( + return new Node( unpacker.unpack(buf), // Identity unpacker.unpack(buf), // Labels unpacker.unpack(buf) // Properties ); }, rel : ( unpacker, buf ) => { - return new GraphType.Relationship( + return new Relationship( unpacker.unpack(buf), // Identity unpacker.unpack(buf), // Start Node Identity unpacker.unpack(buf), // End Node Identity @@ -117,7 +127,7 @@ let _mappers = { ); }, unboundRel : ( unpacker, buf ) => { - return new GraphType.UnboundRelationship( + return new UnboundRelationship( unpacker.unpack(buf), // Identity unpacker.unpack(buf), // Type unpacker.unpack(buf) // Properties @@ -136,7 +146,7 @@ let _mappers = { rel; if (relIndex > 0) { rel = rels[relIndex - 1]; - if( rel instanceof GraphType.UnboundRelationship ) { + if( rel instanceof UnboundRelationship ) { // To avoid duplication, relationships in a path do not contain // information about their start and end nodes, that's instead // inferred from the path sequence. This is us inferring (and, @@ -145,16 +155,16 @@ let _mappers = { } } else { rel = rels[-relIndex - 1]; - if( rel instanceof GraphType.UnboundRelationship ) { + if( rel instanceof UnboundRelationship ) { // See above rels[-relIndex - 1] = rel = rel.bind(nextNode.identity, prevNode.identity); } } // Done hydrating one path segment. - segments.push( new GraphType.PathSegment( prevNode, rel, nextNode ) ); + segments.push( new PathSegment( prevNode, rel, nextNode ) ); prevNode = nextNode; } - return new GraphType.Path(nodes[0], nodes[nodes.length - 1], segments ); + return new Path(nodes[0], nodes[nodes.length - 1], segments ); } }; @@ -189,10 +199,10 @@ class Connection { this._pendingObservers = []; this._currentObserver = undefined; this._ch = channel; - this._dechunker = new chunking.Dechunker(); - this._chunker = new chunking.Chunker( channel ); - this._packer = new packstream.Packer( this._chunker ); - this._unpacker = new packstream.Unpacker(); + this._dechunker = new Dechunker(); + this._chunker = new Chunker( channel ); + this._packer = new Packer( this._chunker ); + this._unpacker = new Unpacker(); this._isHandlingFailure = false; // Set to true on fatal errors, to get this out of session pool. @@ -469,7 +479,8 @@ function connect( url, config = {}) { }), url); } -export default { +export { connect, + scheme, Connection } diff --git a/src/v1/internal/log.js b/src/v1/internal/log.js index ce0d77007..4af6413dd 100644 --- a/src/v1/internal/log.js +++ b/src/v1/internal/log.js @@ -16,7 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export default { - "debug": (val) => { console.log(val); } + +function debug(val) { + console.log(val); } + +export default debug \ No newline at end of file diff --git a/src/v1/internal/packstream.js b/src/v1/internal/packstream.js index b8f36ed17..9d925efe4 100644 --- a/src/v1/internal/packstream.js +++ b/src/v1/internal/packstream.js @@ -17,10 +17,11 @@ * limitations under the License. */ -import {debug} from "./log"; +import debug from "./log"; import {alloc} from "./buf"; import utf8 from "./utf8"; -import {Integer, int} from "../integer"; +import Integer from "../integer"; +import {int} from "../integer"; import {newError} from './../error'; let MAX_CHUNK_SIZE = 16383, @@ -375,8 +376,8 @@ class Unpacker { } } -export default { +export { Packer, Unpacker, Structure -}; +} diff --git a/src/v1/internal/pool.js b/src/v1/internal/pool.js index 85a00816f..f869df51a 100644 --- a/src/v1/internal/pool.js +++ b/src/v1/internal/pool.js @@ -83,6 +83,5 @@ class Pool { } } -export default { - Pool -} +export default Pool + diff --git a/src/v1/internal/stream-observer.js b/src/v1/internal/stream-observer.js index ba06c822d..f28af78e5 100644 --- a/src/v1/internal/stream-observer.js +++ b/src/v1/internal/stream-observer.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import {Record} from "../record"; +import Record from "../record"; /** * Handles a RUN/PULL_ALL, or RUN/DISCARD_ALL requests, maps the responses diff --git a/src/v1/internal/utf8.js b/src/v1/internal/utf8.js index 8230dd874..5913b7ab8 100644 --- a/src/v1/internal/utf8.js +++ b/src/v1/internal/utf8.js @@ -20,7 +20,7 @@ // This module defines a cross-platform UTF-8 encoder and decoder that works // with the Buffer API defined in buf.js -import buf from "./buf"; +import {alloc, NodeBuffer, HeapBuffer, CombinedBuffer} from "./buf"; import {StringDecoder} from 'string_decoder'; import {newError} from './../error'; let platformObj = {}; @@ -34,16 +34,16 @@ try { platformObj = { "encode": function (str) { - return new buf.NodeBuffer(new node.Buffer(str, "UTF-8")); + return new NodeBuffer(new node.Buffer(str, "UTF-8")); }, "decode": function (buffer, length) { - if (buffer instanceof buf.NodeBuffer) { + if (buffer instanceof NodeBuffer) { let start = buffer.position, end = start + length; buffer.position = Math.min(end, buffer.length); return buffer._buffer.toString('utf8', start, end); } - else if (buffer instanceof buf.CombinedBuffer) { + else if (buffer instanceof CombinedBuffer) { let out = streamDecodeCombinedBuffer(buffer, length, (partBuffer) => { return decoder.write(partBuffer._buffer); @@ -69,16 +69,16 @@ try { platformObj = { "encode": function (str) { - return new buf.HeapBuffer(encoder.encode(str).buffer); + return new HeapBuffer(encoder.encode(str).buffer); }, "decode": function (buffer, length) { - if (buffer instanceof buf.HeapBuffer) { + if (buffer instanceof HeapBuffer) { return decoder.decode(buffer.readView(Math.min(length, buffer.length - buffer.position))); } else { // Decoding combined buffer is complicated. For simplicity, for now, // we simply copy the combined buffer into a regular buffer and decode that. - var tmpBuf = buf.alloc(length); + var tmpBuf = alloc(length); for (var i = 0; i < length; i++) { tmpBuf.writeUInt8(buffer.readUInt8()); } diff --git a/src/v1/record.js b/src/v1/record.js index 249e918f6..d96221c43 100644 --- a/src/v1/record.js +++ b/src/v1/record.js @@ -132,4 +132,4 @@ class Record { } } -export {Record} +export default Record diff --git a/src/v1/result-summary.js b/src/v1/result-summary.js index d35e979ff..b02bd7af7 100644 --- a/src/v1/result-summary.js +++ b/src/v1/result-summary.js @@ -261,7 +261,8 @@ const statementType = { SCHEMA_WRITE: 's' }; -export default { - ResultSummary, +export { statementType } + +export default ResultSummary diff --git a/src/v1/result.js b/src/v1/result.js index 285328747..e94a90486 100644 --- a/src/v1/result.js +++ b/src/v1/result.js @@ -17,11 +17,10 @@ * limitations under the License. */ -import {ResultSummary} from './result-summary'; +import ResultSummary from './result-summary'; // Ensure Promise is available -import {polyfill as polyfillPromise} from '../external/es6-promise'; -polyfillPromise(); +import "babel-polyfill"; /** * A stream of {@link Record} representing the result of a statement. diff --git a/src/v1/session.js b/src/v1/session.js index 546190226..d0eabb3a6 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -20,7 +20,8 @@ import StreamObserver from './internal/stream-observer'; import Result from './result'; import Transaction from './transaction'; -import {Integer, int} from "./integer"; +import Integer from "./integer"; +import {int} from "./integer"; import {newError} from "./error"; /** diff --git a/src/v1/transaction.js b/src/v1/transaction.js index d4915ad05..b7524b754 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -42,7 +42,7 @@ class Transaction { /** * Run Cypher statement * Could be called with a statement object i.e.: {statement: "MATCH ...", parameters: {param: 1}} - * or with the statem ent and parameters as separate arguments. + * or with the statement and parameters as separate arguments. * @param {mixed} statement - Cypher statement to execute * @param {Object} parameters - Map with parameters to use in statement * @return {Result} - New Result diff --git a/test/internal/buf.test.js b/test/internal/buf.test.js index b2ada9c15..77e35efdc 100644 --- a/test/internal/buf.test.js +++ b/test/internal/buf.test.js @@ -19,7 +19,7 @@ var alloc = require('../../lib/v1/internal/buf').alloc; var CombinedBuffer = require('../../lib/v1/internal/buf').CombinedBuffer; -var utf8 = require('../../lib/v1/internal/utf8'); +var utf8 = require('../../lib/v1/internal/utf8').default; var Unpacker = require("../../lib/v1/internal/packstream.js").Unpacker; describe('buffers', function() { diff --git a/test/internal/packstream.test.js b/test/internal/packstream.test.js index 9da74986d..ec9a0c42e 100644 --- a/test/internal/packstream.test.js +++ b/test/internal/packstream.test.js @@ -18,12 +18,11 @@ */ var alloc = require('../../lib/v1/internal/buf').alloc, - packstream = require("../../lib/v1/internal/packstream.js"), - integer = require("../../lib/v1/integer.js"), + packstream = require("../../lib/v1/internal/packstream"), + Integer = require("../../lib/v1/integer").default, Packer = packstream.Packer, Unpacker = packstream.Unpacker, - Structure = packstream.Structure, - Integer = integer.Integer; + Structure = packstream.Structure; describe('packstream', function() { diff --git a/test/internal/pool.test.js b/test/internal/pool.test.js index 06471b6ce..b9c9bad40 100644 --- a/test/internal/pool.test.js +++ b/test/internal/pool.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -var Pool = require('../../lib/v1/internal/pool').Pool; +var Pool = require('../../lib/v1/internal/pool').default; describe('Pool', function() { it('allocates if pool is empty', function() { diff --git a/test/internal/tls.test.js b/test/internal/tls.test.js index e0fe8f6f8..dbf40ac24 100644 --- a/test/internal/tls.test.js +++ b/test/internal/tls.test.js @@ -20,7 +20,7 @@ var NodeChannel = require('../../lib/v1/internal/ch-node.js'); var neo4j = require("../../lib/v1"); var fs = require("fs"); var path = require('path'); -var hasFeature = require("../../lib/v1/internal/features"); +var hasFeature = require("../../lib/v1/internal/features").default; var isLocalHost = require("../../lib/v1/internal/util").isLocalHost; describe('trust-signed-certificates', function() { diff --git a/test/internal/utf8.test.js b/test/internal/utf8.test.js index ddd17b767..8ab8b5fc2 100644 --- a/test/internal/utf8.test.js +++ b/test/internal/utf8.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -var utf8 = require('../../lib/v1/internal/utf8'); +var utf8 = require('../../lib/v1/internal/utf8').default; var buffers = require('../../lib/v1/internal/buf'); describe('utf8', function() { diff --git a/test/v1/boltkit.js b/test/v1/boltkit.js index f91d21d70..c2d35b0ad 100644 --- a/test/v1/boltkit.js +++ b/test/v1/boltkit.js @@ -19,27 +19,30 @@ var childProcess = require("child_process"); -var BoltKit = function () {}; +var BoltKit = function (verbose) { + this.verbose = verbose || false; +}; BoltKit.prototype.start = function(script, port) { var spawn = childProcess.spawn, server, code = -1; server = spawn('/usr/local/bin/boltstub', ['-v', port, script]); - server.stdout.on('data', (data) => { - console.log(`${data}`); - }); - server.stderr.on('data', (data) => { - console.log(`${data}`); - }); + if (this.verbose) { + server.stdout.on('data', (data) => { + console.log(`${data}`); + }); + server.stderr.on('data', (data) => { + console.log(`${data}`); + }); + server.on('end', function (data) { + console.log(data); + }); + } server.on('close', function (c) { code = c; }); - server.on('end', function (data) { - console.log(data); - }); - server.on('error', function (err) { console.log('Failed to start child process:' + err); }); @@ -57,7 +60,17 @@ BoltKit.prototype.run = function(callback) { setTimeout(callback, 500); }; +function boltKitSupport() { + try { + var test = childProcess.spawn; + return !!test; + } catch (e) { + return false; + } +} + module.exports = { - BoltKit: BoltKit + BoltKit: BoltKit, + BoltKitSupport: boltKitSupport() }; diff --git a/test/v1/direct.driver.test.js b/test/v1/direct.driver.test.js index 614dea6d8..bf2ab9b45 100644 --- a/test/v1/direct.driver.test.js +++ b/test/v1/direct.driver.test.js @@ -17,12 +17,17 @@ * limitations under the License. */ -var neo4j = require("../../lib/v1"); +var neo4j = require("../../lib/v1").default; var boltkit = require('./boltkit'); describe('direct driver', function() { it('should run query', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given var kit = new boltkit.BoltKit(); var server = kit.start('./test/resources/boltkit/return_x.script', 9001); diff --git a/test/v1/examples.test.js b/test/v1/examples.test.js index 42eb213ef..323afc704 100644 --- a/test/v1/examples.test.js +++ b/test/v1/examples.test.js @@ -29,24 +29,32 @@ var _console = console; */ describe('examples', function() { - var driverGlobal, sessionGlobal, out, console; + var driverGlobal, out, console, originalTimeout; - beforeEach(function(done) { + beforeAll(function () { var neo4j = neo4jv1; + //tag::construct-driver[] - driverGlobal = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j")); + var driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j")); //end::construct-driver[] - sessionGlobal = driverGlobal.session(); + driverGlobal = driver; + }); + beforeEach(function(done) { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; // Override console.log, to assert on stdout output out = []; console = { log: function(msg) { out.push(msg); } }; - - sessionGlobal.run("MATCH (n) DETACH DELETE n").then(done); + var session = driverGlobal.session(); + session.run("MATCH (n) DETACH DELETE n").then(function () { + session.close(); + done(); + }); }); - afterEach(function() { - sessionGlobal.close(); + afterAll(function() { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; driverGlobal.close(); }); @@ -100,14 +108,15 @@ describe('examples', function() { }); it('should document a statement', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::statement[] session .run( "CREATE (person:Person {name: {name}})", {name: "Arthur"} ) // end::statement[] .then( function(result) { var theOnesCreated = result.summary.counters.nodesCreated(); - console.log("There were " + theOnesCreated + " the ones created.") + console.log("There were " + theOnesCreated + " the ones created."); + session.close(); }) .then(function() { expect(out[0]).toBe("There were 1 the ones created."); @@ -116,7 +125,7 @@ describe('examples', function() { }); it('should document a statement without parameters', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::statement-without-parameters[] session .run( "CREATE (p:Person { name: 'Arthur' })" ) @@ -124,6 +133,7 @@ describe('examples', function() { .then( function(result) { var theOnesCreated = result.summary.counters.nodesCreated(); console.log("There were " + theOnesCreated + " the ones created."); + session.close(); }); // Then @@ -134,7 +144,7 @@ describe('examples', function() { }); it('should be able to iterate results', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); session .run( "CREATE (weapon:Weapon { name: 'Sword in the stone' })" ) .then(function() { @@ -163,7 +173,7 @@ describe('examples', function() { }); it('should be able to access records', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); session .run( "CREATE (weapon:Weapon { name: 'Sword in the stone', owner: 'Arthur', material: 'Stone', size: 'Huge' })" ) .then(function() { @@ -198,7 +208,7 @@ describe('examples', function() { }); it('should be able to retain for later processing', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); session .run("CREATE (knight:Person:Knight { name: 'Lancelot', castle: 'Camelot' })") @@ -208,16 +218,17 @@ describe('examples', function() { .run("MATCH (knight:Person:Knight) WHERE knight.castle = {castle} RETURN knight.name AS name", {castle: "Camelot"}) .then(function (result) { var records = []; - for (i = 0; i < result.records.length; i++) { + for (var i = 0; i < result.records.length; i++) { records.push(result.records[i]); } return records; }) .then(function (records) { - for(i = 0; i < records.length; i ++) - { + for(var i = 0; i < records.length; i ++) { console.log(records[i].get("name") + " is a knight of Camelot"); } + session.close(); + }); // end::retain-result[] }); @@ -230,7 +241,7 @@ describe('examples', function() { }); it('should be able to do nested queries', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session();; session .run( "CREATE (knight:Person:Knight { name: 'Lancelot', castle: 'Camelot' })" + "CREATE (king:Person { name: 'Arthur', title: 'King' })" ) @@ -249,6 +260,7 @@ describe('examples', function() { .run("MATCH (:Knight)-[:DEFENDS]->() RETURN count(*)") .then(function (result) { console.log("Count is " + result.records[0].get(0).toInt()); + session.close(); }); }, onError: function(error) { @@ -266,27 +278,30 @@ describe('examples', function() { }); it('should be able to handle cypher error', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::handle-cypher-error[] session .run("Then will cause a syntax error") .catch( function(err) { expect(err.fields[0].code).toBe( "Neo.ClientError.Statement.SyntaxError" ); + session.close(); done(); }); // end::handle-cypher-error[] }); it('should be able to profile', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); session.run("CREATE (:Person {name:'Arthur'})").then(function() { // tag::result-summary-query-profile[] session .run("PROFILE MATCH (p:Person {name: {name}}) RETURN id(p)", {name: "Arthur"}) .then(function (result) { + //_console.log(result.summary.profile); console.log(result.summary.profile); + session.close(); }); // end::result-summary-query-profile[] }); @@ -295,11 +310,11 @@ describe('examples', function() { setTimeout(function() { expect(out.length).toBe(1); done(); - }, 1000); + }, 2000); }); it('should be able to see notifications', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::result-summary-notifications[] session @@ -309,6 +324,7 @@ describe('examples', function() { for (i = 0; i < notifications.length; i++) { console.log(notifications[i].code); } + session.close(); }); // end::result-summary-notifications[] @@ -319,28 +335,26 @@ describe('examples', function() { }); it('should document committing a transaction', function() { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::transaction-commit[] var tx = session.beginTransaction(); tx.run( "CREATE (:Person {name: 'Guinevere'})" ); - tx.commit(); + tx.commit().then(function() {session.close()}); // end::transaction-commit[] }); it('should document rolling back a transaction', function() { - var session = sessionGlobal; + var session = driverGlobal.session();; // tag::transaction-rollback[] var tx = session.beginTransaction(); tx.run( "CREATE (:Person {name: 'Merlin'})" ); - tx.rollback(); + tx.rollback().then(function() {session.close()}); // end::transaction-rollback[] }); it('should document how to require encryption', function() { - var session = sessionGlobal; - var neo4j = neo4jv1; // tag::tls-require-encryption[] var driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { diff --git a/test/v1/record.test.js b/test/v1/record.test.js index c5c5a4a65..c059cdd2e 100644 --- a/test/v1/record.test.js +++ b/test/v1/record.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -var Record = require("../../lib/v1/record").Record; +var Record = require("../../lib/v1/record").default; var Neo4jError = require("../../lib/v1/error").Neo4jError; diff --git a/test/v1/session.test.js b/test/v1/session.test.js index 6c8ceee76..b53f5261c 100644 --- a/test/v1/session.test.js +++ b/test/v1/session.test.js @@ -19,7 +19,7 @@ var neo4j = require("../../lib/v1"); var StatementType = require("../../lib/v1/result-summary").statementType; -var Session = require("../../lib/v1/session"); +var Session = require("../../lib/v1/session").default; describe('session', function () { diff --git a/test/v1/tck/steps/matchacceptencesteps.js b/test/v1/tck/steps/matchacceptencesteps.js index 2e460f4a1..10a259cb7 100644 --- a/test/v1/tck/steps/matchacceptencesteps.js +++ b/test/v1/tck/steps/matchacceptencesteps.js @@ -39,7 +39,7 @@ module.exports = function () { this.Then(/^result:$/, function (table, callback) { this.expectedResults = util.literalTableToTestObject(table.hashes()); var self = this; - var errorCallback = function(err) {callback(new Error("Rejected Promise: " + err))} + var errorCallback = function(err) {callback(new Error("Rejected Promise: " + err))}; var successCallback = function(res) { var givenResults = []; var expectedPrint = printable(self.expectedResults); @@ -99,7 +99,7 @@ module.exports = function () { } function getTestObject(rels) { - result = {}; + var result = {}; rels.forEach(function( rel, key ) { if (typeof rel === "object" && rel instanceof Array) { var relArray = []; @@ -120,13 +120,13 @@ module.exports = function () { return val; } var con = val.constructor.name.toLowerCase(); - if (con === NODE) { + if (con === util.NODE) { return stripNode(val); } - else if (con === RELATIONSHIP) { + else if (con === util.RELATIONSHIP) { return stripRelationship(val); } - else if (con === PATH) { + else if (con === util.PATH) { return stripPath(val); } else { @@ -150,8 +150,9 @@ module.exports = function () { var id = 0; var startid = neo4j.int(path.start.identity.toString()); var segments = path.segments; + var segment; for (var i = 0; i < segments.length; i++) { - var segment = segments[i]; + segment = segments[i]; if (startid.notEquals(segment.start.identity)) { throw new Error("Path segment does not make sense") } diff --git a/test/v1/tck/steps/resultapisteps.js b/test/v1/tck/steps/resultapisteps.js index f06ff73c6..d76b64f09 100644 --- a/test/v1/tck/steps/resultapisteps.js +++ b/test/v1/tck/steps/resultapisteps.js @@ -24,7 +24,7 @@ var util = require("./util") module.exports = function () { this.When(/^the `Statement Result` is consumed a `Result Summary` is returned$/, function (callback) { - self = this; + var self = this; this.rc.then(function(res) { self.summary = res.summary; callback(); diff --git a/test/v1/tck/steps/tyepesystemsteps.js b/test/v1/tck/steps/tyepesystemsteps.js index 0116cf7c8..0cd332781 100644 --- a/test/v1/tck/steps/tyepesystemsteps.js +++ b/test/v1/tck/steps/tyepesystemsteps.js @@ -33,19 +33,19 @@ module.exports = function () { this.Given(/^a List of size (\d+) and type (.*)$/, function (size, type) { var list = []; for(var i = 0; i < size; i++ ) { - if (type.toLowerCase() === STRING) { + if (type.toLowerCase() === util.STRING) { list.push(stringOfSize(3)); } - else if (type.toLowerCase() === INT) { + else if (type.toLowerCase() === util.INT) { list.push(randomInt()); } - else if (type.toLowerCase() === BOOL) { + else if (type.toLowerCase() === util.BOOL) { list.push(randomBool()); } - else if (type.toLowerCase() === FLOAT) { + else if (type.toLowerCase() === util.FLOAT) { list.push(randomFloat()); } - else if (type.toLowerCase() === NULL) { + else if (type.toLowerCase() === util.NULL) { list.push(null); } else { @@ -58,19 +58,19 @@ module.exports = function () { this.Given(/^a Map of size (\d+) and type (.*)$/, function (size, type) { var map = {}; for(var i = 0; i < size; i++ ) { - if (type.toLowerCase() === STRING) { + if (type.toLowerCase() === util.STRING) { map["a" + util.sizeOfObject(this.M)] = stringOfSize(3); } - else if (type.toLowerCase() === INT) { + else if (type.toLowerCase() === util.INT) { map["a" + util.sizeOfObject(this.M)] = randomInt(); } - else if (type.toLowerCase() === BOOL) { + else if (type.toLowerCase() === util.BOOL) { map["a" + util.sizeOfObject(this.M)] = randomBool(); } - else if (type.toLowerCase() === FLOAT) { + else if (type.toLowerCase() === util.FLOAT) { map["a" + util.sizeOfObject(this.M)] = randomFloat(); } - else if (type.toLowerCase() === NULL) { + else if (type.toLowerCase() === util.NULL) { map["a" + util.sizeOfObject(this.M)] = null; } else { @@ -208,10 +208,10 @@ module.exports = function () { return f.replace("+", ""); } else if( jsVal === undefined || jsVal === null) { - return 'Null' + return 'Null'; } else if( typeof jsVal === "object" && jsVal instanceof Array ) { - var list = "[" + var list = "["; for(var i = 0; i < jsVal.length; i++ ) { list += jsToCypherLiteral(jsVal[i]); if ( i < jsVal.length -1) { diff --git a/test/v1/tck/steps/util.js b/test/v1/tck/steps/util.js index d9cccf4d9..cd0fc9aac 100644 --- a/test/v1/tck/steps/util.js +++ b/test/v1/tck/steps/util.js @@ -19,14 +19,14 @@ var neo4j = require("../../../../lib/v1"); -INT = 'integer'; -FLOAT = 'float'; -STRING = 'string'; -BOOL = 'boolean'; -NULL = 'null'; -RELATIONSHIP = 'relationship'; -NODE = 'node'; -PATH = 'path'; +const INT = 'integer'; +const FLOAT = 'float'; +const STRING = 'string'; +const BOOL = 'boolean'; +const NULL = 'null'; +const RELATIONSHIP = 'relationship'; +const NODE = 'node'; +const PATH = 'path'; var neorunPath = './neokit/neorun.py'; var neo4jHome = './build/neo4j'; @@ -35,19 +35,6 @@ var neo4jKey = neo4jHome + '/certificates/neo4j.key'; var childProcess = require("child_process"); var fs = require('fs'); -module.exports = { - literalTableToTestObject: literalTableToTestObject, - literalValueToTestValueNormalIntegers : literalValueToTestValueNormalIntegers, - literalValueToTestValue: literalValueToTestValue, - compareValues: compareValues, - sizeOfObject: sizeOfObject, - clone: clone, - printable: printable, - changeCertificates: changeCertificates, - restart: restart, - neo4jCert: neo4jCert -}; - function literalTableToTestObject(literalResults) { var resultValues = []; for ( var i = 0 ; i < literalResults.length ; i++) { @@ -57,7 +44,7 @@ function literalTableToTestObject(literalResults) { } function literalLineToObjects(resultRow) { - resultObject = {}; + var resultObject = {}; for ( var key in resultRow) { resultObject[key] = literalValueToTestValue(resultRow[key]); @@ -177,7 +164,7 @@ function createPath(val) { path.segments = []; if (entities.length > 2) { for (var i = 0; i < entities.length-2; i+=2) { - segment = {"start": entities[i], + var segment = {"start": entities[i], "end": entities[i+2], "relationship": entities[i+1]}; path.segments.push(segment); @@ -206,7 +193,7 @@ function parseNodesAndRelationshipsFromPath(val) { function parseMap(val, bigInt) { if (bigInt) { - return properties = JSON.parse(val, function(k, v) { + return JSON.parse(val, function(k, v) { if (Number.isInteger(v)) { return neo4j.int(v); } @@ -214,7 +201,7 @@ function parseMap(val, bigInt) { }); } else { - return properties = JSON.parse(val); + return JSON.parse(val); } } @@ -256,7 +243,7 @@ function getLabels(val) { if ( val.indexOf(":") < 0) { return []; } - labels = val.split(":") + var labels = val.split(":") labels.splice(0, labels.length-1); return labels; } @@ -384,3 +371,24 @@ var runScript = function(cmd) { throw "Script finished with code " + code } }; + +module.exports = { + literalTableToTestObject: literalTableToTestObject, + literalValueToTestValueNormalIntegers : literalValueToTestValueNormalIntegers, + literalValueToTestValue: literalValueToTestValue, + compareValues: compareValues, + sizeOfObject: sizeOfObject, + clone: clone, + printable: printable, + changeCertificates: changeCertificates, + restart: restart, + neo4jCert: neo4jCert, + INT: INT, + FLOAT: FLOAT, + STRING: STRING, + BOOL: BOOL, + NULL: NULL, + NODE: NODE, + RELATIONSHIP: RELATIONSHIP, + PATH: PATH +}; From 960d12561d82c2e6f7a0c6876a0a1e88613afd3b Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Sat, 8 Oct 2016 16:59:04 +0200 Subject: [PATCH 05/18] Refactor Session and Transaction maintains a promise Instead of keeping a connection `Session` and `Transaction` now keep a promise to a connection instead of a resolved connection. --- src/v1/driver.js | 103 ++++++++++++++++++------ src/v1/session.js | 22 +++-- src/v1/transaction.js | 49 ++++++----- test/v1/boltkit.js | 4 +- test/v1/driver.test.js | 4 + test/v1/routing.driver.test.js | 16 ++-- test/v1/tck/steps/erroreportingsteps.js | 13 +-- test/v1/transaction.test.js | 2 +- 8 files changed, 139 insertions(+), 74 deletions(-) diff --git a/src/v1/driver.js b/src/v1/driver.js index 50d0deb72..146678c09 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -116,7 +116,7 @@ class Driver { } _createSession(conn) { - return new Session(conn, (cb) => { + return new Session(new Promise((resolve, reject) => resolve(conn)), (cb) => { // This gets called on Session#close(), and is where we return // the pooled 'connection' instance. @@ -204,6 +204,47 @@ class RoundRobinArray { } let GET_SERVERS = "CALL dbms.cluster.routing.getServers"; + +class ClusterView { + constructor(expires, routers, readers, writers) { + this.expires = expires; + this.routers = routers; + this.readers = readers; + this.routers = writers; + } +} + +function newClusterView(session) { + return session.run(GET_SERVERS) + .then((res) => { + session.close(); + let record = res.records[0]; + //Note we are loosing precision here but we are not + //terribly worried since it is only + //for dates more than 140000 years into the future. + let expires = record.get('ttl').toNumber(); + let servers = record.get('servers'); + let routers = new RoundRobinArray(); + let readers = new RoundRobinArray(); + let writers = new RoundRobinArray(); + for (let i = 0; i <= servers.length; i++) { + let server = servers[i]; + + let role = server['role']; + let addresses = server['addresses']; + if (role === 'ROUTE') { + routers.pushAll(addresses); + } else if (role === 'WRITE') { + writers.pushAll(addresses); + } else if (role === 'READ') { + readers.pushAll(addresses); + } + } + + return new ClusterView(expires, routers, readers, writers); + }); +} + class RoutingDriver extends Driver { constructor(url, userAgent = 'neo4j-javascript/0.0', token = {}, config = {}) { @@ -213,15 +254,25 @@ class RoutingDriver extends Driver { this._readers = new RoundRobinArray(); this._writers = new RoundRobinArray(); this._expires = Date.now(); - this._checkServers(); } - _checkServers() { + //TODO make nice, expose constants? + session(mode) { + //Check so that we have servers available + this._checkServers().then( () => { + let conn = this._acquireConnection(mode); + return this._createSession(conn); + }); + } + + async _checkServers() { if (this._expires < Date.now() || this._routers.empty() || this._readers.empty() || this._writers.empty()) { - this._callServers(); + return await this._callServers(); + } else { + return new Promise((resolve, reject) => resolve(false)); } } @@ -239,6 +290,7 @@ class RoutingDriver extends Driver { let url = this._routers.hop(); try { let res = await this._call(url); + console.log("got result"); if (res.records.length != 1) continue; let record = res.records[0]; //Note we are loosing precision here but we are not @@ -246,9 +298,11 @@ class RoutingDriver extends Driver { //for dates more than 140000 years into the future. this._expires += record.get('ttl').toNumber(); let servers = record.get('servers'); + console.log(servers); for (let i = 0; i <= servers.length; i++) { let server = servers[i]; - seen.delete(server); + seen.remove(server); + let role = server['role']; let addresses = server['addresses']; if (role === 'ROUTE') { @@ -266,38 +320,33 @@ class RoutingDriver extends Driver { //these are no longer valid according to server let self = this; seen.forEach((key) => { - self._pool.purge(key); + console.log("remove seen"); + self._pools.purge(key); }); success = true; - return; + return new Promise((resolve, reject) => resolve(true)); } catch (error) { //continue - this._forget(url); console.log(error); + this._forget(url); } } + let errorMsg = "Server could not perform discovery, please open a new driver with a different seed address."; if (this.onError) { - this.onError("Server could not perform discovery, please open a new driver with a different seed address."); + this.onError(errorMsg); } - this.close(); - } - //TODO make nice, expose constants? - session(mode) { - let conn = this._aquireConnection(mode); - return this._createSession(conn); + return new Promise((resolve, reject) => reject(errorMsg)); } - _aquireConnection(mode) { + _acquireConnection(mode) { //make sure we have enough servers - this._checkServers(); - let m = mode || WRITE; if (m === READ) { - return this._pools.acquire(this._readers.hop()); + return this._pool.acquire(this._readers.hop()); } else if (m === WRITE) { - return this._pools.acquire(this._writers.hop()); + return this._pool.acquire(this._writers.hop()); } else { //TODO fail } @@ -305,8 +354,8 @@ class RoutingDriver extends Driver { _allServers() { let seen = new Set(this._routers.toArray()); - let writers = this._writers.toArray() - let readers = this._readers.toArray() + let writers = this._writers.toArray(); + let readers = this._readers.toArray(); for (let i = 0; i < writers.length; i++) { seen.add(writers[i]); } @@ -319,18 +368,19 @@ class RoutingDriver extends Driver { async _call(url) { let conn = this._pool.acquire(url); let session = this._createSession(conn); - console.log("calling " + GET_SERVERS); return session.run(GET_SERVERS) .then((res) => { session.close(); return res; }).catch((err) => { + console.log(err); this._forget(url); return Promise.reject(err); }); } _forget(url) { + console.log("forget"); this._pools.purge(url); this._routers.remove(url); this._readers.remove(url); @@ -426,9 +476,12 @@ let USER_AGENT = "neo4j-javascript/" + VERSION; function driver(url, authToken, config = {}) { let sch = scheme(url); if (sch === "bolt+routing://") { - return new RoutingDriver(url, USER_AGENT, authToken, config); - } else { + return new RoutingDriver(url, USER_AGENT, authToken, config); + } else if (sch === "bolt://") { return new Driver(url, USER_AGENT, authToken, config); + } else { + throw new Error("Unknown scheme: " + sch); + } } diff --git a/src/v1/session.js b/src/v1/session.js index d0eabb3a6..845eced2c 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -33,19 +33,15 @@ import {newError} from "./error"; class Session { /** * @constructor - * @param {Connection} conn - A connection to use + * @param {Promise} connectionPromise - A connection to use * @param {function()} onClose - Function to be called on connection close */ - constructor( conn, onClose ) { - this._conn = conn; + constructor( connectionPromise, onClose ) { + this._connectionPromise = connectionPromise; this._onClose = onClose; this._hasTx = false; } - isEncrypted() { - return this._conn.isEncrypted(); - } - /** * Run Cypher statement * Could be called with a statement object i.e.: {statement: "MATCH ...", parameters: {param: 1}} @@ -61,9 +57,11 @@ class Session { } let streamObserver = new _RunObserver(); if (!this._hasTx) { - this._conn.run(statement, parameters, streamObserver); - this._conn.pullAll(streamObserver); - this._conn.sync(); + this._connectionPromise.then((conn) => { + conn.run(statement, parameters, streamObserver); + conn.pullAll(streamObserver); + conn.sync(); + }).catch((err) => streamObserver.onError(err)); } else { streamObserver.onError(newError("Statements cannot be run directly on a " + "session with an open transaction; either run from within the " @@ -82,13 +80,13 @@ class Session { */ beginTransaction() { if (this._hasTx) { - throw new newError("You cannot begin a transaction on a session with an " + throw newError("You cannot begin a transaction on a session with an " + "open transaction; either run from within the transaction or use a " + "different session.") } this._hasTx = true; - return new Transaction(this._conn, () => {this._hasTx = false}); + return new Transaction(this._connectionPromise, () => {this._hasTx = false}); } /** diff --git a/src/v1/transaction.js b/src/v1/transaction.js index b7524b754..1302ec60e 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -27,14 +27,17 @@ import Result from './result'; class Transaction { /** * @constructor - * @param {Connection} conn - A connection to use + * @param {Promise} connectionPromise - A connection to use * @param {function()} onClose - Function to be called when transaction is committed or rolled back. */ - constructor(conn, onClose) { - this._conn = conn; + constructor(connectionPromise, onClose) { + this._connectionPromise = connectionPromise; let streamObserver = new _TransactionStreamObserver(this); - this._conn.run("BEGIN", {}, streamObserver); - this._conn.discardAll(streamObserver); + this._connectionPromise.then((conn) => { + conn.run("BEGIN", {}, streamObserver); + conn.discardAll(streamObserver); + }).catch(streamObserver.onError); + this._state = _states.ACTIVE; this._onClose = onClose; } @@ -52,7 +55,7 @@ class Transaction { parameters = statement.parameters || {}; statement = statement.text; } - return this._state.run(this._conn, new _TransactionStreamObserver(this), statement, parameters); + return this._state.run(this._connectionPromise, new _TransactionStreamObserver(this), statement, parameters); } /** @@ -63,7 +66,7 @@ class Transaction { * @returns {Result} - New Result */ commit() { - let committed = this._state.commit(this._conn, new _TransactionStreamObserver(this)); + let committed = this._state.commit(this._connectionPromise, new _TransactionStreamObserver(this)); this._state = committed.state; //clean up this._onClose(); @@ -79,7 +82,7 @@ class Transaction { * @returns {Result} - New Result */ rollback() { - let committed = this._state.rollback(this._conn, new _TransactionStreamObserver(this)); + let committed = this._state.rollback(this._connectionPromise, new _TransactionStreamObserver(this)); this._state = committed.state; //clean up this._onClose(); @@ -114,17 +117,20 @@ class _TransactionStreamObserver extends StreamObserver { let _states = { //The transaction is running with no explicit success or failure marked ACTIVE: { - commit: (conn, observer) => { - return {result: _runDiscardAll("COMMIT", conn, observer), + commit: (connectionPromise, observer) => { + return {result: _runDiscardAll("COMMIT", connectionPromise, observer), state: _states.SUCCEEDED} }, - rollback: (conn, observer) => { - return {result: _runDiscardAll("ROLLBACK", conn, observer), state: _states.ROLLED_BACK}; + rollback: (connectionPromise, observer) => { + return {result: _runDiscardAll("ROLLBACK", connectionPromise, observer), state: _states.ROLLED_BACK}; }, - run: (conn, observer, statement, parameters) => { - conn.run( statement, parameters || {}, observer ); - conn.pullAll( observer ); - conn.sync(); + run: (connectionPromise, observer, statement, parameters) => { + connectionPromise.then((conn) => { + conn.run( statement, parameters || {}, observer ); + conn.pullAll( observer ); + conn.sync(); + }).catch(observer.onError); + return new Result( observer, statement, parameters ); } }, @@ -196,10 +202,13 @@ let _states = { } }; -function _runDiscardAll(msg, conn, observer) { - conn.run(msg, {}, observer ); - conn.discardAll(observer); - conn.sync(); +function _runDiscardAll(msg, connectionPromise, observer) { + connectionPromise.then((conn) => { + conn.run(msg, {}, observer); + conn.discardAll(observer); + conn.sync(); + }).catch(observer.onError); + return new Result(observer, msg, {}); } diff --git a/test/v1/boltkit.js b/test/v1/boltkit.js index c2d35b0ad..1c71c7c1d 100644 --- a/test/v1/boltkit.js +++ b/test/v1/boltkit.js @@ -49,7 +49,7 @@ BoltKit.prototype.start = function(script, port) { var Server = function(){}; //give process some time to exit - Server.prototype.exit = function(callback) {setTimeout(function(){callback(code);}, 500)}; + Server.prototype.exit = function(callback) {setTimeout(function(){callback(code);}, 1000)}; return new Server(); }; @@ -57,7 +57,7 @@ BoltKit.prototype.start = function(script, port) { //Make sure boltstub is started before running //user code BoltKit.prototype.run = function(callback) { - setTimeout(callback, 500); + setTimeout(callback, 1000); }; function boltKitSupport() { diff --git a/test/v1/driver.test.js b/test/v1/driver.test.js index ef695deb7..4b87e0445 100644 --- a/test/v1/driver.test.js +++ b/test/v1/driver.test.js @@ -48,6 +48,10 @@ describe('driver', function() { driver.session(); }); + it('should handle wrong scheme ', function() { + expect(function(){neo4j.driver("tank://localhost", neo4j.auth.basic("neo4j", "neo4j"))}).toThrow(new Error("Unknown scheme: tank://")); + }); + it('should fail early on wrong credentials', function(done) { // Given var driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "who would use such a password")); diff --git a/test/v1/routing.driver.test.js b/test/v1/routing.driver.test.js index 7b0a0db8b..c09781801 100644 --- a/test/v1/routing.driver.test.js +++ b/test/v1/routing.driver.test.js @@ -27,19 +27,19 @@ xdescribe('routing driver ', function() { return; } // Given - var kit = new boltkit.BoltKit(); + var kit = new boltkit.BoltKit(true); var server = kit.start('./test/resources/boltkit/discover_servers.script', 9001); kit.run(function () { var driver = neo4j.driver("bolt+routing://localhost:9001", neo4j.auth.basic("neo4j", "neo4j")); - - setTimeout(function () { - driver.close(); - server.exit(function (code) { - expect(code).toEqual(0); - done(); + var session = driver.session(); + session.run("MATCH (n) RETURN n.name"). then(function() { + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); }); - }, 1000); }); }); }); diff --git a/test/v1/tck/steps/erroreportingsteps.js b/test/v1/tck/steps/erroreportingsteps.js index 69bcf6c13..fe96a75fd 100644 --- a/test/v1/tck/steps/erroreportingsteps.js +++ b/test/v1/tck/steps/erroreportingsteps.js @@ -36,7 +36,7 @@ module.exports = function () { this.Then(/^it throws a `ClientException`$/, function (table) { var expected = table.rows()[0][0]; - if (this.error === undefined) { + if (!this.error) { throw new Error("Exepcted an error but got none.") } if (this.error.message.indexOf(expected) != 0) { @@ -70,11 +70,12 @@ module.exports = function () { }); this.When(/^I set up a driver with wrong scheme$/, function (callback) { - var self = this; - var driver = neo4j.driver("wrong://localhost:7474", neo4j.auth.basic("neo4j", "neo4j")); - driver.session(); - driver.onError = function (error) { self.error = error; callback()}; - driver.close(); + try { + neo4j.driver("wrong://localhost:7474", neo4j.auth.basic("neo4j", "neo4j")); + } catch (e){ + this.error = e; + callback(); + } }); }; diff --git a/test/v1/transaction.test.js b/test/v1/transaction.test.js index 9a9b9ba73..cdef0e23f 100644 --- a/test/v1/transaction.test.js +++ b/test/v1/transaction.test.js @@ -49,7 +49,7 @@ describe('transaction', function() { expect(result.records[0].get('count(t2)').toInt()) .toBe(1); done(); - }); + }).catch(function (e) {console.log(e)}); }); }); From 591f2a7f41573eacdae84c18df4b66fcfe805df4 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Tue, 11 Oct 2016 08:59:29 +0200 Subject: [PATCH 06/18] Basic support for routing. Support routing on session acquistion. --- src/v1/driver.js | 262 +++++++-------- src/v1/error.js | 6 +- src/v1/index.js | 15 +- src/v1/internal/ch-node.js | 8 +- src/v1/internal/connector.js | 31 +- src/v1/internal/pool.js | 2 +- test/internal/pool.test.js | 34 +- .../boltkit/acquire_endpoints.script | 9 + test/resources/boltkit/dead_server.script | 7 + .../boltkit/discover_new_servers.script | 13 + .../resources/boltkit/discover_servers.script | 6 +- .../handle_empty_get_servers_response.script | 8 + test/resources/boltkit/read_server.script | 11 + test/resources/boltkit/write_server.script | 8 + test/v1/routing.driver.test.js | 300 +++++++++++++++++- 15 files changed, 526 insertions(+), 194 deletions(-) create mode 100644 test/resources/boltkit/acquire_endpoints.script create mode 100644 test/resources/boltkit/dead_server.script create mode 100644 test/resources/boltkit/discover_new_servers.script create mode 100644 test/resources/boltkit/handle_empty_get_servers_response.script create mode 100644 test/resources/boltkit/read_server.script create mode 100644 test/resources/boltkit/write_server.script diff --git a/src/v1/driver.js b/src/v1/driver.js index 146678c09..411a9a9f2 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -20,9 +20,10 @@ import Session from './session'; import Pool from './internal/pool'; import Integer from './integer'; -import {connect, scheme} from "./internal/connector"; +import {connect, parseScheme, parseUrl} from "./internal/connector"; import StreamObserver from './internal/stream-observer'; import VERSION from '../version'; +import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from "./error"; import "babel-polyfill"; let READ = 'READ', WRITE = 'WRITE'; @@ -66,10 +67,10 @@ class Driver { * @return {Connection} new connector-api session instance, a low level session API. * @access private */ - _createConnection(release) { + _createConnection(url, release) { let sessionId = this._sessionIdGenerator++; let streamObserver = new _ConnectionStreamObserver(this); - let conn = connect(this._url, this._config); + let conn = connect(url, this._config); conn.initialize(this._userAgent, this._token, streamObserver); conn._id = sessionId; conn._release = () => release(this._url, conn); @@ -112,11 +113,11 @@ class Driver { */ session() { let conn = this._pool.acquire(this._url); - return this._createSession(conn); + return this._createSession(Promise.resolve(conn)); } - _createSession(conn) { - return new Session(new Promise((resolve, reject) => resolve(conn)), (cb) => { + _createSession(connectionPromise) { + return new Session(connectionPromise, (cb) => { // This gets called on Session#close(), and is where we return // the pooled 'connection' instance. @@ -126,11 +127,14 @@ class Driver { // Queue up a 'reset', to ensure the next user gets a clean // session to work with. - conn.reset(); - conn.sync(); + connectionPromise.then( (conn) => { + conn.reset(); + conn.sync(); + + // Return connection to the pool + conn._release(); + }); - // Return connection to the pool - conn._release(); // Call user callback if (cb) { @@ -161,7 +165,11 @@ class RoundRobinArray { hop() { let elem = this._items[this._index]; - this._index = (this._index + 1) % (this._items.length - 1); + if (this._items.length === 0) { + this._index = 0; + } else { + this._index = (this._index + 1) % (this._items.length); + } return elem; } @@ -198,7 +206,11 @@ class RoundRobinArray { this._index -= 1; } //make sure we are in range - this._index %= (this._items.length - 1); + if (this._items.length === 0) { + this._index = 0; + } else { + this._index %= this._items.length; + } } } } @@ -206,11 +218,38 @@ class RoundRobinArray { let GET_SERVERS = "CALL dbms.cluster.routing.getServers"; class ClusterView { - constructor(expires, routers, readers, writers) { - this.expires = expires; - this.routers = routers; - this.readers = readers; - this.routers = writers; + constructor(routers, readers, writers, expires) { + this.routers = routers || new RoundRobinArray(); + this.readers = readers || new RoundRobinArray(); + this.writers = writers || new RoundRobinArray(); + this._expires = expires || -1; + + } + + needsUpdate() { + return this._expires < Date.now() || + this.routers.empty() || + this.readers.empty() || + this.writers.empty(); + } + + all() { + let seen = new Set(this.routers.toArray()); + let writers = this.writers.toArray(); + let readers = this.readers.toArray(); + for (let i = 0; i < writers.length; i++) { + seen.add(writers[i]); + } + for (let i = 0; i < readers.length; i++) { + seen.add(readers[i]); + } + return seen; + } + + remove(item) { + this.routers.remove(item); + this.readers.remove(item); + this.writers.remove(item); } } @@ -218,16 +257,19 @@ function newClusterView(session) { return session.run(GET_SERVERS) .then((res) => { session.close(); + if (res.records.length != 1) { + return Promise.reject(newError("Invalid routing response from server", SERVICE_UNAVAILABLE)); + } let record = res.records[0]; - //Note we are loosing precision here but we are not - //terribly worried since it is only - //for dates more than 140000 years into the future. + //Note we are loosing precision here but let's hope that in + //the 140000 years to come before this precision loss + //hits us, that we get native 64 bit integers in javascript let expires = record.get('ttl').toNumber(); let servers = record.get('servers'); let routers = new RoundRobinArray(); let readers = new RoundRobinArray(); let writers = new RoundRobinArray(); - for (let i = 0; i <= servers.length; i++) { + for (let i = 0; i < servers.length; i++) { let server = servers[i]; let role = server['role']; @@ -240,8 +282,7 @@ function newClusterView(session) { readers.pushAll(addresses); } } - - return new ClusterView(expires, routers, readers, writers); + return new ClusterView(routers, readers, writers, expires); }); } @@ -249,142 +290,67 @@ class RoutingDriver extends Driver { constructor(url, userAgent = 'neo4j-javascript/0.0', token = {}, config = {}) { super(url, userAgent, token, config); - this._routers = new RoundRobinArray(); - this._routers.push(url); - this._readers = new RoundRobinArray(); - this._writers = new RoundRobinArray(); - this._expires = Date.now(); + this._clusterView = new ClusterView(new RoundRobinArray([parseUrl(url)])); } - //TODO make nice, expose constants? session(mode) { - //Check so that we have servers available - this._checkServers().then( () => { - let conn = this._acquireConnection(mode); - return this._createSession(conn); - }); + let conn = this._acquireConnection(mode); + return this._createSession(conn); } - async _checkServers() { - if (this._expires < Date.now() || - this._routers.empty() || - this._readers.empty() || - this._writers.empty()) { - return await this._callServers(); + _updatedClusterView() { + if (!this._clusterView.needsUpdate()) { + return Promise.resolve(this._clusterView); } else { - return new Promise((resolve, reject) => resolve(false)); - } - } - - async _callServers() { - let seen = this._allServers(); - //clear writers and readers - this._writers.clear(); - this._readers.clear(); - //we have to wait to clear routers until - //we have discovered new ones - let newRouters = new RoundRobinArray(); - let success = false; - - while (!this._routers.empty() && !success) { - let url = this._routers.hop(); - try { - let res = await this._call(url); - console.log("got result"); - if (res.records.length != 1) continue; - let record = res.records[0]; - //Note we are loosing precision here but we are not - //terribly worried since it is only - //for dates more than 140000 years into the future. - this._expires += record.get('ttl').toNumber(); - let servers = record.get('servers'); - console.log(servers); - for (let i = 0; i <= servers.length; i++) { - let server = servers[i]; - seen.remove(server); - - let role = server['role']; - let addresses = server['addresses']; - if (role === 'ROUTE') { - newRouters.push(server); - } else if (role === 'WRITE') { - this._writers.push(server); - } else if (role === 'READ') { - this._readers.push(server); - } - } - - if (newRouters.empty()) continue; - //we have results - this._routers = newRouters(); - //these are no longer valid according to server - let self = this; - seen.forEach((key) => { - console.log("remove seen"); - self._pools.purge(key); + let routers = this._clusterView.routers; + let acc = Promise.reject(); + for (let i = 0; i < routers.size(); i++) { + acc = acc.catch(() => { + let conn = this._pool.acquire(routers.hop()); + let session = this._createSession(Promise.resolve(conn)); + return newClusterView(session).catch((err) => { + this._forget(conn); + return Promise.reject(err); + }); }); - success = true; - return new Promise((resolve, reject) => resolve(true)); - } catch (error) { - //continue - console.log(error); - this._forget(url); } - } - let errorMsg = "Server could not perform discovery, please open a new driver with a different seed address."; - if (this.onError) { - this.onError(errorMsg); + return acc; } - - return new Promise((resolve, reject) => reject(errorMsg)); + } + _diff(oldView, updatedView) { + let oldSet = oldView.all(); + let newSet = updatedView.all(); + newSet.forEach((item) => { + oldSet.delete(item); + }); + return oldSet; } _acquireConnection(mode) { - //make sure we have enough servers let m = mode || WRITE; - if (m === READ) { - return this._pool.acquire(this._readers.hop()); - } else if (m === WRITE) { - return this._pool.acquire(this._writers.hop()); - } else { - //TODO fail - } - } - - _allServers() { - let seen = new Set(this._routers.toArray()); - let writers = this._writers.toArray(); - let readers = this._readers.toArray(); - for (let i = 0; i < writers.length; i++) { - seen.add(writers[i]); - } - for (let i = 0; i < readers.length; i++) { - seen.add(writers[i]); - } - return seen; - } - - async _call(url) { - let conn = this._pool.acquire(url); - let session = this._createSession(conn); - return session.run(GET_SERVERS) - .then((res) => { - session.close(); - return res; - }).catch((err) => { - console.log(err); - this._forget(url); - return Promise.reject(err); + //make sure we have enough servers + return this._updatedClusterView().then((view) => { + let toRemove = this._diff(this._clusterView, view); + let self = this; + toRemove.forEach((url) => { + self._pool.purge(url); }); + //update our cached view + this._clusterView = view; + if (m === READ) { + return this._pool.acquire(view.readers.hop()); + } else if (m === WRITE) { + return this._pool.acquire(view.writers.hop()); + } else { + return Promise.reject(m + " is not a valid option"); + } + }); } _forget(url) { - console.log("forget"); - this._pools.purge(url); - this._routers.remove(url); - this._readers.remove(url); - this._writers.remove(url); + this._pool.purge(url); + this._clusterView.remove(url); } } @@ -474,15 +440,15 @@ let USER_AGENT = "neo4j-javascript/" + VERSION; * @returns {Driver} */ function driver(url, authToken, config = {}) { - let sch = scheme(url); - if (sch === "bolt+routing://") { - return new RoutingDriver(url, USER_AGENT, authToken, config); - } else if (sch === "bolt://") { - return new Driver(url, USER_AGENT, authToken, config); + let scheme = parseScheme(url); + if (scheme === "bolt+routing://") { + return new RoutingDriver(parseUrl(url), USER_AGENT, authToken, config); + } else if (scheme === "bolt://") { + return new Driver(parseUrl(url), USER_AGENT, authToken, config); } else { - throw new Error("Unknown scheme: " + sch); + throw new Error("Unknown scheme: " + scheme); } } -export {Driver, driver} +export {Driver, driver, READ, WRITE} diff --git a/src/v1/error.js b/src/v1/error.js index 4db0e7147..cf998f80d 100644 --- a/src/v1/error.js +++ b/src/v1/error.js @@ -20,6 +20,8 @@ // A common place for constructing error objects, to keep them // uniform across the driver surface. +let SERVICE_UNAVAILABLE = 'ServiceUnavailable'; +let SESSION_EXPIRED = 'SessionExpired'; function newError(message, code="N/A") { // TODO: Idea is that we can check the cod here and throw sub-classes // of Neo4jError as appropriate @@ -36,5 +38,7 @@ class Neo4jError extends Error { export { newError, - Neo4jError + Neo4jError, + SERVICE_UNAVAILABLE, + SESSION_EXPIRED } diff --git a/src/v1/index.js b/src/v1/index.js index 413067fd8..9f6827629 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -20,10 +20,11 @@ import {int, isInt} from './integer'; import {driver} from './driver'; import {Node, Relationship, UnboundRelationship, PathSegment, Path} from './graph-types' -import {Neo4jError} from './error'; +import {Neo4jError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from './error'; import Result from './result'; import ResultSummary from './result-summary'; import Record from './record'; +import {READ, WRITE} from './driver'; const auth ={ basic: (username, password, realm = undefined) => { @@ -60,7 +61,11 @@ const forExport = { isInt, Neo4jError, auth, - types + types, + READ, + WRITE, + SERVICE_UNAVAILABLE, + SESSION_EXPIRED }; export { @@ -69,6 +74,10 @@ export { isInt, Neo4jError, auth, - types + types, + READ, + WRITE, + SERVICE_UNAVAILABLE, + SESSION_EXPIRED } export default forExport diff --git a/src/v1/internal/ch-node.js b/src/v1/internal/ch-node.js index 00b464501..766ab29d7 100644 --- a/src/v1/internal/ch-node.js +++ b/src/v1/internal/ch-node.js @@ -24,7 +24,7 @@ import path from 'path'; import {EOL} from 'os'; import {NodeBuffer} from './buf'; import {isLocalHost, ENCRYPTION_NON_LOCAL, ENCRYPTION_OFF} from './util'; -import {newError} from './../error'; +import {newError, SESSION_EXPIRED} from './../error'; let _CONNECTION_IDGEN = 0; @@ -300,8 +300,10 @@ class NodeChannel { } _handleConnectionTerminated() { - this._open = false; - this._conn = undefined; + this._error = newError('Connection was closed by server', SESSION_EXPIRED); + if( this.onerror ) { + this.onerror(this._error); + } } isEncrypted() { diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index 60c367a88..fb9d1bc94 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -63,26 +63,26 @@ MAGIC_PREAMBLE = 0x6060B017, DEBUG = false; let URLREGEX = new RegExp([ - "([^/]+//)", // scheme + "([^/]+//)?", // scheme "(([^:/?#]*)", // hostname "(?::([0-9]+))?)", // port (optional) ".*"].join("")); // everything else -function host( url ) { - return url.match( URLREGEX )[3]; +function parseScheme( url ) { + let scheme = url.match(URLREGEX)[1] || ''; + return scheme.toLowerCase(); } -function port( url ) { - return url.match( URLREGEX )[4]; +function parseUrl(url) { + return url.match( URLREGEX )[2]; } -function scheme( url ) { - let scheme = url.match( URLREGEX )[1]; - if (scheme) { - return scheme.toLowerCase(); - } +function parseHost( url ) { + return url.match( URLREGEX )[3]; +} - return scheme; +function parsePort( url ) { + return url.match( URLREGEX )[4]; } /** @@ -468,10 +468,10 @@ class Connection { function connect( url, config = {}) { let Ch = config.channel || Channel; return new Connection( new Ch({ - host: host(url), - port: port(url) || 7687, + host: parseHost(url), + port: parsePort(url) || 7687, // Default to using ENCRYPTION_NON_LOCAL if trust-on-first-use is available - encrypted : shouldEncrypt(config.encrypted, (hasFeature("trust_on_first_use") ? ENCRYPTION_NON_LOCAL : ENCRYPTION_OFF), host(url)), + encrypted : shouldEncrypt(config.encrypted, (hasFeature("trust_on_first_use") ? ENCRYPTION_NON_LOCAL : ENCRYPTION_OFF), parseHost(url)), // Default to using TRUST_ON_FIRST_USE if it is available trust : config.trust || (hasFeature("trust_on_first_use") ? "TRUST_ON_FIRST_USE" : "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES"), trustedCertificates : config.trustedCertificates || [], @@ -481,6 +481,7 @@ function connect( url, config = {}) { export { connect, - scheme, + parseScheme, + parseUrl, Connection } diff --git a/src/v1/internal/pool.js b/src/v1/internal/pool.js index f869df51a..4bc1c89e3 100644 --- a/src/v1/internal/pool.js +++ b/src/v1/internal/pool.js @@ -52,7 +52,7 @@ class Pool { } } - return this._create(this._release); + return this._create(key, this._release); } purge(key) { diff --git a/test/internal/pool.test.js b/test/internal/pool.test.js index b9c9bad40..a18d3657a 100644 --- a/test/internal/pool.test.js +++ b/test/internal/pool.test.js @@ -24,7 +24,7 @@ describe('Pool', function() { // Given var counter = 0; var key = "bolt://localhost:7687"; - var pool = new Pool( function (release) { return new Resource(counter++, release) } ); + var pool = new Pool( function (url, release) { return new Resource(url, counter++, release) } ); // When var r0 = pool.acquire(key); @@ -39,11 +39,11 @@ describe('Pool', function() { // Given a pool that allocates var counter = 0; var key = "bolt://localhost:7687"; - var pool = new Pool( function (release) { return new Resource(counter++, release) } ); + var pool = new Pool( function (url, release) { return new Resource(url, counter++, release) } ); // When var r0 = pool.acquire(key); - r0.close(key); + r0.close(); var r1 = pool.acquire(key); // Then @@ -56,12 +56,12 @@ describe('Pool', function() { var counter = 0; var key1 = "bolt://localhost:7687"; var key2 = "bolt://localhost:7688"; - var pool = new Pool( function (release) { return new Resource(counter++, release) } ); + var pool = new Pool( function (url, release) { return new Resource(url, counter++, release) } ); // When var r0 = pool.acquire(key1); var r1 = pool.acquire(key2); - r0.close(key1); + r0.close(); var r2 = pool.acquire(key1); var r3 = pool.acquire(key2); @@ -78,7 +78,7 @@ describe('Pool', function() { destroyed = []; var key = "bolt://localhost:7687"; var pool = new Pool( - function (release) { return new Resource(counter++, release) }, + function (url, release) { return new Resource(url, counter++, release) }, function (resource) { destroyed.push(resource); }, function (resource) { return true; }, 2 // maxIdle @@ -88,9 +88,9 @@ describe('Pool', function() { var r0 = pool.acquire(key); var r1 = pool.acquire(key); var r2 = pool.acquire(key); - r0.close(key); - r1.close(key); - r2.close(key); + r0.close(); + r1.close(); + r2.close(); // Then expect( destroyed.length ).toBe( 1 ); @@ -103,7 +103,7 @@ describe('Pool', function() { destroyed = []; var key = "bolt://localhost:7687"; var pool = new Pool( - function (release) { return new Resource(counter++, release) }, + function (url, release) { return new Resource(url, counter++, release) }, function (resource) { destroyed.push(resource); }, function (resource) { return false; }, 1000 // maxIdle @@ -112,8 +112,8 @@ describe('Pool', function() { // When var r0 = pool.acquire(key); var r1 = pool.acquire(key); - r0.close(key); - r1.close(key); + r0.close(); + r1.close(); // Then expect( destroyed.length ).toBe( 2 ); @@ -127,15 +127,15 @@ describe('Pool', function() { var counter = 0; var key1 = "bolt://localhost:7687"; var key2 = "bolt://localhost:7688"; - var pool = new Pool( function (release) { return new Resource(counter++, release) }, + var pool = new Pool( function (url, release) { return new Resource(url, counter++, release) }, function (res) {res.destroyed = true; return true} ); // When var r0 = pool.acquire(key1); var r1 = pool.acquire(key2); - r0.close(key1); - r1.close(key2); + r0.close(); + r1.close(); expect(pool.has(key1)).toBe(true); expect(pool.has(key2)).toBe(true); pool.purge(key1); @@ -154,8 +154,8 @@ describe('Pool', function() { }); }); -function Resource( id, release) { +function Resource( key, id, release) { var self = this; this.id = id; - this.close = function(key) { release(key, self); }; + this.close = function() { release(key, self); }; } \ No newline at end of file diff --git a/test/resources/boltkit/acquire_endpoints.script b/test/resources/boltkit/acquire_endpoints.script new file mode 100644 index 000000000..9c92e4863 --- /dev/null +++ b/test/resources/boltkit/acquire_endpoints.script @@ -0,0 +1,9 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/dead_server.script b/test/resources/boltkit/dead_server.script new file mode 100644 index 000000000..037a6be65 --- /dev/null +++ b/test/resources/boltkit/dead_server.script @@ -0,0 +1,7 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "MATCH (n) RETURN n.name" {} +C: PULL_ALL +S: \ No newline at end of file diff --git a/test/resources/boltkit/discover_new_servers.script b/test/resources/boltkit/discover_new_servers.script new file mode 100644 index 000000000..2d5aa55ae --- /dev/null +++ b/test/resources/boltkit/discover_new_servers.script @@ -0,0 +1,13 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9003"], "role": "READ"},{"addresses": ["127.0.0.1:9004","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} +C: RUN "MATCH (n) RETURN n.name" {} + PULL_ALL +S: SUCCESS {"fields": ["n.name"]} + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/discover_servers.script b/test/resources/boltkit/discover_servers.script index 50b3d815d..6f92d458f 100644 --- a/test/resources/boltkit/discover_servers.script +++ b/test/resources/boltkit/discover_servers.script @@ -1,6 +1,5 @@ !: AUTO INIT !: AUTO RESET -!: AUTO RUN "RETURN 1 // JavaDriver poll to test connection" {} !: AUTO PULL_ALL C: RUN "CALL dbms.cluster.routing.getServers" {} @@ -8,3 +7,8 @@ C: RUN "CALL dbms.cluster.routing.getServers" {} S: SUCCESS {"fields": ["ttl", "servers"]} RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002","127.0.0.1:9003"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] SUCCESS {} +C: RUN "MATCH (n) RETURN n.name" {} + PULL_ALL +S: SUCCESS {"fields": ["n.name"]} + SUCCESS {} + diff --git a/test/resources/boltkit/handle_empty_get_servers_response.script b/test/resources/boltkit/handle_empty_get_servers_response.script new file mode 100644 index 000000000..872dffadd --- /dev/null +++ b/test/resources/boltkit/handle_empty_get_servers_response.script @@ -0,0 +1,8 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/read_server.script b/test/resources/boltkit/read_server.script new file mode 100644 index 000000000..0b4d44748 --- /dev/null +++ b/test/resources/boltkit/read_server.script @@ -0,0 +1,11 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "MATCH (n) RETURN n.name" {} + PULL_ALL +S: SUCCESS {"fields": ["n.name"]} + RECORD ["Bob"] + RECORD ["Alice"] + RECORD ["Tina"] + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/write_server.script b/test/resources/boltkit/write_server.script new file mode 100644 index 000000000..993910f6c --- /dev/null +++ b/test/resources/boltkit/write_server.script @@ -0,0 +1,8 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CREATE (n {name:'Bob'})" {} + PULL_ALL +S: SUCCESS {} + SUCCESS {} \ No newline at end of file diff --git a/test/v1/routing.driver.test.js b/test/v1/routing.driver.test.js index c09781801..dbc424514 100644 --- a/test/v1/routing.driver.test.js +++ b/test/v1/routing.driver.test.js @@ -19,7 +19,7 @@ var neo4j = require("../../lib/v1"); var boltkit = require('./boltkit'); -xdescribe('routing driver ', function() { +describe('routing driver ', function() { it('should discover server', function (done) { if (!boltkit.BoltKitSupport) { @@ -27,13 +27,20 @@ xdescribe('routing driver ', function() { return; } // Given - var kit = new boltkit.BoltKit(true); + var kit = new boltkit.BoltKit(); var server = kit.start('./test/resources/boltkit/discover_servers.script', 9001); kit.run(function () { - var driver = neo4j.driver("bolt+routing://localhost:9001", neo4j.auth.basic("neo4j", "neo4j")); - var session = driver.session(); - session.run("MATCH (n) RETURN n.name"). then(function() { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("MATCH (n) RETURN n.name").then(function() { + + // Then + expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]); + expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9002","127.0.0.1:9003"]); + expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); + driver.close(); server.exit(function (code) { expect(code).toEqual(0); @@ -42,5 +49,288 @@ xdescribe('routing driver ', function() { }); }); }); + + it('should discover new servers', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/discover_new_servers.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("MATCH (n) RETURN n.name").then(function() { + + // Then + expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9004","127.0.0.1:9002","127.0.0.1:9003"]); + expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9005","127.0.0.1:9003"]); + expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); + + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + + it('should handle empty response from server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/handle_empty_get_servers_response.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.SERVICE_UNAVAILABLE); + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + + it('should acquire read server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/read_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").then(function(res) { + + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should round-robin among read servers', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer1 = kit.start('./test/resources/boltkit/read_server.script', 9005); + var readServer2 = kit.start('./test/resources/boltkit/read_server.script', 9006); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + session.close(); + session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + session.close(); + + driver.close(); + seedServer.exit(function (code1) { + readServer1.exit(function (code2) { + readServer2.exit(function (code3) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + expect(code3).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should handle missing read server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/dead_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + console.log(err.sessionExpired); + expect(err.code).toEqual(neo4j.SESSION_EXPIRED); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should acquire write server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var writeServer = kit.start('./test/resources/boltkit/write_server.script', 9007); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.WRITE); + session.run("CREATE (n {name:'Bob'})").then(function() { + + // Then + driver.close(); + seedServer.exit(function (code1) { + writeServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should round-robin among write servers', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer1 = kit.start('./test/resources/boltkit/write_server.script', 9007); + var readServer2 = kit.start('./test/resources/boltkit/write_server.script', 9008); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.WRITE); + session.run("CREATE (n {name:'Bob'})").then(function () { + session = driver.session(neo4j.WRITE); + session.run("CREATE (n {name:'Bob'})").then(function () { + // Then + driver.close(); + seedServer.exit(function (code1) { + readServer1.exit(function (code2) { + readServer2.exit(function (code3) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + expect(code3).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should handle missing write server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/dead_server.script', 9007); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.WRITE); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.SESSION_EXPIRED); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should remember endpoints', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/read_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").then(function() { + + // Then + expect(driver._clusterView.routers.toArray()).toEqual(['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003']); + expect(driver._clusterView.readers.toArray()).toEqual(['127.0.0.1:9005', '127.0.0.1:9006']); + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9007', '127.0.0.1:9008']); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); }); From f3e3d011f294a67cd6da7244ecdba097d5277db2 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Tue, 11 Oct 2016 16:38:25 +0200 Subject: [PATCH 07/18] Refactoring: moved classes and functions into separate files --- src/v1/driver.js | 274 +----------------------- src/v1/index.js | 79 ++++++- src/v1/internal/round-robin-array.js | 82 +++++++ src/v1/routing-driver.js | 176 +++++++++++++++ src/v1/session.js | 2 +- test/internal/round-robin-array.test.js | 127 +++++++++++ 6 files changed, 467 insertions(+), 273 deletions(-) create mode 100644 src/v1/internal/round-robin-array.js create mode 100644 src/v1/routing-driver.js create mode 100644 test/internal/round-robin-array.test.js diff --git a/src/v1/driver.js b/src/v1/driver.js index 411a9a9f2..3e6e2a62b 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -20,10 +20,9 @@ import Session from './session'; import Pool from './internal/pool'; import Integer from './integer'; -import {connect, parseScheme, parseUrl} from "./internal/connector"; +import {connect} from "./internal/connector"; import StreamObserver from './internal/stream-observer'; -import VERSION from '../version'; -import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from "./error"; +import {newError} from "./error"; import "babel-polyfill"; let READ = 'READ', WRITE = 'WRITE'; @@ -157,203 +156,6 @@ class Driver { } } -class RoundRobinArray { - constructor(items) { - this._items = items || []; - this._index = 0; - } - - hop() { - let elem = this._items[this._index]; - if (this._items.length === 0) { - this._index = 0; - } else { - this._index = (this._index + 1) % (this._items.length); - } - return elem; - } - - push(elem) { - this._items.push(elem); - } - - pushAll(elems) { - Array.prototype.push.apply(this._items, elems); - } - - empty() { - return this._items.length === 0; - } - - clear() { - this._items = []; - this._index = 0; - } - - size() { - return this._items.length; - } - - toArray() { - return this._items; - } - - remove(item) { - let index = this._items.indexOf(item); - while (index != -1) { - this._items.splice(index, 1); - if (index < this._index) { - this._index -= 1; - } - //make sure we are in range - if (this._items.length === 0) { - this._index = 0; - } else { - this._index %= this._items.length; - } - } - } -} - -let GET_SERVERS = "CALL dbms.cluster.routing.getServers"; - -class ClusterView { - constructor(routers, readers, writers, expires) { - this.routers = routers || new RoundRobinArray(); - this.readers = readers || new RoundRobinArray(); - this.writers = writers || new RoundRobinArray(); - this._expires = expires || -1; - - } - - needsUpdate() { - return this._expires < Date.now() || - this.routers.empty() || - this.readers.empty() || - this.writers.empty(); - } - - all() { - let seen = new Set(this.routers.toArray()); - let writers = this.writers.toArray(); - let readers = this.readers.toArray(); - for (let i = 0; i < writers.length; i++) { - seen.add(writers[i]); - } - for (let i = 0; i < readers.length; i++) { - seen.add(readers[i]); - } - return seen; - } - - remove(item) { - this.routers.remove(item); - this.readers.remove(item); - this.writers.remove(item); - } -} - -function newClusterView(session) { - return session.run(GET_SERVERS) - .then((res) => { - session.close(); - if (res.records.length != 1) { - return Promise.reject(newError("Invalid routing response from server", SERVICE_UNAVAILABLE)); - } - let record = res.records[0]; - //Note we are loosing precision here but let's hope that in - //the 140000 years to come before this precision loss - //hits us, that we get native 64 bit integers in javascript - let expires = record.get('ttl').toNumber(); - let servers = record.get('servers'); - let routers = new RoundRobinArray(); - let readers = new RoundRobinArray(); - let writers = new RoundRobinArray(); - for (let i = 0; i < servers.length; i++) { - let server = servers[i]; - - let role = server['role']; - let addresses = server['addresses']; - if (role === 'ROUTE') { - routers.pushAll(addresses); - } else if (role === 'WRITE') { - writers.pushAll(addresses); - } else if (role === 'READ') { - readers.pushAll(addresses); - } - } - return new ClusterView(routers, readers, writers, expires); - }); -} - -class RoutingDriver extends Driver { - - constructor(url, userAgent = 'neo4j-javascript/0.0', token = {}, config = {}) { - super(url, userAgent, token, config); - this._clusterView = new ClusterView(new RoundRobinArray([parseUrl(url)])); - } - - session(mode) { - let conn = this._acquireConnection(mode); - return this._createSession(conn); - } - - _updatedClusterView() { - if (!this._clusterView.needsUpdate()) { - return Promise.resolve(this._clusterView); - } else { - let routers = this._clusterView.routers; - let acc = Promise.reject(); - for (let i = 0; i < routers.size(); i++) { - acc = acc.catch(() => { - let conn = this._pool.acquire(routers.hop()); - let session = this._createSession(Promise.resolve(conn)); - return newClusterView(session).catch((err) => { - this._forget(conn); - return Promise.reject(err); - }); - }); - } - - return acc; - } - } - _diff(oldView, updatedView) { - let oldSet = oldView.all(); - let newSet = updatedView.all(); - newSet.forEach((item) => { - oldSet.delete(item); - }); - return oldSet; - } - - _acquireConnection(mode) { - let m = mode || WRITE; - //make sure we have enough servers - return this._updatedClusterView().then((view) => { - let toRemove = this._diff(this._clusterView, view); - let self = this; - toRemove.forEach((url) => { - self._pool.purge(url); - }); - //update our cached view - this._clusterView = view; - if (m === READ) { - return this._pool.acquire(view.readers.hop()); - } else if (m === WRITE) { - return this._pool.acquire(view.writers.hop()); - } else { - return Promise.reject(m + " is not a valid option"); - } - }); - } - - _forget(url) { - this._pool.purge(url); - this._clusterView.remove(url); - } -} - /** Internal stream observer used for connection state */ class _ConnectionStreamObserver extends StreamObserver { constructor(driver) { @@ -379,76 +181,8 @@ class _ConnectionStreamObserver extends StreamObserver { } } -let USER_AGENT = "neo4j-javascript/" + VERSION; -/** - * Construct a new Neo4j Driver. This is your main entry point for this - * library. - * - * ## Configuration - * - * This function optionally takes a configuration argument. Available configuration - * options are as follows: - * - * { - * // Encryption level: one of ENCRYPTION_ON, ENCRYPTION_OFF or ENCRYPTION_NON_LOCAL. - * // ENCRYPTION_NON_LOCAL is on by default in modern NodeJS installs, - * // but off by default in the Web Bundle and old (<=1.0.0) NodeJS installs - * // due to technical limitations on those platforms. - * encrypted: ENCRYPTION_ON|ENCRYPTION_OFF|ENCRYPTION_NON_LOCAL - * - * // Trust strategy to use if encryption is enabled. There is no mode to disable - * // trust other than disabling encryption altogether. The reason for - * // this is that if you don't know who you are talking to, it is easy for an - * // attacker to hijack your encrypted connection, rendering encryption pointless. - * // - * // TRUST_ON_FIRST_USE is the default for modern NodeJS deployments, and works - * // similarly to how `ssl` works - the first time we connect to a new host, - * // we remember the certificate they use. If the certificate ever changes, we - * // assume it is an attempt to hijack the connection and require manual intervention. - * // This means that by default, connections "just work" while still giving you - * // good encrypted protection. - * // - * // TRUST_CUSTOM_CA_SIGNED_CERTIFICATES is the classic approach to trust verification - - * // whenever we establish an encrypted connection, we ensure the host is using - * // an encryption certificate that is in, or is signed by, a certificate listed - * // as trusted. In the web bundle, this list of trusted certificates is maintained - * // by the web browser. In NodeJS, you configure the list with the next config option. - * // - * // TRUST_SYSTEM_CA_SIGNED_CERTIFICATES meand that you trust whatever certificates - * // are in the default certificate chain of th - * trust: "TRUST_ON_FIRST_USE" | "TRUST_SIGNED_CERTIFICATES" | TRUST_CUSTOM_CA_SIGNED_CERTIFICATES | - * TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, - * - * // List of one or more paths to trusted encryption certificates. This only - * // works in the NodeJS bundle, and only matters if you use "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES". - * // The certificate files should be in regular X.509 PEM format. - * // For instance, ['./trusted.pem'] - * trustedCertificates: [], - * - * // Path to a file where the driver saves hosts it has seen in the past, this is - * // very similar to the ssl tool's known_hosts file. Each time we connect to a - * // new host, a hash of their certificate is stored along with the domain name and - * // port, and this is then used to verify the host certificate does not change. - * // This setting has no effect unless TRUST_ON_FIRST_USE is enabled. - * knownHosts:"~/.neo4j/known_hosts", - * } - * - * @param {string} url The URL for the Neo4j database, for instance "bolt://localhost" - * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. - * @param {Object} config Configuration object. See the configuration section above for details. - * @returns {Driver} - */ -function driver(url, authToken, config = {}) { - let scheme = parseScheme(url); - if (scheme === "bolt+routing://") { - return new RoutingDriver(parseUrl(url), USER_AGENT, authToken, config); - } else if (scheme === "bolt://") { - return new Driver(parseUrl(url), USER_AGENT, authToken, config); - } else { - throw new Error("Unknown scheme: " + scheme); - } -} +export {Driver, READ, WRITE} -export {Driver, driver, READ, WRITE} +export default Driver diff --git a/src/v1/index.js b/src/v1/index.js index 9f6827629..a2cb00958 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -18,13 +18,16 @@ */ import {int, isInt} from './integer'; -import {driver} from './driver'; import {Node, Relationship, UnboundRelationship, PathSegment, Path} from './graph-types' import {Neo4jError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from './error'; import Result from './result'; import ResultSummary from './result-summary'; import Record from './record'; -import {READ, WRITE} from './driver'; +import {Driver, READ, WRITE} from './driver'; +import RoutingDriver from './routing-driver'; +import VERSION from '../version'; +import {parseScheme, parseUrl} from "./internal/connector"; + const auth ={ basic: (username, password, realm = undefined) => { @@ -43,6 +46,78 @@ const auth ={ } } }; +let USER_AGENT = "neo4j-javascript/" + VERSION; + +/** + * Construct a new Neo4j Driver. This is your main entry point for this + * library. + * + * ## Configuration + * + * This function optionally takes a configuration argument. Available configuration + * options are as follows: + * + * { + * // Encryption level: one of ENCRYPTION_ON, ENCRYPTION_OFF or ENCRYPTION_NON_LOCAL. + * // ENCRYPTION_NON_LOCAL is on by default in modern NodeJS installs, + * // but off by default in the Web Bundle and old (<=1.0.0) NodeJS installs + * // due to technical limitations on those platforms. + * encrypted: ENCRYPTION_ON|ENCRYPTION_OFF|ENCRYPTION_NON_LOCAL + * + * // Trust strategy to use if encryption is enabled. There is no mode to disable + * // trust other than disabling encryption altogether. The reason for + * // this is that if you don't know who you are talking to, it is easy for an + * // attacker to hijack your encrypted connection, rendering encryption pointless. + * // + * // TRUST_ON_FIRST_USE is the default for modern NodeJS deployments, and works + * // similarly to how `ssl` works - the first time we connect to a new host, + * // we remember the certificate they use. If the certificate ever changes, we + * // assume it is an attempt to hijack the connection and require manual intervention. + * // This means that by default, connections "just work" while still giving you + * // good encrypted protection. + * // + * // TRUST_CUSTOM_CA_SIGNED_CERTIFICATES is the classic approach to trust verification - + * // whenever we establish an encrypted connection, we ensure the host is using + * // an encryption certificate that is in, or is signed by, a certificate listed + * // as trusted. In the web bundle, this list of trusted certificates is maintained + * // by the web browser. In NodeJS, you configure the list with the next config option. + * // + * // TRUST_SYSTEM_CA_SIGNED_CERTIFICATES meand that you trust whatever certificates + * // are in the default certificate chain of th + * trust: "TRUST_ON_FIRST_USE" | "TRUST_SIGNED_CERTIFICATES" | TRUST_CUSTOM_CA_SIGNED_CERTIFICATES | + * TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, + * + * // List of one or more paths to trusted encryption certificates. This only + * // works in the NodeJS bundle, and only matters if you use "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES". + * // The certificate files should be in regular X.509 PEM format. + * // For instance, ['./trusted.pem'] + * trustedCertificates: [], + * + * // Path to a file where the driver saves hosts it has seen in the past, this is + * // very similar to the ssl tool's known_hosts file. Each time we connect to a + * // new host, a hash of their certificate is stored along with the domain name and + * // port, and this is then used to verify the host certificate does not change. + * // This setting has no effect unless TRUST_ON_FIRST_USE is enabled. + * knownHosts:"~/.neo4j/known_hosts", + * } + * + * @param {string} url The URL for the Neo4j database, for instance "bolt://localhost" + * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. + * @param {Object} config Configuration object. See the configuration section above for details. + * @returns {Driver} + */ +function driver(url, authToken, config = {}) { + let scheme = parseScheme(url); + if (scheme === "bolt+routing://") { + return new RoutingDriver(parseUrl(url), USER_AGENT, authToken, config); + } else if (scheme === "bolt://") { + return new Driver(parseUrl(url), USER_AGENT, authToken, config); + } else { + throw new Error("Unknown scheme: " + scheme); + + } +} + const types ={ Node, diff --git a/src/v1/internal/round-robin-array.js b/src/v1/internal/round-robin-array.js new file mode 100644 index 000000000..9392f0dc1 --- /dev/null +++ b/src/v1/internal/round-robin-array.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2002-2016 "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. + */ + +/** + * An array that lets you hop through the elements endlessly. + */ +class RoundRobinArray { + constructor(items) { + this._items = items || []; + this._index = 0; + } + + hop() { + let elem = this._items[this._index]; + if (this._items.length === 0) { + this._index = 0; + } else { + this._index = (this._index + 1) % (this._items.length); + } + return elem; + } + + push(elem) { + this._items.push(elem); + } + + pushAll(elems) { + Array.prototype.push.apply(this._items, elems); + } + + empty() { + return this._items.length === 0; + } + + clear() { + this._items = []; + this._index = 0; + } + + size() { + return this._items.length; + } + + toArray() { + return this._items; + } + + remove(item) { + let index = this._items.indexOf(item); + while (index != -1) { + this._items.splice(index, 1); + if (index < this._index) { + this._index -= 1; + } + //make sure we are in range + if (this._items.length === 0) { + this._index = 0; + } else { + this._index %= this._items.length; + } + index = this._items.indexOf(item, index); + } + } +} + +export default RoundRobinArray \ No newline at end of file diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js new file mode 100644 index 000000000..53ad4ab56 --- /dev/null +++ b/src/v1/routing-driver.js @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2002-2016 "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 Session from './session'; +import {Driver, READ, WRITE} from './driver'; +import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from "./error"; +import RoundRobinArray from './internal/round-robin-array'; +import "babel-polyfill"; + + + +/** + * A driver that supports routing in a core-edge cluster. + */ +class RoutingDriver extends Driver { + + constructor(url, userAgent = 'neo4j-javascript/0.0', token = {}, config = {}) { + super(url, userAgent, token, config); + this._clusterView = new ClusterView(new RoundRobinArray([url])); + } + + session(mode) { + let conn = this._acquireConnection(mode); + return this._createSession(conn); + } + + _updatedClusterView() { + if (!this._clusterView.needsUpdate()) { + return Promise.resolve(this._clusterView); + } else { + let routers = this._clusterView.routers; + let acc = Promise.reject(); + for (let i = 0; i < routers.size(); i++) { + acc = acc.catch(() => { + let conn = this._pool.acquire(routers.hop()); + let session = this._createSession(Promise.resolve(conn)); + return newClusterView(session).catch((err) => { + this._forget(conn); + return Promise.reject(err); + }); + }); + } + + return acc; + } + } + _diff(oldView, updatedView) { + let oldSet = oldView.all(); + let newSet = updatedView.all(); + newSet.forEach((item) => { + oldSet.delete(item); + }); + return oldSet; + } + + _acquireConnection(mode) { + let m = mode || WRITE; + //make sure we have enough servers + return this._updatedClusterView().then((view) => { + let toRemove = this._diff(this._clusterView, view); + let self = this; + toRemove.forEach((url) => { + self._pool.purge(url); + }); + //update our cached view + this._clusterView = view; + if (m === READ) { + return this._pool.acquire(view.readers.hop()); + } else if (m === WRITE) { + return this._pool.acquire(view.writers.hop()); + } else { + return Promise.reject(m + " is not a valid option"); + } + }); + } + + _forget(url) { + this._pool.purge(url); + this._clusterView.remove(url); + } +} + + +class ClusterView { + constructor(routers, readers, writers, expires) { + this.routers = routers || new RoundRobinArray(); + this.readers = readers || new RoundRobinArray(); + this.writers = writers || new RoundRobinArray(); + this._expires = expires || -1; + + } + + needsUpdate() { + return this._expires < Date.now() || + this.routers.empty() || + this.readers.empty() || + this.writers.empty(); + } + + all() { + let seen = new Set(this.routers.toArray()); + let writers = this.writers.toArray(); + let readers = this.readers.toArray(); + for (let i = 0; i < writers.length; i++) { + seen.add(writers[i]); + } + for (let i = 0; i < readers.length; i++) { + seen.add(readers[i]); + } + return seen; + } + + remove(item) { + this.routers.remove(item); + this.readers.remove(item); + this.writers.remove(item); + } +} + +let GET_SERVERS = "CALL dbms.cluster.routing.getServers"; + +/** + * Calls `getServers` and retrieves a new promise of a ClusterView. + * @param session + * @returns {Promise.} + */ +function newClusterView(session) { + return session.run(GET_SERVERS) + .then((res) => { + session.close(); + if (res.records.length != 1) { + return Promise.reject(newError("Invalid routing response from server", SERVICE_UNAVAILABLE)); + } + let record = res.records[0]; + //Note we are loosing precision here but let's hope that in + //the 140000 years to come before this precision loss + //hits us, that we get native 64 bit integers in javascript + let expires = record.get('ttl').toNumber(); + let servers = record.get('servers'); + let routers = new RoundRobinArray(); + let readers = new RoundRobinArray(); + let writers = new RoundRobinArray(); + for (let i = 0; i < servers.length; i++) { + let server = servers[i]; + + let role = server['role']; + let addresses = server['addresses']; + if (role === 'ROUTE') { + routers.pushAll(addresses); + } else if (role === 'WRITE') { + writers.pushAll(addresses); + } else if (role === 'READ') { + readers.pushAll(addresses); + } + } + return new ClusterView(routers, readers, writers, expires); + }); +} + +export default RoutingDriver diff --git a/src/v1/session.js b/src/v1/session.js index 845eced2c..e2eb72995 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -33,7 +33,7 @@ import {newError} from "./error"; class Session { /** * @constructor - * @param {Promise} connectionPromise - A connection to use + * @param {Promise.} connectionPromise - Promise of a connection to use * @param {function()} onClose - Function to be called on connection close */ constructor( connectionPromise, onClose ) { diff --git a/test/internal/round-robin-array.test.js b/test/internal/round-robin-array.test.js new file mode 100644 index 000000000..364f33ee7 --- /dev/null +++ b/test/internal/round-robin-array.test.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2002-2016 "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. + */ + +var RoundRobinArray = require('../../lib/v1/internal/round-robin-array').default; + +describe('round-robin-array', function() { + it('should step through array', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(3); + expect(array.hop()).toEqual(4); + expect(array.hop()).toEqual(5); + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + //.... + }); + + it('should step through single element array', function () { + var array = new RoundRobinArray([5]); + + expect(array.hop()).toEqual(5); + expect(array.hop()).toEqual(5); + expect(array.hop()).toEqual(5); + //.... + }); + + it('should handle deleting item before current ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + array.remove(2); + expect(array.hop()).toEqual(3); + expect(array.hop()).toEqual(4); + expect(array.hop()).toEqual(5); + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(3); + //.... + }); + + it('should handle deleting item on current ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + array.remove(3); + expect(array.hop()).toEqual(4); + expect(array.hop()).toEqual(5); + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(4); + //.... + }); + + it('should handle deleting item after current ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + array.remove(4); + expect(array.hop()).toEqual(3); + expect(array.hop()).toEqual(5); + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(3); + //.... + }); + + it('should handle deleting last item ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(3); + expect(array.hop()).toEqual(4); + array.remove(5); + expect(array.hop()).toEqual(1); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(3); + expect(array.hop()).toEqual(4); + expect(array.hop()).toEqual(1); + //.... + }); + + it('should handle deleting first item ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + array.remove(1); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(3); + expect(array.hop()).toEqual(4); + expect(array.hop()).toEqual(5); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(3); + expect(array.hop()).toEqual(4); + expect(array.hop()).toEqual(5); + //.... + }); + + it('should handle deleting multiple items ', function () { + var array = new RoundRobinArray([1,2,3,1,1]); + array.remove(1); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(3); + expect(array.hop()).toEqual(2); + expect(array.hop()).toEqual(3); + //.... + }); + +}); From d29d74eaf71cadfc07a1b397cc898a85f67fe570 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Thu, 13 Oct 2016 16:01:54 +0200 Subject: [PATCH 08/18] Routing support for running on session. --- src/v1/driver.js | 24 ++- src/v1/internal/ch-node.js | 5 +- src/v1/internal/pool.js | 18 +- src/v1/internal/stream-observer.js | 15 +- src/v1/routing-driver.js | 67 +++++++- src/v1/session.js | 12 +- test/resources/boltkit/non_discovery.script | 8 + .../boltkit/not_able_to_write.script | 11 ++ test/resources/boltkit/rediscover.script | 14 ++ test/v1/routing.driver.test.js | 157 +++++++++++++++++- 10 files changed, 302 insertions(+), 29 deletions(-) create mode 100644 test/resources/boltkit/non_discovery.script create mode 100644 test/resources/boltkit/not_able_to_write.script create mode 100644 test/resources/boltkit/rediscover.script diff --git a/src/v1/driver.js b/src/v1/driver.js index 3e6e2a62b..e0649519d 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -108,15 +108,12 @@ class Driver { * it is returned to the pool, the session will be reset to a clean state and * made available for others to use. * + * @param {String} mode of session - optional * @return {Session} new session. */ - session() { - let conn = this._pool.acquire(this._url); - return this._createSession(Promise.resolve(conn)); - } - - _createSession(connectionPromise) { - return new Session(connectionPromise, (cb) => { + session(mode) { + let connectionPromise = this._acquireConnection(mode); + return this._createSession(connectionPromise, (cb) => { // This gets called on Session#close(), and is where we return // the pooled 'connection' instance. @@ -134,7 +131,6 @@ class Driver { conn._release(); }); - // Call user callback if (cb) { cb(); @@ -142,6 +138,16 @@ class Driver { }); } + //Extension point + _acquireConnection(mode) { + return Promise.resolve(this._pool.acquire(this._url)); + } + + //Extension point + _createSession(connectionPromise, cb) { + return new Session(connectionPromise, cb); + } + /** * Close all open sessions and other associated resources. You should * make sure to use this when you are done with this driver instance. @@ -152,6 +158,8 @@ class Driver { if (this._openSessions.hasOwnProperty(sessionId)) { this._openSessions[sessionId].close(); } + + this._pool.purgeAll(); } } } diff --git a/src/v1/internal/ch-node.js b/src/v1/internal/ch-node.js index 766ab29d7..69d0f5c99 100644 --- a/src/v1/internal/ch-node.js +++ b/src/v1/internal/ch-node.js @@ -293,9 +293,10 @@ class NodeChannel { } _handleConnectionError( err ) { - this._error = err; + let msg = err.message || 'Failed to connect to server'; + this._error = newError(msg, SESSION_EXPIRED); if( this.onerror ) { - this.onerror(err); + this.onerror(this._error); } } diff --git a/src/v1/internal/pool.js b/src/v1/internal/pool.js index 4bc1c89e3..116eeb39b 100644 --- a/src/v1/internal/pool.js +++ b/src/v1/internal/pool.js @@ -41,7 +41,11 @@ class Pool { acquire(key) { let resource; - let pool = this._pools[key] || []; + let pool = this._pools[key]; + if (!pool) { + pool = []; + this._pools[key] = pool; + } while (pool.length) { resource = pool.pop(); @@ -65,6 +69,14 @@ class Pool { delete this._pools[key] } + purgeAll() { + for (let key in this._pools.keys) { + if (this._pools.hasOwnPropertykey) { + this.purge(key); + } + } + } + has(key) { return (key in this._pools); } @@ -72,8 +84,8 @@ class Pool { _release(key, resource) { let pool = this._pools[key]; if (!pool) { - pool = []; - this._pools[key] = pool; + //key has been purged, don't put it back + return; } if( pool.length >= this._maxIdle || !this._validate(resource) ) { this._destroy(resource); diff --git a/src/v1/internal/stream-observer.js b/src/v1/internal/stream-observer.js index f28af78e5..d576f53c7 100644 --- a/src/v1/internal/stream-observer.js +++ b/src/v1/internal/stream-observer.js @@ -32,14 +32,16 @@ import Record from "../record"; class StreamObserver { /** * @constructor + * @param errorCallback optional callback to be used for adding additional logic on error */ - constructor() { + constructor(errorCallback = () => {}) { this._fieldKeys = null; this._fieldLookup = null; this._queuedRecords = []; this._tail = null; this._error = null; this._hasFailed = false; + this._errorCallback = errorCallback; } /** @@ -81,6 +83,10 @@ class StreamObserver { } } + resolveConnection(conn) { + this._conn = conn; + } + /** * Will be called on errors. * If user-provided observer is present, pass the error @@ -88,18 +94,19 @@ class StreamObserver { * @param {Object} error - An error object */ onError(error) { + let transformedError = this._errorCallback(error, this._conn); if(this._hasFailed) { return; } this._hasFailed = true; if( this._observer ) { if( this._observer.onError ) { - this._observer.onError( error ); + this._observer.onError( transformedError ); } else { - console.log( error ); + console.log( transformedError ); } } else { - this._error = error; + this._error = transformedError; } } diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js index 53ad4ab56..9ccb9fd4d 100644 --- a/src/v1/routing-driver.js +++ b/src/v1/routing-driver.js @@ -23,8 +23,6 @@ import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from "./error"; import RoundRobinArray from './internal/round-robin-array'; import "babel-polyfill"; - - /** * A driver that supports routing in a core-edge cluster. */ @@ -35,9 +33,40 @@ class RoutingDriver extends Driver { this._clusterView = new ClusterView(new RoundRobinArray([url])); } - session(mode) { - let conn = this._acquireConnection(mode); - return this._createSession(conn); + _createSession(connectionPromise, cb) { + return new RoutingSession(connectionPromise, cb, (err, conn) => { + let code = err.code; + if (!code) { + try { + code = err.fields[0].code; + } catch (e) { + code = 'UNKNOWN'; + } + } + + if (code === SERVICE_UNAVAILABLE || code === SESSION_EXPIRED) { + if (conn) { + this._forget(conn.url) + } else { + connectionPromise.then((conn) => { + this._forget(conn.url); + }); + } + return err; + } else if (code === 'Neo.ClientError.Cluster.NotALeader') { + let url = 'UNKNOWN'; + if (conn) { + url = conn.url; + this._clusterView.writers.remove(conn.url); + } else { + connectionPromise.then((conn) => { + this._clusterView.writers.remove(conn.url); + }); + } + + return newError("No longer possible to write to server at " + url, SESSION_EXPIRED); + } + }); } _updatedClusterView() { @@ -60,6 +89,7 @@ class RoutingDriver extends Driver { return acc; } } + _diff(oldView, updatedView) { let oldSet = oldView.all(); let newSet = updatedView.all(); @@ -81,9 +111,17 @@ class RoutingDriver extends Driver { //update our cached view this._clusterView = view; if (m === READ) { - return this._pool.acquire(view.readers.hop()); + let key = view.readers.hop(); + if (!key) { + return Promise.reject(newError('No read servers available', SESSION_EXPIRED)); + } + return this._pool.acquire(key); } else if (m === WRITE) { - return this._pool.acquire(view.writers.hop()); + let key = view.writers.hop(); + if (!key) { + return Promise.reject(newError('No write servers available', SESSION_EXPIRED)); + } + return this._pool.acquire(key); } else { return Promise.reject(m + " is not a valid option"); } @@ -96,7 +134,6 @@ class RoutingDriver extends Driver { } } - class ClusterView { constructor(routers, readers, writers, expires) { this.routers = routers || new RoundRobinArray(); @@ -133,6 +170,17 @@ class ClusterView { } } +class RoutingSession extends Session { + constructor(connectionPromise, onClose, onFailedConnection) { + super(connectionPromise, onClose); + this._onFailedConnection = onFailedConnection; + } + + _onRunFailure() { + return this._onFailedConnection; + } +} + let GET_SERVERS = "CALL dbms.cluster.routing.getServers"; /** @@ -170,6 +218,9 @@ function newClusterView(session) { } } return new ClusterView(routers, readers, writers, expires); + }) + .catch(() => { + return Promise.reject(newError("No servers could be found at this instant.", SERVICE_UNAVAILABLE)); }); } diff --git a/src/v1/session.js b/src/v1/session.js index e2eb72995..2f2646b40 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -55,9 +55,10 @@ class Session { parameters = statement.parameters || {}; statement = statement.text; } - let streamObserver = new _RunObserver(); + let streamObserver = new _RunObserver(this._onRunFailure()); if (!this._hasTx) { this._connectionPromise.then((conn) => { + streamObserver.resolveConnection(conn); conn.run(statement, parameters, streamObserver); conn.pullAll(streamObserver); conn.sync(); @@ -105,12 +106,17 @@ class Session { cb(); } } + + //Can be overridden to add error callback on RUN + _onRunFailure() { + return (err) => {return err}; + } } /** Internal stream observer used for transactional results*/ class _RunObserver extends StreamObserver { - constructor() { - super(); + constructor(onError) { + super(onError); this._meta = {}; } diff --git a/test/resources/boltkit/non_discovery.script b/test/resources/boltkit/non_discovery.script new file mode 100644 index 000000000..b9c99f9dc --- /dev/null +++ b/test/resources/boltkit/non_discovery.script @@ -0,0 +1,8 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} +C: PULL_ALL +S: FAILURE {"code": "Neo.ClientError.Procedure.ProcedureNotFound", "message": "blabla"} +S: IGNORED diff --git a/test/resources/boltkit/not_able_to_write.script b/test/resources/boltkit/not_able_to_write.script new file mode 100644 index 000000000..141583196 --- /dev/null +++ b/test/resources/boltkit/not_able_to_write.script @@ -0,0 +1,11 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL +!: AUTO RUN "ROLLBACK" {} +!: AUTO RUN "BEGIN" {} +!: AUTO PULL_ALL + +C: RUN "CREATE ()" {} +C: PULL_ALL +S: FAILURE {"code": "Neo.ClientError.Cluster.NotALeader", "message": "blabla"} +S: IGNORED \ No newline at end of file diff --git a/test/resources/boltkit/rediscover.script b/test/resources/boltkit/rediscover.script new file mode 100644 index 000000000..ea5c4ed59 --- /dev/null +++ b/test/resources/boltkit/rediscover.script @@ -0,0 +1,14 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"], "role": "ROUTE"}]] + SUCCESS {} +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9004"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005"], "role": "READ"},{"addresses": ["127.0.0.1:9002","127.0.0.1:9003","127.0.0.1:9004"], "role": "ROUTE"}]] + SUCCESS {} diff --git a/test/v1/routing.driver.test.js b/test/v1/routing.driver.test.js index dbc424514..910680d97 100644 --- a/test/v1/routing.driver.test.js +++ b/test/v1/routing.driver.test.js @@ -36,7 +36,9 @@ describe('routing driver ', function() { var session = driver.session(); session.run("MATCH (n) RETURN n.name").then(function() { + session.close(); // Then + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]); expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9002","127.0.0.1:9003"]); expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); @@ -119,6 +121,10 @@ describe('routing driver ', function() { var session = driver.session(neo4j.READ); session.run("MATCH (n) RETURN n.name").then(function(res) { + session.close(); + + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); + expect(driver._pool.has('127.0.0.1:9005')).toBeTruthy(); // Then expect(res.records[0].get('n.name')).toEqual('Bob'); expect(res.records[1].get('n.name')).toEqual('Alice'); @@ -195,7 +201,6 @@ describe('routing driver ', function() { // When var session = driver.session(neo4j.READ); session.run("MATCH (n) RETURN n.name").catch(function (err) { - console.log(err.sessionExpired); expect(err.code).toEqual(neo4j.SESSION_EXPIRED); driver.close(); seedServer.exit(function (code1) { @@ -332,5 +337,155 @@ describe('routing driver ', function() { }); }); }); + + it('should forget endpoints on failure', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/dead_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").catch(function() { + session.close(); + // Then + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); + expect(driver._pool.has('127.0.0.1:9005')).toBeFalsy(); + expect(driver._clusterView.routers.toArray()).toEqual(['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003']); + expect(driver._clusterView.readers.toArray()).toEqual(['127.0.0.1:9006']); + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9007', '127.0.0.1:9008']); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should forget endpoints on session acquisition failure', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").catch(function(err) { + session.close(); + // Then + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); + expect(driver._pool.has('127.0.0.1:9005')).toBeFalsy(); + expect(driver._clusterView.routers.toArray()).toEqual(['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003']); + expect(driver._clusterView.readers.toArray()).toEqual(['127.0.0.1:9006']); + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9007', '127.0.0.1:9008']); + driver.close(); + seedServer.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + + it('should rediscover if necessary', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/rediscover.script', 9001); + var readServer = kit.start('./test/resources/boltkit/read_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + + it('should handle server not able to do routing', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(true); + var server = kit.start('./test/resources/boltkit/non_discovery.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.SERVICE_UNAVAILABLE); + session.close(); + driver.close(); + server.exit(function(code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + + it('should handle leader switch while writing', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(true); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/not_able_to_write.script', 9007); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("CREATE ()").catch(function (err) { + //the server at 9007 should have been removed + expect(driver._clusterView.writers.toArray()).toEqual([ '127.0.0.1:9008']); + expect(err.code).toEqual(neo4j.SESSION_EXPIRED); + session.close(); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); }); From 2277afbfb34993b6644a5b3089320866c03e4920 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Fri, 14 Oct 2016 08:49:45 +0200 Subject: [PATCH 09/18] Support for routing when running on transactions --- src/v1/internal/stream-observer.js | 2 +- src/v1/session.js | 2 +- src/v1/transaction.js | 8 +++-- .../boltkit/not_able_to_write.script | 1 + .../not_able_to_write_in_transaction.script | 14 ++++++++ test/v1/routing.driver.test.js | 34 +++++++++++++++++++ 6 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 test/resources/boltkit/not_able_to_write_in_transaction.script diff --git a/src/v1/internal/stream-observer.js b/src/v1/internal/stream-observer.js index d576f53c7..ab8ffb1cf 100644 --- a/src/v1/internal/stream-observer.js +++ b/src/v1/internal/stream-observer.js @@ -34,7 +34,7 @@ class StreamObserver { * @constructor * @param errorCallback optional callback to be used for adding additional logic on error */ - constructor(errorCallback = () => {}) { + constructor(errorCallback = (err) => {return err}) { this._fieldKeys = null; this._fieldLookup = null; this._queuedRecords = []; diff --git a/src/v1/session.js b/src/v1/session.js index 2f2646b40..13ef83aef 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -87,7 +87,7 @@ class Session { } this._hasTx = true; - return new Transaction(this._connectionPromise, () => {this._hasTx = false}); + return new Transaction(this._connectionPromise, () => {this._hasTx = false}, this._onRunFailure()); } /** diff --git a/src/v1/transaction.js b/src/v1/transaction.js index 1302ec60e..d69327127 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -30,16 +30,18 @@ class Transaction { * @param {Promise} connectionPromise - A connection to use * @param {function()} onClose - Function to be called when transaction is committed or rolled back. */ - constructor(connectionPromise, onClose) { + constructor(connectionPromise, onClose, errorTransformer) { this._connectionPromise = connectionPromise; let streamObserver = new _TransactionStreamObserver(this); this._connectionPromise.then((conn) => { + streamObserver.resolveConnection(conn); conn.run("BEGIN", {}, streamObserver); conn.discardAll(streamObserver); }).catch(streamObserver.onError); this._state = _states.ACTIVE; this._onClose = onClose; + this._errorTransformer = errorTransformer; } /** @@ -98,7 +100,7 @@ class Transaction { /** Internal stream observer used for transactional results*/ class _TransactionStreamObserver extends StreamObserver { constructor(tx) { - super(); + super(tx._errorTransformer || ((err) => {return err})); this._tx = tx; //this is to to avoid multiple calls to onError caused by IGNORED this._hasFailed = false; @@ -126,6 +128,7 @@ let _states = { }, run: (connectionPromise, observer, statement, parameters) => { connectionPromise.then((conn) => { + observer.resolveConnection(conn); conn.run( statement, parameters || {}, observer ); conn.pullAll( observer ); conn.sync(); @@ -204,6 +207,7 @@ let _states = { function _runDiscardAll(msg, connectionPromise, observer) { connectionPromise.then((conn) => { + observer.resolveConnection(conn); conn.run(msg, {}, observer); conn.discardAll(observer); conn.sync(); diff --git a/test/resources/boltkit/not_able_to_write.script b/test/resources/boltkit/not_able_to_write.script index 141583196..6f14544b5 100644 --- a/test/resources/boltkit/not_able_to_write.script +++ b/test/resources/boltkit/not_able_to_write.script @@ -1,6 +1,7 @@ !: AUTO INIT !: AUTO RESET !: AUTO PULL_ALL +!: AUTO DISCARD_ALL !: AUTO RUN "ROLLBACK" {} !: AUTO RUN "BEGIN" {} !: AUTO PULL_ALL diff --git a/test/resources/boltkit/not_able_to_write_in_transaction.script b/test/resources/boltkit/not_able_to_write_in_transaction.script new file mode 100644 index 000000000..a09382494 --- /dev/null +++ b/test/resources/boltkit/not_able_to_write_in_transaction.script @@ -0,0 +1,14 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL +!: AUTO DISCARD_ALL +!: AUTO RUN "ROLLBACK" {} +!: AUTO RUN "BEGIN" {} +!: AUTO PULL_ALL + +C: RUN "CREATE ()" {} +C: PULL_ALL +S: FAILURE {"code": "Neo.ClientError.Cluster.NotALeader", "message": "blabla"} +S: IGNORED +C: RUN "COMMIT" {} +S: IGNORED \ No newline at end of file diff --git a/test/v1/routing.driver.test.js b/test/v1/routing.driver.test.js index 910680d97..3d28e4732 100644 --- a/test/v1/routing.driver.test.js +++ b/test/v1/routing.driver.test.js @@ -487,5 +487,39 @@ describe('routing driver ', function() { }); }); }); + + it('should handle leader switch while writing on transaction', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(true); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/not_able_to_write_in_transaction.script', 9007); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + var tx = session.beginTransaction(); + tx.run("CREATE ()"); + + tx.commit().catch(function (err) { + //the server at 9007 should have been removed + expect(driver._clusterView.writers.toArray()).toEqual([ '127.0.0.1:9008']); + expect(err.code).toEqual(neo4j.SESSION_EXPIRED); + session.close(); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); }); From dc6ed54706452eb3d2132a46d2b2648209616996 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Fri, 14 Oct 2016 09:23:21 +0200 Subject: [PATCH 10/18] Refactor: moved constants and codes to namespaces --- src/v1/index.js | 21 +++++---- src/v1/internal/stream-observer.js | 8 ++-- src/v1/transaction.js | 1 + test/v1/routing.driver.test.js | 75 +++++++++++++++++++++--------- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/src/v1/index.js b/src/v1/index.js index a2cb00958..5099263c9 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -130,6 +130,15 @@ const types ={ Record }; +const session = { + READ, + WRITE +}; +const error = { + SERVICE_UNAVAILABLE, + SESSION_EXPIRED +}; + const forExport = { driver, int, @@ -137,10 +146,8 @@ const forExport = { Neo4jError, auth, types, - READ, - WRITE, - SERVICE_UNAVAILABLE, - SESSION_EXPIRED + session, + error }; export { @@ -150,9 +157,7 @@ export { Neo4jError, auth, types, - READ, - WRITE, - SERVICE_UNAVAILABLE, - SESSION_EXPIRED + session, + error } export default forExport diff --git a/src/v1/internal/stream-observer.js b/src/v1/internal/stream-observer.js index ab8ffb1cf..c98bbd61c 100644 --- a/src/v1/internal/stream-observer.js +++ b/src/v1/internal/stream-observer.js @@ -32,16 +32,16 @@ import Record from "../record"; class StreamObserver { /** * @constructor - * @param errorCallback optional callback to be used for adding additional logic on error + * @param errorTransformer optional callback to be used for adding additional logic on error */ - constructor(errorCallback = (err) => {return err}) { + constructor(errorTransformer = (err) => {return err}) { this._fieldKeys = null; this._fieldLookup = null; this._queuedRecords = []; this._tail = null; this._error = null; this._hasFailed = false; - this._errorCallback = errorCallback; + this._errorTransformer = errorTransformer; } /** @@ -94,7 +94,7 @@ class StreamObserver { * @param {Object} error - An error object */ onError(error) { - let transformedError = this._errorCallback(error, this._conn); + let transformedError = this._errorTransformer(error, this._conn); if(this._hasFailed) { return; } diff --git a/src/v1/transaction.js b/src/v1/transaction.js index d69327127..1cfb73028 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -29,6 +29,7 @@ class Transaction { * @constructor * @param {Promise} connectionPromise - A connection to use * @param {function()} onClose - Function to be called when transaction is committed or rolled back. + * @param errorTransformer callback use to transform error */ constructor(connectionPromise, onClose, errorTransformer) { this._connectionPromise = connectionPromise; diff --git a/test/v1/routing.driver.test.js b/test/v1/routing.driver.test.js index 3d28e4732..d0b55f2f4 100644 --- a/test/v1/routing.driver.test.js +++ b/test/v1/routing.driver.test.js @@ -81,6 +81,37 @@ describe('routing driver ', function() { }); }); + it('should discover new servers using subscribe', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/discover_new_servers.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("MATCH (n) RETURN n.name").subscribe({ + onCompleted: function () { + + // Then + expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9004", "127.0.0.1:9002", "127.0.0.1:9003"]); + expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9005", "127.0.0.1:9003"]); + expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); + + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + } + }); + }); + }); + it('should handle empty response from server', function (done) { if (!boltkit.BoltKitSupport) { done(); @@ -95,7 +126,7 @@ describe('routing driver ', function() { // When var session = driver.session(neo4j.READ); session.run("MATCH (n) RETURN n.name").catch(function (err) { - expect(err.code).toEqual(neo4j.SERVICE_UNAVAILABLE); + expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); driver.close(); server.exit(function (code) { expect(code).toEqual(0); @@ -118,7 +149,7 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.READ); + var session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").then(function(res) { session.close(); @@ -155,14 +186,14 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.READ); + var session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").then(function (res) { // Then expect(res.records[0].get('n.name')).toEqual('Bob'); expect(res.records[1].get('n.name')).toEqual('Alice'); expect(res.records[2].get('n.name')).toEqual('Tina'); session.close(); - session = driver.session(neo4j.READ); + session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").then(function (res) { // Then expect(res.records[0].get('n.name')).toEqual('Bob'); @@ -199,9 +230,9 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.READ); + var session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").catch(function (err) { - expect(err.code).toEqual(neo4j.SESSION_EXPIRED); + expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); driver.close(); seedServer.exit(function (code1) { readServer.exit(function (code2) { @@ -227,7 +258,7 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.WRITE); + var session = driver.session(neo4j.session.WRITE); session.run("CREATE (n {name:'Bob'})").then(function() { // Then @@ -257,9 +288,9 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.WRITE); + var session = driver.session(neo4j.session.WRITE); session.run("CREATE (n {name:'Bob'})").then(function () { - session = driver.session(neo4j.WRITE); + session = driver.session(neo4j.session.WRITE); session.run("CREATE (n {name:'Bob'})").then(function () { // Then driver.close(); @@ -291,9 +322,9 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.WRITE); + var session = driver.session(neo4j.session.WRITE); session.run("MATCH (n) RETURN n.name").catch(function (err) { - expect(err.code).toEqual(neo4j.SESSION_EXPIRED); + expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); driver.close(); seedServer.exit(function (code1) { readServer.exit(function (code2) { @@ -319,7 +350,7 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.READ); + var session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").then(function() { // Then @@ -351,7 +382,7 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.READ); + var session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").catch(function() { session.close(); // Then @@ -384,7 +415,7 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.READ); + var session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").catch(function(err) { session.close(); // Then @@ -415,9 +446,9 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When - var session = driver.session(neo4j.READ); + var session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").catch(function (err) { - session = driver.session(neo4j.READ); + session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").then(function (res) { driver.close(); seedServer.exit(function (code1) { @@ -438,7 +469,7 @@ describe('routing driver ', function() { return; } // Given - var kit = new boltkit.BoltKit(true); + var kit = new boltkit.BoltKit(); var server = kit.start('./test/resources/boltkit/non_discovery.script', 9001); kit.run(function () { @@ -446,7 +477,7 @@ describe('routing driver ', function() { // When var session = driver.session(); session.run("MATCH (n) RETURN n.name").catch(function (err) { - expect(err.code).toEqual(neo4j.SERVICE_UNAVAILABLE); + expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); session.close(); driver.close(); server.exit(function(code) { @@ -463,7 +494,7 @@ describe('routing driver ', function() { return; } // Given - var kit = new boltkit.BoltKit(true); + var kit = new boltkit.BoltKit(); var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); var readServer = kit.start('./test/resources/boltkit/not_able_to_write.script', 9007); @@ -474,7 +505,7 @@ describe('routing driver ', function() { session.run("CREATE ()").catch(function (err) { //the server at 9007 should have been removed expect(driver._clusterView.writers.toArray()).toEqual([ '127.0.0.1:9008']); - expect(err.code).toEqual(neo4j.SESSION_EXPIRED); + expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); session.close(); driver.close(); seedServer.exit(function (code1) { @@ -494,7 +525,7 @@ describe('routing driver ', function() { return; } // Given - var kit = new boltkit.BoltKit(true); + var kit = new boltkit.BoltKit(); var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); var readServer = kit.start('./test/resources/boltkit/not_able_to_write_in_transaction.script', 9007); @@ -508,7 +539,7 @@ describe('routing driver ', function() { tx.commit().catch(function (err) { //the server at 9007 should have been removed expect(driver._clusterView.writers.toArray()).toEqual([ '127.0.0.1:9008']); - expect(err.code).toEqual(neo4j.SESSION_EXPIRED); + expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); session.close(); driver.close(); seedServer.exit(function (code1) { From 1178d1a34263341092562bfa1a74f1c980e5a160 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Mon, 17 Oct 2016 10:15:04 +0200 Subject: [PATCH 11/18] Support for bookmarking --- src/v1/session.js | 10 ++++++++-- src/v1/transaction.js | 18 ++++++++++++++++-- test/v1/transaction.test.js | 13 +++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/v1/session.js b/src/v1/session.js index 13ef83aef..85bda64b4 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -79,7 +79,7 @@ class Session { * * @returns {Transaction} - New Transaction */ - beginTransaction() { + beginTransaction(bookmark) { if (this._hasTx) { throw newError("You cannot begin a transaction on a session with an " + "open transaction; either run from within the transaction or use a " @@ -87,7 +87,13 @@ class Session { } this._hasTx = true; - return new Transaction(this._connectionPromise, () => {this._hasTx = false}, this._onRunFailure()); + return new Transaction(this._connectionPromise, () => { + this._hasTx = false}, + this._onRunFailure(), bookmark, (bookmark) => {this._lastBookmark = bookmark}); + } + + lastBookmark() { + return this._lastBookmark; } /** diff --git a/src/v1/transaction.js b/src/v1/transaction.js index 1cfb73028..cb92c0d6a 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -30,19 +30,25 @@ class Transaction { * @param {Promise} connectionPromise - A connection to use * @param {function()} onClose - Function to be called when transaction is committed or rolled back. * @param errorTransformer callback use to transform error + * @param bookmark optional bookmark */ - constructor(connectionPromise, onClose, errorTransformer) { + constructor(connectionPromise, onClose, errorTransformer, bookmark, onBookmark) { this._connectionPromise = connectionPromise; let streamObserver = new _TransactionStreamObserver(this); + let params = {}; + if (bookmark) { + params = {bookmark: bookmark}; + } this._connectionPromise.then((conn) => { streamObserver.resolveConnection(conn); - conn.run("BEGIN", {}, streamObserver); + conn.run("BEGIN", params, streamObserver); conn.discardAll(streamObserver); }).catch(streamObserver.onError); this._state = _states.ACTIVE; this._onClose = onClose; this._errorTransformer = errorTransformer; + this._onBookmark = onBookmark || (() => {}); } /** @@ -114,6 +120,14 @@ class _TransactionStreamObserver extends StreamObserver { this._hasFailed = true; } } + + onCompleted(meta) { + super.onCompleted(meta); + let bookmark = meta.bookmark; + if (bookmark) { + this._tx._onBookmark(bookmark); + } + } } /** internal state machine of the transaction*/ diff --git a/test/v1/transaction.test.js b/test/v1/transaction.test.js index cdef0e23f..70ca1e489 100644 --- a/test/v1/transaction.test.js +++ b/test/v1/transaction.test.js @@ -214,4 +214,17 @@ describe('transaction', function() { done(); }); }); + + it('should provide bookmark on commit', function (done) { + // When + var tx = session.beginTransaction(); + expect(session.lastBookmark()).not.toBeDefined(); + tx.run("CREATE (:TXNode1)"); + tx.run("CREATE (:TXNode2)"); + tx.commit() + .then(function () { + expect(session.lastBookmark()).toBeDefined(); + done(); + }); + }); }); From ddeef04a4ea35404af662e1aff0b232ad95c1e21 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Mon, 17 Oct 2016 10:50:42 +0200 Subject: [PATCH 12/18] Moved boltkit tests to not run by default --- gulpfile.babel.js | 11 +++++++++++ package.json | 1 + runTests.sh | 2 +- ...t.driver.test.js => direct.driver.boltkit.it.js} | 0 ....driver.test.js => routing.driver.boltkit.it.js} | 0 test/v1/transaction.test.js | 13 +++++++++++-- 6 files changed, 24 insertions(+), 3 deletions(-) rename test/v1/{direct.driver.test.js => direct.driver.boltkit.it.js} (100%) rename test/v1/{routing.driver.test.js => routing.driver.boltkit.it.js} (100%) diff --git a/gulpfile.babel.js b/gulpfile.babel.js index e48dcc07e..9f4c20b16 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -154,6 +154,17 @@ gulp.task('test-nodejs', ['nodejs'], function () { })); }); +gulp.task('test-boltkit', ['nodejs'], function () { + return gulp.src('test/**/*.boltkit.it.js') + .pipe(jasmine({ + // reporter: new reporters.JUnitXmlReporter({ + // savePath: "build/nodejs-test-reports", + // consolidateAll: false + // }), + includeStackTrace: true + })); +}); + gulp.task('test-browser', function (cb) { runSequence('all', 'run-browser-test', cb) }); diff --git a/package.json b/package.json index 8d288f98a..42046b4f2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "scripts": { "test": "gulp test", + "boltkit": "gulp test-boltkit", "build": "gulp all", "start-neo4j": "gulp start-neo4j", "stop-neo4j": "gulp stop-neo4j", diff --git a/runTests.sh b/runTests.sh index 6ec1f977b..41879af35 100755 --- a/runTests.sh +++ b/runTests.sh @@ -16,4 +16,4 @@ else fi sleep 2 -npm test +npm test \ No newline at end of file diff --git a/test/v1/direct.driver.test.js b/test/v1/direct.driver.boltkit.it.js similarity index 100% rename from test/v1/direct.driver.test.js rename to test/v1/direct.driver.boltkit.it.js diff --git a/test/v1/routing.driver.test.js b/test/v1/routing.driver.boltkit.it.js similarity index 100% rename from test/v1/routing.driver.test.js rename to test/v1/routing.driver.boltkit.it.js diff --git a/test/v1/transaction.test.js b/test/v1/transaction.test.js index 70ca1e489..b2aa334f3 100644 --- a/test/v1/transaction.test.js +++ b/test/v1/transaction.test.js @@ -21,10 +21,13 @@ var neo4j = require("../../lib/v1"); describe('transaction', function() { - var driver, session; + var driver, session, server; beforeEach(function(done) { driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j")); + driver.onCompleted = function (meta) { + server = meta['server']; + }; session = driver.session(); session.run("MATCH (n) DETACH DELETE n").then(done); @@ -162,7 +165,7 @@ describe('transaction', function() { // When var tx = session.beginTransaction(); tx.run("CREATE (:TXNode1)"); - tx.rollback() + tx.rollback(); tx.commit() .catch(function (error) { @@ -216,6 +219,12 @@ describe('transaction', function() { }); it('should provide bookmark on commit', function (done) { + //bookmarking is not in 3.0 + if (!server) { + done(); + return; + } + // When var tx = session.beginTransaction(); expect(session.lastBookmark()).not.toBeDefined(); From 5e1e8939371da69daff84be89cc3460319b0557f Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Fri, 21 Oct 2016 14:21:13 +0200 Subject: [PATCH 13/18] Fixed bug in expiry time handling --- src/v1/routing-driver.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js index 9ccb9fd4d..407fa84e0 100644 --- a/src/v1/routing-driver.js +++ b/src/v1/routing-driver.js @@ -21,6 +21,8 @@ import Session from './session'; import {Driver, READ, WRITE} from './driver'; import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from "./error"; import RoundRobinArray from './internal/round-robin-array'; +import {int} from './integer' +import Integer from './integer' import "babel-polyfill"; /** @@ -139,13 +141,13 @@ class ClusterView { this.routers = routers || new RoundRobinArray(); this.readers = readers || new RoundRobinArray(); this.writers = writers || new RoundRobinArray(); - this._expires = expires || -1; + this._expires = expires || int(-1); } needsUpdate() { - return this._expires < Date.now() || - this.routers.empty() || + return this._expires.lessThan(Date.now()) || + this.routers.size() <= 1 || this.readers.empty() || this.writers.empty(); } @@ -196,10 +198,13 @@ function newClusterView(session) { return Promise.reject(newError("Invalid routing response from server", SERVICE_UNAVAILABLE)); } let record = res.records[0]; - //Note we are loosing precision here but let's hope that in - //the 140000 years to come before this precision loss - //hits us, that we get native 64 bit integers in javascript - let expires = record.get('ttl').toNumber(); + let now = int(Date.now()); + let expires = record.get('ttl').multiply(1000).add(now); + //if the server uses a really big expire time like Long.MAX_VALUE + //this may have overflowed + if (expires.lessThan(now)) { + expires = Integer.MAX_VALUE; + } let servers = record.get('servers'); let routers = new RoundRobinArray(); let readers = new RoundRobinArray(); From b359da5236da486976aa1c3b233710be56384d85 Mon Sep 17 00:00:00 2001 From: Oskar Hane Date: Fri, 21 Oct 2016 15:00:03 +0200 Subject: [PATCH 14/18] Expose driver by version as well as the latest one as deault export --- src/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index ed5f3c016..118d4c1e6 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,5 @@ import * as v1 from './v1/index'; -export default { - v1: v1 -} +export { v1 } +export default v1 From 0630b64872ee83efc50fe26fbce35eacc3e7fc5c Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Fri, 21 Oct 2016 15:57:50 +0200 Subject: [PATCH 15/18] Error handling for connecting to a non-routing server --- src/v1/driver.js | 9 ++++++++- src/v1/routing-driver.js | 20 ++++++++++++++++++-- test/v1/driver.test.js | 17 ++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/v1/driver.js b/src/v1/driver.js index e0649519d..086d8e910 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -22,7 +22,7 @@ import Pool from './internal/pool'; import Integer from './integer'; import {connect} from "./internal/connector"; import StreamObserver from './internal/stream-observer'; -import {newError} from "./error"; +import {newError, SERVICE_UNAVAILABLE} from "./error"; import "babel-polyfill"; let READ = 'READ', WRITE = 'WRITE'; @@ -113,6 +113,13 @@ class Driver { */ session(mode) { let connectionPromise = this._acquireConnection(mode); + connectionPromise.catch((err) => { + if (this.onError && err.code === SERVICE_UNAVAILABLE) { + this.onError(err); + } else { + return Promise.reject(err); + } + }); return this._createSession(connectionPromise, (cb) => { // This gets called on Session#close(), and is where we return // the pooled 'connection' instance. diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js index 407fa84e0..9145d261f 100644 --- a/src/v1/routing-driver.js +++ b/src/v1/routing-driver.js @@ -38,6 +38,7 @@ class RoutingDriver extends Driver { _createSession(connectionPromise, cb) { return new RoutingSession(connectionPromise, cb, (err, conn) => { let code = err.code; + let msg = err.message; if (!code) { try { code = err.fields[0].code; @@ -45,6 +46,16 @@ class RoutingDriver extends Driver { code = 'UNKNOWN'; } } + if (!msg) { + try { + msg = err.fields[0].message; + } catch (e) { + msg = 'Unknown failure occurred'; + } + } + //just to simplify later error handling + err.code = code; + err.message = msg; if (code === SERVICE_UNAVAILABLE || code === SESSION_EXPIRED) { if (conn) { @@ -65,8 +76,9 @@ class RoutingDriver extends Driver { this._clusterView.writers.remove(conn.url); }); } - return newError("No longer possible to write to server at " + url, SESSION_EXPIRED); + } else { + return err; } }); } @@ -224,8 +236,12 @@ function newClusterView(session) { } return new ClusterView(routers, readers, writers, expires); }) - .catch(() => { + .catch((e) => { + if (e.code === 'Neo.ClientError.Procedure.ProcedureNotFound') { + return Promise.reject(newError("Server could not perform routing, make sure you are connecting to a causal cluster", SERVICE_UNAVAILABLE)); + } else { return Promise.reject(newError("No servers could be found at this instant.", SERVICE_UNAVAILABLE)); + } }); } diff --git a/test/v1/driver.test.js b/test/v1/driver.test.js index 4b87e0445..dbb2cac72 100644 --- a/test/v1/driver.test.js +++ b/test/v1/driver.test.js @@ -111,7 +111,22 @@ describe('driver', function() { var driver = neo4j.driver("bolt://localhost", neo4j.auth.custom("neo4j", "neo4j", "native", "basic", {secret: 42})); // Expect - driver.onCompleted = function (meta) { + driver.onCompleted = function () { + done(); + }; + + // When + driver.session(); + }); + + it('should fail nicely when connecting with routing to standalone server', function(done) { + // Given + var driver = neo4j.driver("bolt+routing://localhost", neo4j.auth.basic("neo4j", "neo4j")); + + // Expect + driver.onError = function (err) { + expect(err.message).toEqual('Server could not perform routing, make sure you are connecting to a causal cluster'); + expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); done(); }; From 226de19b6fe63100a67097bebf575bb6c88fcb3b Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Mon, 24 Oct 2016 12:05:41 +0200 Subject: [PATCH 16/18] Removed `Unhandled rejection` warning --- src/v1/driver.js | 6 +++--- src/v1/error.js | 2 +- src/v1/routing-driver.js | 24 ++++++++++++------------ test/v1/routing.driver.boltkit.it.js | 5 ++++- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/v1/driver.js b/src/v1/driver.js index 086d8e910..bca4985ab 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -117,7 +117,7 @@ class Driver { if (this.onError && err.code === SERVICE_UNAVAILABLE) { this.onError(err); } else { - return Promise.reject(err); + //we don't need to tell the driver about this error } }); return this._createSession(connectionPromise, (cb) => { @@ -130,13 +130,14 @@ class Driver { // Queue up a 'reset', to ensure the next user gets a clean // session to work with. + connectionPromise.then( (conn) => { conn.reset(); conn.sync(); // Return connection to the pool conn._release(); - }); + }).catch( () => {/*ignore errors here*/}); // Call user callback if (cb) { @@ -165,7 +166,6 @@ class Driver { if (this._openSessions.hasOwnProperty(sessionId)) { this._openSessions[sessionId].close(); } - this._pool.purgeAll(); } } diff --git a/src/v1/error.js b/src/v1/error.js index cf998f80d..f2c1b17dc 100644 --- a/src/v1/error.js +++ b/src/v1/error.js @@ -23,7 +23,7 @@ let SERVICE_UNAVAILABLE = 'ServiceUnavailable'; let SESSION_EXPIRED = 'SessionExpired'; function newError(message, code="N/A") { - // TODO: Idea is that we can check the cod here and throw sub-classes + // TODO: Idea is that we can check the code here and throw sub-classes // of Neo4jError as appropriate return new Neo4jError(message, code); } diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js index 9145d261f..93c3112db 100644 --- a/src/v1/routing-driver.js +++ b/src/v1/routing-driver.js @@ -63,7 +63,7 @@ class RoutingDriver extends Driver { } else { connectionPromise.then((conn) => { this._forget(conn.url); - }); + }).catch(() => {/*ignore*/}); } return err; } else if (code === 'Neo.ClientError.Cluster.NotALeader') { @@ -74,7 +74,7 @@ class RoutingDriver extends Driver { } else { connectionPromise.then((conn) => { this._clusterView.writers.remove(conn.url); - }); + }).catch(() => {/*ignore*/}); } return newError("No longer possible to write to server at " + url, SESSION_EXPIRED); } else { @@ -87,19 +87,19 @@ class RoutingDriver extends Driver { if (!this._clusterView.needsUpdate()) { return Promise.resolve(this._clusterView); } else { + let p = () => { + let conn = this._pool.acquire(routers.hop()); + let session = this._createSession(Promise.resolve(conn)); + return newClusterView(session).catch((err) => { + this._forget(conn); + return Promise.reject(err); + }); + }; let routers = this._clusterView.routers; let acc = Promise.reject(); for (let i = 0; i < routers.size(); i++) { - acc = acc.catch(() => { - let conn = this._pool.acquire(routers.hop()); - let session = this._createSession(Promise.resolve(conn)); - return newClusterView(session).catch((err) => { - this._forget(conn); - return Promise.reject(err); - }); - }); + acc = acc.catch(p); } - return acc; } } @@ -139,7 +139,7 @@ class RoutingDriver extends Driver { } else { return Promise.reject(m + " is not a valid option"); } - }); + }).catch((err) => {return Promise.reject(err)}); } _forget(url) { diff --git a/test/v1/routing.driver.boltkit.it.js b/test/v1/routing.driver.boltkit.it.js index d0b55f2f4..8c8563432 100644 --- a/test/v1/routing.driver.boltkit.it.js +++ b/test/v1/routing.driver.boltkit.it.js @@ -123,16 +123,19 @@ describe('routing driver ', function() { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When var session = driver.session(neo4j.READ); session.run("MATCH (n) RETURN n.name").catch(function (err) { expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); + + session.close(); driver.close(); server.exit(function (code) { expect(code).toEqual(0); done(); }); - }); + }).catch(function (err) {console.log(err)}); }); }); From 0ce99828cfaaa81a048f01d2723c3bad3f4b153f Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Mon, 24 Oct 2016 14:57:25 +0200 Subject: [PATCH 17/18] Added test for routing among several servers --- src/v1/routing-driver.js | 9 +- test/resources/boltkit/short_ttl.script | 9 ++ test/v1/examples.test.js | 5 +- test/v1/routing.driver.boltkit.it.js | 118 ++++++++++++++++++------ 4 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 test/resources/boltkit/short_ttl.script diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js index 93c3112db..0ecb2f33f 100644 --- a/src/v1/routing-driver.js +++ b/src/v1/routing-driver.js @@ -87,7 +87,7 @@ class RoutingDriver extends Driver { if (!this._clusterView.needsUpdate()) { return Promise.resolve(this._clusterView); } else { - let p = () => { + let call = () => { let conn = this._pool.acquire(routers.hop()); let session = this._createSession(Promise.resolve(conn)); return newClusterView(session).catch((err) => { @@ -96,9 +96,12 @@ class RoutingDriver extends Driver { }); }; let routers = this._clusterView.routers; + //Build a promise chain that ends on the first successful call + //i.e. call().catch(call).catch(call).catch(call)... + //each call will try a different router let acc = Promise.reject(); for (let i = 0; i < routers.size(); i++) { - acc = acc.catch(p); + acc = acc.catch(call); } return acc; } @@ -240,7 +243,7 @@ function newClusterView(session) { if (e.code === 'Neo.ClientError.Procedure.ProcedureNotFound') { return Promise.reject(newError("Server could not perform routing, make sure you are connecting to a causal cluster", SERVICE_UNAVAILABLE)); } else { - return Promise.reject(newError("No servers could be found at this instant.", SERVICE_UNAVAILABLE)); + return Promise.reject(newError("No servers could be found at this instant.", SERVICE_UNAVAILABLE)); } }); } diff --git a/test/resources/boltkit/short_ttl.script b/test/resources/boltkit/short_ttl.script new file mode 100644 index 000000000..ceffcc0ef --- /dev/null +++ b/test/resources/boltkit/short_ttl.script @@ -0,0 +1,9 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [0, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9004"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} \ No newline at end of file diff --git a/test/v1/examples.test.js b/test/v1/examples.test.js index 323afc704..16a7f820e 100644 --- a/test/v1/examples.test.js +++ b/test/v1/examples.test.js @@ -33,6 +33,8 @@ describe('examples', function() { beforeAll(function () { var neo4j = neo4jv1; + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; //tag::construct-driver[] var driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j")); @@ -41,8 +43,7 @@ describe('examples', function() { }); beforeEach(function(done) { - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + // Override console.log, to assert on stdout output out = []; console = { log: function(msg) { out.push(msg); } }; diff --git a/test/v1/routing.driver.boltkit.it.js b/test/v1/routing.driver.boltkit.it.js index 8c8563432..c46781b9a 100644 --- a/test/v1/routing.driver.boltkit.it.js +++ b/test/v1/routing.driver.boltkit.it.js @@ -19,7 +19,17 @@ var neo4j = require("../../lib/v1"); var boltkit = require('./boltkit'); -describe('routing driver ', function() { +describe('routing driver ', function () { + var originalTimeout; + + beforeAll(function(){ + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + }); + + afterAll(function(){ + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); it('should discover server', function (done) { if (!boltkit.BoltKitSupport) { @@ -34,21 +44,21 @@ describe('routing driver ', function() { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When var session = driver.session(); - session.run("MATCH (n) RETURN n.name").then(function() { + session.run("MATCH (n) RETURN n.name").then(function () { - session.close(); - // Then - expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); - expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]); - expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9002","127.0.0.1:9003"]); - expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); + session.close(); + // Then + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); + expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]); + expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9002", "127.0.0.1:9003"]); + expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); - driver.close(); - server.exit(function (code) { - expect(code).toEqual(0); - done(); - }); + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); }); + }); }); }); @@ -65,11 +75,11 @@ describe('routing driver ', function() { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When var session = driver.session(); - session.run("MATCH (n) RETURN n.name").then(function() { + session.run("MATCH (n) RETURN n.name").then(function () { // Then - expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9004","127.0.0.1:9002","127.0.0.1:9003"]); - expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9005","127.0.0.1:9003"]); + expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9004", "127.0.0.1:9002", "127.0.0.1:9003"]); + expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9005", "127.0.0.1:9003"]); expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); driver.close(); @@ -135,7 +145,9 @@ describe('routing driver ', function() { expect(code).toEqual(0); done(); }); - }).catch(function (err) {console.log(err)}); + }).catch(function (err) { + console.log(err) + }); }); }); @@ -153,7 +165,7 @@ describe('routing driver ', function() { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When var session = driver.session(neo4j.session.READ); - session.run("MATCH (n) RETURN n.name").then(function(res) { + session.run("MATCH (n) RETURN n.name").then(function (res) { session.close(); @@ -175,6 +187,56 @@ describe('routing driver ', function() { }); }); + it('should pick first available route-server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/short_ttl.script', 9000); + var nextRouter = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9003); + var readServer1 = kit.start('./test/resources/boltkit/read_server.script', 9004); + var readServer2 = kit.start('./test/resources/boltkit/read_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9000", neo4j.auth.basic("neo4j", "neo4j")); + //driver.onError = console.log; + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + session.close(); + + session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + session.close(); + driver.close(); + seedServer.exit(function (code1) { + nextRouter.exit(function (code2) { + readServer1.exit(function (code3) { + readServer2.exit(function (code4) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + expect(code3).toEqual(0); + expect(code4).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + it('should round-robin among read servers', function (done) { if (!boltkit.BoltKitSupport) { done(); @@ -262,7 +324,7 @@ describe('routing driver ', function() { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When var session = driver.session(neo4j.session.WRITE); - session.run("CREATE (n {name:'Bob'})").then(function() { + session.run("CREATE (n {name:'Bob'})").then(function () { // Then driver.close(); @@ -354,7 +416,7 @@ describe('routing driver ', function() { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When var session = driver.session(neo4j.session.READ); - session.run("MATCH (n) RETURN n.name").then(function() { + session.run("MATCH (n) RETURN n.name").then(function () { // Then expect(driver._clusterView.routers.toArray()).toEqual(['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003']); @@ -386,7 +448,7 @@ describe('routing driver ', function() { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When var session = driver.session(neo4j.session.READ); - session.run("MATCH (n) RETURN n.name").catch(function() { + session.run("MATCH (n) RETURN n.name").catch(function () { session.close(); // Then expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); @@ -419,7 +481,7 @@ describe('routing driver ', function() { var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); // When var session = driver.session(neo4j.session.READ); - session.run("MATCH (n) RETURN n.name").catch(function(err) { + session.run("MATCH (n) RETURN n.name").catch(function (err) { session.close(); // Then expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); @@ -429,9 +491,9 @@ describe('routing driver ', function() { expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9007', '127.0.0.1:9008']); driver.close(); seedServer.exit(function (code) { - expect(code).toEqual(0); - done(); - }); + expect(code).toEqual(0); + done(); + }); }); }); }); @@ -483,7 +545,7 @@ describe('routing driver ', function() { expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); session.close(); driver.close(); - server.exit(function(code) { + server.exit(function (code) { expect(code).toEqual(0); done(); }); @@ -507,7 +569,7 @@ describe('routing driver ', function() { var session = driver.session(); session.run("CREATE ()").catch(function (err) { //the server at 9007 should have been removed - expect(driver._clusterView.writers.toArray()).toEqual([ '127.0.0.1:9008']); + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9008']); expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); session.close(); driver.close(); @@ -541,7 +603,7 @@ describe('routing driver ', function() { tx.commit().catch(function (err) { //the server at 9007 should have been removed - expect(driver._clusterView.writers.toArray()).toEqual([ '127.0.0.1:9008']); + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9008']); expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); session.close(); driver.close(); From f207842f2ea90607399b6f0d16fd5dfe0d1809f1 Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Tue, 25 Oct 2016 14:42:02 +0200 Subject: [PATCH 18/18] Retry on empty writer list If the response from `getServers` contains an empty `writers` list we should reject that answer and procede to the next available router. --- src/v1/internal/round-robin-array.js | 2 +- src/v1/routing-driver.js | 9 +- test/internal/round-robin-array.test.js | 104 +++++++++++------------ test/resources/boltkit/no_writers.script | 9 ++ test/v1/routing.driver.boltkit.it.js | 25 +++++- 5 files changed, 92 insertions(+), 57 deletions(-) create mode 100644 test/resources/boltkit/no_writers.script diff --git a/src/v1/internal/round-robin-array.js b/src/v1/internal/round-robin-array.js index 9392f0dc1..8ece12e9f 100644 --- a/src/v1/internal/round-robin-array.js +++ b/src/v1/internal/round-robin-array.js @@ -26,7 +26,7 @@ class RoundRobinArray { this._index = 0; } - hop() { + next() { let elem = this._items[this._index]; if (this._items.length === 0) { this._index = 0; diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js index 0ecb2f33f..0cad5411b 100644 --- a/src/v1/routing-driver.js +++ b/src/v1/routing-driver.js @@ -88,7 +88,7 @@ class RoutingDriver extends Driver { return Promise.resolve(this._clusterView); } else { let call = () => { - let conn = this._pool.acquire(routers.hop()); + let conn = this._pool.acquire(routers.next()); let session = this._createSession(Promise.resolve(conn)); return newClusterView(session).catch((err) => { this._forget(conn); @@ -128,13 +128,13 @@ class RoutingDriver extends Driver { //update our cached view this._clusterView = view; if (m === READ) { - let key = view.readers.hop(); + let key = view.readers.next(); if (!key) { return Promise.reject(newError('No read servers available', SESSION_EXPIRED)); } return this._pool.acquire(key); } else if (m === WRITE) { - let key = view.writers.hop(); + let key = view.writers.next(); if (!key) { return Promise.reject(newError('No write servers available', SESSION_EXPIRED)); } @@ -237,6 +237,9 @@ function newClusterView(session) { readers.pushAll(addresses); } } + if (routers.empty() || writers.empty()) { + return Promise.reject(newError("Invalid routing response from server", SERVICE_UNAVAILABLE)) + } return new ClusterView(routers, readers, writers, expires); }) .catch((e) => { diff --git a/test/internal/round-robin-array.test.js b/test/internal/round-robin-array.test.js index 364f33ee7..5d2ff8530 100644 --- a/test/internal/round-robin-array.test.js +++ b/test/internal/round-robin-array.test.js @@ -23,104 +23,104 @@ describe('round-robin-array', function() { it('should step through array', function () { var array = new RoundRobinArray([1,2,3,4,5]); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(3); - expect(array.hop()).toEqual(4); - expect(array.hop()).toEqual(5); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); //.... }); it('should step through single element array', function () { var array = new RoundRobinArray([5]); - expect(array.hop()).toEqual(5); - expect(array.hop()).toEqual(5); - expect(array.hop()).toEqual(5); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(5); //.... }); it('should handle deleting item before current ', function () { var array = new RoundRobinArray([1,2,3,4,5]); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); array.remove(2); - expect(array.hop()).toEqual(3); - expect(array.hop()).toEqual(4); - expect(array.hop()).toEqual(5); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(3); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(3); //.... }); it('should handle deleting item on current ', function () { var array = new RoundRobinArray([1,2,3,4,5]); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); array.remove(3); - expect(array.hop()).toEqual(4); - expect(array.hop()).toEqual(5); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(4); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(4); //.... }); it('should handle deleting item after current ', function () { var array = new RoundRobinArray([1,2,3,4,5]); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); array.remove(4); - expect(array.hop()).toEqual(3); - expect(array.hop()).toEqual(5); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(3); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); //.... }); it('should handle deleting last item ', function () { var array = new RoundRobinArray([1,2,3,4,5]); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(3); - expect(array.hop()).toEqual(4); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); array.remove(5); - expect(array.hop()).toEqual(1); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(3); - expect(array.hop()).toEqual(4); - expect(array.hop()).toEqual(1); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(1); //.... }); it('should handle deleting first item ', function () { var array = new RoundRobinArray([1,2,3,4,5]); array.remove(1); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(3); - expect(array.hop()).toEqual(4); - expect(array.hop()).toEqual(5); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(3); - expect(array.hop()).toEqual(4); - expect(array.hop()).toEqual(5); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); //.... }); it('should handle deleting multiple items ', function () { var array = new RoundRobinArray([1,2,3,1,1]); array.remove(1); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(3); - expect(array.hop()).toEqual(2); - expect(array.hop()).toEqual(3); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); //.... }); diff --git a/test/resources/boltkit/no_writers.script b/test/resources/boltkit/no_writers.script new file mode 100644 index 000000000..c7ae7b29a --- /dev/null +++ b/test/resources/boltkit/no_writers.script @@ -0,0 +1,9 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": [],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} \ No newline at end of file diff --git a/test/v1/routing.driver.boltkit.it.js b/test/v1/routing.driver.boltkit.it.js index c46781b9a..2cc813ba1 100644 --- a/test/v1/routing.driver.boltkit.it.js +++ b/test/v1/routing.driver.boltkit.it.js @@ -201,7 +201,6 @@ describe('routing driver ', function () { kit.run(function () { var driver = neo4j.driver("bolt+routing://127.0.0.1:9000", neo4j.auth.basic("neo4j", "neo4j")); - //driver.onError = console.log; // When var session = driver.session(neo4j.session.READ); session.run("MATCH (n) RETURN n.name").then(function (res) { @@ -617,5 +616,29 @@ describe('routing driver ', function () { }); }); }); + + it('should fail if missing write server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/no_writers.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.WRITE); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); + driver.close(); + seedServer.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); });