Skip to content

Commit 42e5855

Browse files
bigmontzrobsdedude
andauthored
Introduce Bookmark Manager (#974)
Neo4j clusters are causally consistent, meaning that by default there is no guarantee a query will be able to read changes made by a previous query. For cases where such a guarantee is necessary, the server provides bookmarks to the client. A bookmark is an abstract token that represents some state of the database. By passing one or multiple bookmarks along with a query, the server will make sure that the query will not get executed before the represented state(s) (or a later state) have been established. The bookmark manager is an interface used by the driver for keeping track of the bookmarks and this way keeping sessions automatically consistent. ### Usage in the session Enabling it is done by supplying an BookmarkManager implementation instance to this param. A default implementation could be acquired by calling the factory function `neo4j.bookmarkManager()`. **Warning**: Share the same BookmarkManager instance across all session can have a negative impact on performance since all the queries will wait for the latest changes being propagated across the cluster. For keeping consistency between a group of queries, use `Session` for grouping them. For keeping consistency between a group of sessions, use `BookmarkManager` instance for grouping them. ```javascript const bookmarkManager = neo4j.bookmarkManager() const linkedSession1 = driver.session({ database:'neo4j', bookmarkManager }) const linkedSession2 = driver.session({ database:'neo4j', bookmarkManager }) const unlinkedSession = driver.session({ database:'neo4j' }) // Creating Driver User const createUserQueryResult = await linkedSession1.run('CREATE (p:Person {name: $name})', { name: 'Driver User'}) // Reading Driver User will *NOT* wait of the changes being propagated to the server before RUN the query // So the 'Driver User' person might not exist in the Result const unlinkedReadResult = await unlinkedSession.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Driver User'}) // Reading Driver User will wait of the changes being propagated to the server before RUN the query // So the 'Driver User' person should exist in the Result, unless deleted. const linkedSession2 = await linkedSession2.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Driver User'}) await linkedSession1.close() await linkedSession2.close() await unlinkedSession.close() ``` Co-authored-by: Robsdedude <[email protected]>
1 parent 7f309ca commit 42e5855

29 files changed

+1750
-169
lines changed

packages/bolt-connection/src/connection-provider/connection-provider-routing.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
146146
const routingTable = await this._freshRoutingTable({
147147
accessMode,
148148
database: context.database,
149-
bookmarks: bookmarks,
149+
bookmarks,
150150
impersonatedUser,
151151
onDatabaseNameResolved: (databaseName) => {
152152
context.database = context.database || databaseName

packages/core/src/bookmark-manager.ts

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
/**
21+
* Interface for the piece of software responsible for keeping track of current active bookmarks accross the driver.
22+
* @interface
23+
* @since 5.0
24+
* @experimental
25+
*/
26+
export default class BookmarkManager {
27+
/**
28+
* @constructor
29+
* @private
30+
*/
31+
private constructor () {
32+
throw new Error('Not implemented')
33+
}
34+
35+
/**
36+
* Method called when the bookmarks get updated when a transaction finished.
37+
*
38+
* This method will be called when auto-commit queries finish and when explicit transactions
39+
* get commited.
40+
*
41+
* @param {string} database The database which the bookmarks belongs to
42+
* @param {Iterable<string>} previousBookmarks The bookmarks used when starting the transaction
43+
* @param {Iterable<string>} newBookmarks The new bookmarks received at the end of the transaction.
44+
* @returns {void}
45+
*/
46+
async updateBookmarks (database: string, previousBookmarks: Iterable<string>, newBookmarks: Iterable<string>): Promise<void> {
47+
throw new Error('Not implemented')
48+
}
49+
50+
/**
51+
* Method called by the driver to get the bookmarks for one specific database
52+
*
53+
* @param {string} database The database which the bookmarks belong to
54+
* @returns {Iterable<string>} The set of bookmarks
55+
*/
56+
async getBookmarks (database: string): Promise<Iterable<string>> {
57+
throw new Error('Not implemented')
58+
}
59+
60+
/**
61+
* Method called by the driver for getting all the bookmarks.
62+
*
63+
* This method should return all bookmarks for all databases present in the BookmarkManager.
64+
*
65+
* @returns {Iterable<string>} The set of bookmarks
66+
*/
67+
async getAllBookmarks (): Promise<Iterable<string>> {
68+
throw new Error('Not implemented')
69+
}
70+
71+
/**
72+
* Forget the databases and its bookmarks
73+
*
74+
* This method is not called by the driver. Forgetting unused databases is the user's responsibility.
75+
*
76+
* @param {Iterable<string>} databases The databases which the bookmarks will be removed for.
77+
*/
78+
async forget (databases: Iterable<string>): Promise<void> {
79+
throw new Error('Not implemented')
80+
}
81+
}
82+
83+
export interface BookmarkManagerConfig {
84+
initialBookmarks?: Map<string, Iterable<string>>
85+
bookmarksSupplier?: (database?: string) => Promise<Iterable<string>>
86+
bookmarksConsumer?: (database: string, bookmarks: Iterable<string>) => Promise<void>
87+
}
88+
89+
/**
90+
* @typedef {Object} BookmarkManagerConfig
91+
*
92+
* @since 5.0
93+
* @experimental
94+
* @property {Map<string,Iterable<string>>} [initialBookmarks@experimental] Defines the initial set of bookmarks. The key is the database name and the values are the bookmarks.
95+
* @property {function([database]: string):Promise<Iterable<string>>} [bookmarksSupplier] Called for supplying extra bookmarks to the BookmarkManager
96+
* 1. supplying bookmarks from the given database when the default BookmarkManager's `.getBookmarks(database)` gets called.
97+
* 2. supplying all the bookmarks when the default BookmarkManager's `.getAllBookmarks()` gets called
98+
* @property {function(database: string, bookmarks: Iterable<string>): Promise<void>} [bookmarksConsumer] Called when the set of bookmarks for database get updated
99+
*/
100+
/**
101+
* Provides an configured {@link BookmarkManager} instance.
102+
*
103+
* @since 5.0
104+
* @experimental
105+
* @param {BookmarkManagerConfig} [config={}]
106+
* @returns {BookmarkManager}
107+
*/
108+
export function bookmarkManager (config: BookmarkManagerConfig = {}): BookmarkManager {
109+
const initialBookmarks = new Map<string, Set<string>>()
110+
111+
config.initialBookmarks?.forEach((v, k) => initialBookmarks.set(k, new Set(v)))
112+
113+
return new Neo4jBookmarkManager(
114+
initialBookmarks,
115+
config.bookmarksSupplier,
116+
config.bookmarksConsumer
117+
)
118+
}
119+
120+
class Neo4jBookmarkManager implements BookmarkManager {
121+
constructor (
122+
private readonly _bookmarksPerDb: Map<string, Set<string>>,
123+
private readonly _bookmarksSupplier?: (database?: string) => Promise<Iterable<string>>,
124+
private readonly _bookmarksConsumer?: (database: string, bookmark: Iterable<string>) => Promise<void>
125+
) {
126+
127+
}
128+
129+
async updateBookmarks (database: string, previousBookmarks: Iterable<string>, newBookmarks: Iterable<string>): Promise<void> {
130+
const bookmarks = this._getOrInitializeBookmarks(database)
131+
for (const bm of previousBookmarks) {
132+
bookmarks.delete(bm)
133+
}
134+
for (const bm of newBookmarks) {
135+
bookmarks.add(bm)
136+
}
137+
if (typeof this._bookmarksConsumer === 'function') {
138+
await this._bookmarksConsumer(database, [...bookmarks])
139+
}
140+
}
141+
142+
private _getOrInitializeBookmarks (database: string): Set<string> {
143+
let maybeBookmarks = this._bookmarksPerDb.get(database)
144+
if (maybeBookmarks === undefined) {
145+
maybeBookmarks = new Set()
146+
this._bookmarksPerDb.set(database, maybeBookmarks)
147+
}
148+
return maybeBookmarks
149+
}
150+
151+
async getBookmarks (database: string): Promise<Iterable<string>> {
152+
const bookmarks = new Set(this._bookmarksPerDb.get(database))
153+
154+
if (typeof this._bookmarksSupplier === 'function') {
155+
const suppliedBookmarks = await this._bookmarksSupplier(database) ?? []
156+
for (const bm of suppliedBookmarks) {
157+
bookmarks.add(bm)
158+
}
159+
}
160+
161+
return [...bookmarks]
162+
}
163+
164+
async getAllBookmarks (): Promise<Iterable<string>> {
165+
const bookmarks = new Set<string>()
166+
167+
for (const [, dbBookmarks] of this._bookmarksPerDb) {
168+
for (const bm of dbBookmarks) {
169+
bookmarks.add(bm)
170+
}
171+
}
172+
if (typeof this._bookmarksSupplier === 'function') {
173+
const suppliedBookmarks = await this._bookmarksSupplier() ?? []
174+
for (const bm of suppliedBookmarks) {
175+
bookmarks.add(bm)
176+
}
177+
}
178+
179+
return bookmarks
180+
}
181+
182+
async forget (databases: Iterable<string>): Promise<void> {
183+
for (const database of databases) {
184+
this._bookmarksPerDb.delete(database)
185+
}
186+
}
187+
}

0 commit comments

Comments
 (0)