From 79cf7da8bdf0c459befe49c9da4d51fc00229f36 Mon Sep 17 00:00:00 2001 From: lutovich Date: Mon, 19 Jun 2017 17:32:46 +0200 Subject: [PATCH] Multiple bookmarks support Previously it was possible to only supply a single bookmark when creating a new session. However there is a use-case to be able to supply multiple bookmarks when multiple concurrent tasks execute write queries and then reader should be able to observe all those writes. To achieve this driver now allows passing an array of bookmarks to `Driver#session()` function. Driver will now send: ``` { bookmark: "max", bookmarks: ["one", "two", "max"] } ``` instead of simple: ``` { bookmark: "max" } ``` this is done to maintain backwards compatibility with databases that only support a single bookmark. It forces driver to parse and compare bookmarks which violates the fact that bookmarks are opaque. This is done only to maintain backwards compatibility and should not be copied. Code doing this will eventually be removed. Related Bolt server PR: https://github.com/neo4j/neo4j/pull/9404 --- src/v1/driver.js | 8 +- src/v1/internal/bookmark.js | 141 ++++++++++++++++++ src/v1/internal/util.js | 1 + src/v1/session.js | 34 +++-- src/v1/transaction.js | 19 +-- test/internal/bookmark.test.js | 121 +++++++++++++++ .../boltkit/multiple_bookmarks.script | 16 ++ .../boltkit/read_tx_with_bookmarks.script | 4 +- ...rite_read_tx_with_bookmark_override.script | 8 +- .../write_read_tx_with_bookmarks.script | 8 +- .../boltkit/write_tx_with_bookmarks.script | 4 +- test/v1/direct.driver.boltkit.it.js | 32 ++-- test/v1/routing.driver.boltkit.it.js | 52 +++++-- test/v1/session.test.js | 43 +++++- test/v1/transaction.test.js | 6 +- 15 files changed, 424 insertions(+), 73 deletions(-) create mode 100644 src/v1/internal/bookmark.js create mode 100644 test/internal/bookmark.test.js create mode 100644 test/resources/boltkit/multiple_bookmarks.script diff --git a/src/v1/driver.js b/src/v1/driver.js index 4dd03c593..771120761 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -23,6 +23,7 @@ import {connect} from './internal/connector'; import StreamObserver from './internal/stream-observer'; import {newError, SERVICE_UNAVAILABLE} from './error'; import {DirectConnectionProvider} from './internal/connection-providers'; +import Bookmark from './internal/bookmark'; const READ = 'READ', WRITE = 'WRITE'; /** @@ -115,13 +116,14 @@ class Driver { * made available for others to use. * * @param {string} [mode=WRITE] the access mode of this session, allowed values are {@link READ} and {@link WRITE}. - * @param {string} [bookmark=null] the initial reference to some previous transaction. Value is optional and - * absence indicates that that the bookmark does not exist or is unknown. + * @param {string|string[]} [bookmarkOrBookmarks=null] the initial reference or references to some previous + * transactions. Value is optional and absence indicates that that the bookmarks do not exist or are unknown. * @return {Session} new session. */ - session(mode, bookmark) { + session(mode, bookmarkOrBookmarks) { const sessionMode = Driver._validateSessionMode(mode); const connectionProvider = this._getOrCreateConnectionProvider(); + const bookmark = new Bookmark(bookmarkOrBookmarks); return this._createSession(sessionMode, connectionProvider, bookmark, this._config); } diff --git a/src/v1/internal/bookmark.js b/src/v1/internal/bookmark.js new file mode 100644 index 000000000..ccb350130 --- /dev/null +++ b/src/v1/internal/bookmark.js @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2002-2017 "Neo Technology,"," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as util from './util'; + +const BOOKMARK_KEY = 'bookmark'; +const BOOKMARKS_KEY = 'bookmarks'; +const BOOKMARK_PREFIX = 'neo4j:bookmark:v1:tx'; + +const UNKNOWN_BOOKMARK_VALUE = -1; + +export default class Bookmark { + + /** + * @constructor + * @param {string|string[]} values single bookmark as string or multiple bookmarks as a string array. + */ + constructor(values) { + this._values = asStringArray(values); + this._maxValue = maxBookmark(this._values); + } + + /** + * Check if the given bookmark is meaningful and can be send to the database. + * @return {boolean} returns true bookmark has a value, false otherwise. + */ + isEmpty() { + return this._maxValue === null; + } + + /** + * Get maximum value of this bookmark as string. + * @return {string|null} the maximum value or null if it is not defined. + */ + maxBookmarkAsString() { + return this._maxValue; + } + + /** + * Get this bookmark as an object for begin transaction call. + * @return {object} the value of this bookmark as object. + */ + asBeginTransactionParameters() { + if (this.isEmpty()) { + return {}; + } + + // Driver sends {bookmark: "max", bookmarks: ["one", "two", "max"]} instead of simple + // {bookmarks: ["one", "two", "max"]} for backwards compatibility reasons. Old servers can only accept single + // bookmark that is why driver has to parse and compare given list of bookmarks. This functionality will + // eventually be removed. + return { + [BOOKMARK_KEY]: this._maxValue, + [BOOKMARKS_KEY]: this._values + }; + } +} + +/** + * Converts given value to an array. + * @param {string|string[]} [value=undefined] argument to convert. + * @return {string[]} value converted to an array. + */ +function asStringArray(value) { + if (!value) { + return []; + } + + if (util.isString(value)) { + return [value]; + } + + if (Array.isArray(value)) { + const result = []; + for (let i = 0; i < value.length; i++) { + const element = value[i]; + if (!util.isString(element)) { + throw new TypeError(`Bookmark should be a string, given: '${element}'`); + } + result.push(element); + } + return result; + } + + throw new TypeError(`Bookmark should either be a string or a string array, given: '${value}'`); +} + +/** + * Find latest bookmark in the given array of bookmarks. + * @param {string[]} bookmarks array of bookmarks. + * @return {string|null} latest bookmark value. + */ +function maxBookmark(bookmarks) { + if (!bookmarks || bookmarks.length === 0) { + return null; + } + + let maxBookmark = bookmarks[0]; + let maxValue = bookmarkValue(maxBookmark); + + for (let i = 1; i < bookmarks.length; i++) { + const bookmark = bookmarks[i]; + const value = bookmarkValue(bookmark); + + if (value > maxValue) { + maxBookmark = bookmark; + maxValue = value; + } + } + + return maxBookmark; +} + +/** + * Calculate numeric value for the given bookmark. + * @param {string} bookmark argument to get numeric value for. + * @return {number} value of the bookmark. + */ +function bookmarkValue(bookmark) { + if (bookmark && bookmark.indexOf(BOOKMARK_PREFIX) === 0) { + const result = parseInt(bookmark.substring(BOOKMARK_PREFIX.length)); + return result ? result : UNKNOWN_BOOKMARK_VALUE; + } + return UNKNOWN_BOOKMARK_VALUE; +} diff --git a/src/v1/internal/util.js b/src/v1/internal/util.js index e7fd5c66e..50811e23a 100644 --- a/src/v1/internal/util.js +++ b/src/v1/internal/util.js @@ -116,6 +116,7 @@ function trimAndVerify(string, name, url) { export { isEmptyObjectOrNull, + isString, assertString, parseScheme, parseUrl, diff --git a/src/v1/session.js b/src/v1/session.js index 74770a936..5ff5f13e6 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -24,6 +24,7 @@ import {assertString} from './internal/util'; import ConnectionHolder from './internal/connection-holder'; import Driver, {READ, WRITE} from './driver'; import TransactionExecutor from './internal/transaction-executor'; +import Bookmark from './internal/bookmark'; /** * A Session instance is used for handling the connection and @@ -37,7 +38,7 @@ class Session { * @constructor * @param {string} mode the default access mode for this session. * @param {ConnectionProvider} connectionProvider - the connection provider to acquire connections from. - * @param {string} [bookmark=undefined] - the initial bookmark for this session. + * @param {Bookmark} bookmark - the initial bookmark for this session. * @param {Object} [config={}] - this driver configuration. */ constructor(mode, connectionProvider, bookmark, config) { @@ -94,21 +95,17 @@ class Session { * * While a transaction is open the session cannot be used to run statements outside the transaction. * - * @param {string} bookmark - a reference to a previous transaction. DEPRECATED: This parameter is deprecated in - * favour of {@link Driver#session} that accepts an initial bookmark. Session will ensure that all nested - * transactions are chained with bookmarks to guarantee causal consistency. + * @param {string|string[]} [bookmarkOrBookmarks=null] - reference or references to some previous transactions. + * DEPRECATED: This parameter is deprecated in favour of {@link Driver#session} that accepts an initial bookmark. + * Session will ensure that all nested transactions are chained with bookmarks to guarantee causal consistency. * @returns {Transaction} - New Transaction */ - beginTransaction(bookmark) { - return this._beginTransaction(this._mode, bookmark); + beginTransaction(bookmarkOrBookmarks) { + this._updateBookmark(new Bookmark(bookmarkOrBookmarks)); + return this._beginTransaction(this._mode); } - _beginTransaction(accessMode, bookmark) { - if (bookmark) { - assertString(bookmark, 'Bookmark'); - this._updateBookmark(bookmark); - } - + _beginTransaction(accessMode) { 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 different session.'); @@ -128,10 +125,10 @@ class Session { /** * Return the bookmark received following the last completed {@link Transaction}. * - * @return a reference to a previous transaction + * @return {string|null} a reference to a previous transaction */ lastBookmark() { - return this._lastBookmark; + return this._lastBookmark.maxBookmarkAsString(); } /** @@ -170,13 +167,18 @@ class Session { _runTransaction(accessMode, transactionWork) { return this._transactionExecutor.execute( - () => this._beginTransaction(accessMode, this.lastBookmark()), + () => this._beginTransaction(accessMode), transactionWork ); } + /** + * Update value of the last bookmark. + * @param {Bookmark} newBookmark the new bookmark. + * @private + */ _updateBookmark(newBookmark) { - if (newBookmark) { + if (newBookmark && !newBookmark.isEmpty()) { this._lastBookmark = newBookmark; } } diff --git a/src/v1/transaction.js b/src/v1/transaction.js index c4c0fa7e4..1c1ac34b3 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -20,6 +20,7 @@ import StreamObserver from './internal/stream-observer'; import Result from './result'; import {assertString} from './internal/util'; import {EMPTY_CONNECTION_HOLDER} from './internal/connection-holder'; +import Bookmark from './internal/bookmark'; /** * Represents a transaction in the Neo4j database. @@ -31,27 +32,23 @@ class Transaction { * @constructor * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. * @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 - * @param onBookmark callback invoked when new bookmark is produced + * @param {function(error: Error): Error} errorTransformer callback use to transform error. + * @param {Bookmark} bookmark bookmark for transaction begin. + * @param {function(bookmark: Bookmark)} onBookmark callback invoked when new bookmark is produced. */ constructor(connectionHolder, onClose, errorTransformer, bookmark, onBookmark) { this._connectionHolder = connectionHolder; - let streamObserver = new _TransactionStreamObserver(this); - let params = {}; - if (bookmark) { - params = {bookmark: bookmark}; - } + const streamObserver = new _TransactionStreamObserver(this); this._connectionHolder.getConnection(streamObserver).then(conn => { - conn.run('BEGIN', params, streamObserver); + conn.run('BEGIN', bookmark.asBeginTransactionParameters(), streamObserver); conn.pullAll(streamObserver); }).catch(error => streamObserver.onError(error)); this._state = _states.ACTIVE; this._onClose = onClose; this._errorTransformer = errorTransformer; - this._onBookmark = onBookmark || (() => {}); + this._onBookmark = onBookmark; } /** @@ -149,7 +146,7 @@ class _TransactionStreamObserver extends StreamObserver { onCompleted(meta) { super.onCompleted(meta); - const bookmark = meta.bookmark; + const bookmark = new Bookmark(meta.bookmark); this._tx._onBookmark(bookmark); } } diff --git a/test/internal/bookmark.test.js b/test/internal/bookmark.test.js new file mode 100644 index 000000000..7b406c65b --- /dev/null +++ b/test/internal/bookmark.test.js @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2002-2017 "Neo Technology,"," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Bookmark from '../../src/v1/internal/bookmark'; + +describe('Bookmark', () => { + + it('should be possible to construct bookmark from string', () => { + const bookmark = new Bookmark('neo4j:bookmark:v1:tx412'); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx412'); + }); + + it('should be possible to construct bookmark from string array', () => { + const bookmark = new Bookmark(['neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx2', 'neo4j:bookmark:v1:tx3']); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx3'); + }); + + it('should be possible to construct bookmark from null', () => { + const bookmark = new Bookmark(null); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.maxBookmarkAsString()).toBeNull(); + }); + + it('should be possible to construct bookmark from undefined', () => { + const bookmark = new Bookmark(undefined); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.maxBookmarkAsString()).toBeNull(); + }); + + it('should be possible to construct bookmark from an empty string', () => { + const bookmark = new Bookmark(''); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.maxBookmarkAsString()).toBeNull(); + }); + + it('should be possible to construct bookmark from empty array', () => { + const bookmark = new Bookmark([]); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.maxBookmarkAsString()).toBeNull(); + }); + + it('should not be possible to construct bookmark from object', () => { + expect(() => new Bookmark({})).toThrowError(TypeError); + expect(() => new Bookmark({bookmark: 'neo4j:bookmark:v1:tx1'})).toThrowError(TypeError); + }); + + it('should not be possible to construct bookmark from number array', () => { + expect(() => new Bookmark([1, 2, 3])).toThrowError(TypeError); + }); + + it('should not be possible to construct bookmark from mixed array', () => { + expect(() => new Bookmark(['neo4j:bookmark:v1:tx1', 2, 'neo4j:bookmark:v1:tx3'])).toThrowError(TypeError); + }); + + it('should keep unparsable bookmark', () => { + const bookmark = new Bookmark('neo4j:bookmark:v1:txWrong'); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:txWrong'); + }); + + it('should skip unparsable bookmarks', () => { + const bookmark = new Bookmark(['neo4j:bookmark:v1:tx42', 'neo4j:bookmark:v1:txWrong', 'neo4j:bookmark:v1:tx4242']); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx4242'); + }); + + it('should turn into empty transaction params when empty', () => { + const bookmark = new Bookmark(null); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.asBeginTransactionParameters()).toEqual({}); + }); + + it('should turn into transaction params when represents single bookmark', () => { + const bookmark = new Bookmark('neo4j:bookmark:v1:tx142'); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.asBeginTransactionParameters()).toEqual({ + bookmark: 'neo4j:bookmark:v1:tx142', + bookmarks: ['neo4j:bookmark:v1:tx142'] + }); + }); + + it('should turn into transaction params when represents multiple bookmarks', () => { + const bookmark = new Bookmark( + ['neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx3', 'neo4j:bookmark:v1:tx42', 'neo4j:bookmark:v1:tx5'] + ); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.asBeginTransactionParameters()).toEqual({ + bookmark: 'neo4j:bookmark:v1:tx42', + bookmarks: ['neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx3', 'neo4j:bookmark:v1:tx42', 'neo4j:bookmark:v1:tx5'] + }); + }); + +}); diff --git a/test/resources/boltkit/multiple_bookmarks.script b/test/resources/boltkit/multiple_bookmarks.script new file mode 100644 index 000000000..5e7344103 --- /dev/null +++ b/test/resources/boltkit/multiple_bookmarks.script @@ -0,0 +1,16 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx94", "bookmarks": ["neo4j:bookmark:v1:tx5", "neo4j:bookmark:v1:tx29", "neo4j:bookmark:v1:tx94", "neo4j:bookmark:v1:tx56", "neo4j:bookmark:v1:tx16", "neo4j:bookmark:v1:tx68"]} + PULL_ALL +S: SUCCESS {} + SUCCESS {} +C: RUN "CREATE (n {name:'Bob'})" {} + PULL_ALL +S: SUCCESS {} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx95"} +C: RUN "COMMIT" {} + PULL_ALL +S: SUCCESS {} + SUCCESS {} diff --git a/test/resources/boltkit/read_tx_with_bookmarks.script b/test/resources/boltkit/read_tx_with_bookmarks.script index a48ee65f9..cb22ffba3 100644 --- a/test/resources/boltkit/read_tx_with_bookmarks.script +++ b/test/resources/boltkit/read_tx_with_bookmarks.script @@ -2,7 +2,7 @@ !: AUTO RESET !: AUTO PULL_ALL -C: RUN "BEGIN" {"bookmark": "OldBookmark"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} @@ -11,7 +11,7 @@ C: RUN "MATCH (n) RETURN n.name AS name" {} S: SUCCESS {"fields": ["name"]} RECORD ["Bob"] RECORD ["Alice"] - SUCCESS {"bookmark": "NewBookmark"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} diff --git a/test/resources/boltkit/write_read_tx_with_bookmark_override.script b/test/resources/boltkit/write_read_tx_with_bookmark_override.script index 889b5d324..238d55b09 100644 --- a/test/resources/boltkit/write_read_tx_with_bookmark_override.script +++ b/test/resources/boltkit/write_read_tx_with_bookmark_override.script @@ -2,19 +2,19 @@ !: AUTO RESET !: AUTO PULL_ALL -C: RUN "BEGIN" {"bookmark": "BookmarkA"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "BookmarkB"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} SUCCESS {} -C: RUN "BEGIN" {"bookmark": "BookmarkOverride"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx99", "bookmarks": ["neo4j:bookmark:v1:tx99"]} PULL_ALL S: SUCCESS {} SUCCESS {} @@ -22,7 +22,7 @@ C: RUN "MATCH (n) RETURN n.name AS name" {} PULL_ALL S: SUCCESS {"fields": ["name"]} RECORD ["Bob"] - SUCCESS {"bookmark": "BookmarkC"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx424242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} diff --git a/test/resources/boltkit/write_read_tx_with_bookmarks.script b/test/resources/boltkit/write_read_tx_with_bookmarks.script index e8207f887..fc4a18e3a 100644 --- a/test/resources/boltkit/write_read_tx_with_bookmarks.script +++ b/test/resources/boltkit/write_read_tx_with_bookmarks.script @@ -2,19 +2,19 @@ !: AUTO RESET !: AUTO PULL_ALL -C: RUN "BEGIN" {"bookmark": "BookmarkA"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "BookmarkB"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} SUCCESS {} -C: RUN "BEGIN" {"bookmark": "BookmarkB"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx4242", "bookmarks": ["neo4j:bookmark:v1:tx4242"]} PULL_ALL S: SUCCESS {} SUCCESS {} @@ -22,7 +22,7 @@ C: RUN "MATCH (n) RETURN n.name AS name" {} PULL_ALL S: SUCCESS {"fields": ["name"]} RECORD ["Bob"] - SUCCESS {"bookmark": "BookmarkC"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx424242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} diff --git a/test/resources/boltkit/write_tx_with_bookmarks.script b/test/resources/boltkit/write_tx_with_bookmarks.script index 08247a9e2..82269cb37 100644 --- a/test/resources/boltkit/write_tx_with_bookmarks.script +++ b/test/resources/boltkit/write_tx_with_bookmarks.script @@ -2,14 +2,14 @@ !: AUTO RESET !: AUTO PULL_ALL -C: RUN "BEGIN" {"bookmark": "OldBookmark"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "NewBookmark"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} diff --git a/test/v1/direct.driver.boltkit.it.js b/test/v1/direct.driver.boltkit.it.js index 781f78df3..ce03ba837 100644 --- a/test/v1/direct.driver.boltkit.it.js +++ b/test/v1/direct.driver.boltkit.it.js @@ -17,8 +17,8 @@ * limitations under the License. */ -import neo4j from '../../lib/v1'; -import {READ, WRITE} from '../../lib/v1/driver'; +import neo4j from '../../src/v1'; +import {READ, WRITE} from '../../src/v1/driver'; import boltkit from './boltkit'; import sharedNeo4j from '../internal/shared-neo4j'; @@ -62,7 +62,7 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(READ, 'OldBookmark'); + const session = driver.session(READ, 'neo4j:bookmark:v1:tx42'); const tx = session.beginTransaction(); tx.run('MATCH (n) RETURN n.name AS name').then(result => { const records = result.records; @@ -71,7 +71,7 @@ describe('direct driver', () => { expect(records[1].get('name')).toEqual('Alice'); tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(() => { driver.close(); @@ -96,14 +96,14 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(WRITE, 'OldBookmark'); + const session = driver.session(WRITE, 'neo4j:bookmark:v1:tx42'); const tx = session.beginTransaction(); tx.run('CREATE (n {name:\'Bob\'})').then(result => { const records = result.records; expect(records.length).toEqual(0); tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(() => { driver.close(); @@ -128,14 +128,14 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(WRITE, 'BookmarkA'); + const session = driver.session(WRITE, 'neo4j:bookmark:v1:tx42'); const writeTx = session.beginTransaction(); writeTx.run('CREATE (n {name:\'Bob\'})').then(result => { const records = result.records; expect(records.length).toEqual(0); writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkB'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); const readTx = session.beginTransaction(); readTx.run('MATCH (n) RETURN n.name AS name').then(result => { @@ -144,7 +144,7 @@ describe('direct driver', () => { expect(records[0].get('name')).toEqual('Bob'); readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkC'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242'); session.close(() => { driver.close(); @@ -171,23 +171,23 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(WRITE, 'BookmarkA'); + const session = driver.session(WRITE, 'neo4j:bookmark:v1:tx42'); const writeTx = session.beginTransaction(); writeTx.run('CREATE (n {name:\'Bob\'})').then(result => { const records = result.records; expect(records.length).toEqual(0); writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkB'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); - const readTx = session.beginTransaction('BookmarkOverride'); + const readTx = session.beginTransaction('neo4j:bookmark:v1:tx99'); readTx.run('MATCH (n) RETURN n.name AS name').then(result => { const records = result.records; expect(records.length).toEqual(1); expect(records[0].get('name')).toEqual('Bob'); readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkC'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242'); session.close(() => { driver.close(); @@ -215,14 +215,14 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(WRITE, 'BookmarkA'); + const session = driver.session(WRITE, 'neo4j:bookmark:v1:tx42'); const writeTx = session.beginTransaction(); writeTx.run('CREATE (n {name:\'Bob\'})').then(result => { const records = result.records; expect(records.length).toEqual(0); writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkB'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); const readTx = session.beginTransaction(null); readTx.run('MATCH (n) RETURN n.name AS name').then(result => { @@ -231,7 +231,7 @@ describe('direct driver', () => { expect(records[0].get('name')).toEqual('Bob'); readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkC'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242'); session.close(() => { driver.close(); diff --git a/test/v1/routing.driver.boltkit.it.js b/test/v1/routing.driver.boltkit.it.js index ebc97eb15..769cb22a1 100644 --- a/test/v1/routing.driver.boltkit.it.js +++ b/test/v1/routing.driver.boltkit.it.js @@ -1072,10 +1072,10 @@ describe('routing driver', () => { const driver = newDriver('bolt+routing://127.0.0.1:9001'); const session = driver.session(); - const tx = session.beginTransaction('OldBookmark'); + const tx = session.beginTransaction('neo4j:bookmark:v1:tx42'); tx.run('CREATE (n {name:\'Bob\'})').then(() => { tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(); driver.close(); @@ -1093,11 +1093,11 @@ describe('routing driver', () => { }); it('should send initial bookmark wihtout access mode', done => { - testWriteSessionWithAccessModeAndBookmark(null, 'OldBookmark', done); + testWriteSessionWithAccessModeAndBookmark(null, 'neo4j:bookmark:v1:tx42', done); }); it('should use write session mode and initial bookmark', done => { - testWriteSessionWithAccessModeAndBookmark(WRITE, 'OldBookmark', done); + testWriteSessionWithAccessModeAndBookmark(WRITE, 'neo4j:bookmark:v1:tx42', done); }); it('should use read session mode and initial bookmark', done => { @@ -1113,7 +1113,7 @@ describe('routing driver', () => { kit.run(() => { const driver = newDriver('bolt+routing://127.0.0.1:9001'); - const session = driver.session(READ, 'OldBookmark'); + const session = driver.session(READ, 'neo4j:bookmark:v1:tx42'); const tx = session.beginTransaction(); tx.run('MATCH (n) RETURN n.name AS name').then(result => { const records = result.records; @@ -1122,7 +1122,7 @@ describe('routing driver', () => { expect(records[1].get('name')).toEqual('Alice'); tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(); driver.close(); @@ -1152,11 +1152,11 @@ describe('routing driver', () => { kit.run(() => { const driver = newDriver('bolt+routing://127.0.0.1:9001'); - const session = driver.session(null, 'BookmarkA'); + const session = driver.session(null, 'neo4j:bookmark:v1:tx42'); const writeTx = session.beginTransaction(); writeTx.run('CREATE (n {name:\'Bob\'})').then(() => { writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkB'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); const readTx = session.beginTransaction(); readTx.run('MATCH (n) RETURN n.name AS name').then(result => { @@ -1165,7 +1165,7 @@ describe('routing driver', () => { expect(records[0].get('name')).toEqual('Bob'); readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkC'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242'); session.close(); driver.close(); @@ -1858,6 +1858,38 @@ describe('routing driver', () => { }); }); + it('should send multiple bookmarks', done => { + const kit = new boltkit.BoltKit(); + const router = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9010); + const writer = kit.start('./test/resources/boltkit/multiple_bookmarks.script', 9007); + + kit.run(() => { + const driver = newDriver('bolt+routing://127.0.0.1:9010'); + + const bookmarks = ['neo4j:bookmark:v1:tx5', 'neo4j:bookmark:v1:tx29', 'neo4j:bookmark:v1:tx94', + 'neo4j:bookmark:v1:tx56', 'neo4j:bookmark:v1:tx16', 'neo4j:bookmark:v1:tx68']; + const session = driver.session(WRITE, bookmarks); + const tx = session.beginTransaction(); + + tx.run(`CREATE (n {name:'Bob'})`).then(() => { + tx.commit().then(() => { + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx95'); + + session.close(); + driver.close(); + + router.exit(code1 => { + writer.exit(code2 => { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + function moveNextDateNow30SecondsForward() { const currentTime = Date.now(); hijackNextDateNowCall(currentTime + 30 * 1000 + 1); @@ -1880,7 +1912,7 @@ describe('routing driver', () => { const tx = session.beginTransaction(); tx.run('CREATE (n {name:\'Bob\'})').then(() => { tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(); driver.close(); diff --git a/test/v1/session.test.js b/test/v1/session.test.js index de4a2daf5..daf6a0dc2 100644 --- a/test/v1/session.test.js +++ b/test/v1/session.test.js @@ -24,6 +24,7 @@ import {READ} from '../../src/v1/driver'; import {SingleConnectionProvider} from '../../src/v1/internal/connection-providers'; import FakeConnection from '../internal/fake-connection'; import sharedNeo4j from '../internal/shared-neo4j'; +import _ from 'lodash'; describe('session', () => { @@ -434,9 +435,9 @@ describe('session', () => { it('should fail nicely for illegal bookmark', () => { expect(() => session.beginTransaction({})).toThrowError(TypeError); + expect(() => session.beginTransaction({foo: 'bar'})).toThrowError(TypeError); expect(() => session.beginTransaction(42)).toThrowError(TypeError); - expect(() => session.beginTransaction([])).toThrowError(TypeError); - expect(() => session.beginTransaction(['bookmark'])).toThrowError(TypeError); + expect(() => session.beginTransaction([42.0, 42.0])).toThrowError(TypeError); }); it('should allow creation of a ' + neo4j.session.READ + ' session', done => { @@ -943,6 +944,27 @@ describe('session', () => { }); }); + it('should send multiple bookmarks', done => { + if (!serverIs31OrLater(done)) { + return; + } + + const nodeCount = 17; + const bookmarkPromises = _.range(nodeCount).map(() => runQueryAndGetBookmark(driver)); + + Promise.all(bookmarkPromises).then(bookmarks => { + expect(_.uniq(bookmarks).length > 1).toBeTruthy(); + bookmarks.forEach(bookmark => expect(_.isString(bookmark)).toBeTruthy()); + + const session = driver.session(READ, bookmarks); + session.run('MATCH (n) RETURN count(n)').then(result => { + const count = result.records[0].get(0).toInt(); + expect(count).toEqual(nodeCount); + session.close(() => done()); + }); + }); + }); + function serverIs31OrLater(done) { // lazy way of checking the version number // if server has been set we know it is at least 3.1 @@ -1004,4 +1026,21 @@ describe('session', () => { }); }); } + + function runQueryAndGetBookmark(driver) { + const session = driver.session(); + const tx = session.beginTransaction(); + + return new Promise((resolve, reject) => { + tx.run('CREATE ()').then(() => { + tx.commit().then(() => { + const bookmark = session.lastBookmark(); + session.close(() => { + resolve(bookmark); + }); + }).catch(error => reject(error)); + }).catch(error => reject(error)); + }); + } + }); diff --git a/test/v1/transaction.test.js b/test/v1/transaction.test.js index c69d44ae7..baa5309b2 100644 --- a/test/v1/transaction.test.js +++ b/test/v1/transaction.test.js @@ -233,7 +233,7 @@ describe('transaction', () => { } const tx = session.beginTransaction(); - expect(session.lastBookmark()).not.toBeDefined(); + expect(session.lastBookmark()).toBeNull(); tx.run("CREATE (:TXNode1)").then(() => { tx.run("CREATE (:TXNode2)").then(() => { tx.commit().then(() => { @@ -249,7 +249,7 @@ describe('transaction', () => { return; } - expect(session.lastBookmark()).not.toBeDefined(); + expect(session.lastBookmark()).toBeNull(); const tx1 = session.beginTransaction(); tx1.run('CREATE ()').then(() => { @@ -282,7 +282,7 @@ describe('transaction', () => { return; } - expect(session.lastBookmark()).not.toBeDefined(); + expect(session.lastBookmark()).toBeNull(); const tx1 = session.beginTransaction(); tx1.run('CREATE ()').then(() => {