Skip to content

Commit 33d88ca

Browse files
authored
Introduce connectionLivenessCheckTimeout configuration (neo4j#1162)
This configuration defines the number of milliseconds the connection might be idle before need to perform a liveness check on acquiring from the pool. ```typescript const driver = neo4j.driver(URL, AUTH, { // other configurations, then connectionLivenessCheckTimeout: 30000 // 30 seconds }) ``` Check the API docs for more information.
1 parent bedb211 commit 33d88ca

23 files changed

+691
-34
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Pool, { PoolConfig } from '../pool'
2020
import { error, ConnectionProvider, ServerInfo, newError } from 'neo4j-driver-core'
2121
import AuthenticationProvider from './authentication-provider'
2222
import { object } from '../lang'
23+
import LivenessCheckProvider from './liveness-check-provider'
2324

2425
const { SERVICE_UNAVAILABLE } = error
2526
const AUTHENTICATION_ERRORS = [
@@ -40,6 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider {
4041
this._config = config
4142
this._log = log
4243
this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent, boltAgent })
44+
this._livenessCheckProvider = new LivenessCheckProvider({ connectionLivenessCheckTimeout: config.connectionLivenessCheckTimeout })
4345
this._userAgent = userAgent
4446
this._boltAgent = boltAgent
4547
this._createChannelConnection =
@@ -81,6 +83,7 @@ export default class PooledConnectionProvider extends ConnectionProvider {
8183
_createConnection ({ auth }, address, release) {
8284
return this._createChannelConnection(address).then(connection => {
8385
connection.release = () => {
86+
connection.idleTimestamp = Date.now()
8487
return release(address, connection)
8588
}
8689
this._openConnections[connection.id] = connection
@@ -100,6 +103,15 @@ export default class PooledConnectionProvider extends ConnectionProvider {
100103
return false
101104
}
102105

106+
try {
107+
await this._livenessCheckProvider.check(conn)
108+
} catch (error) {
109+
this._log.debug(
110+
`The connection ${conn.id} is not alive because of an error ${error.code} '${error.message}'`
111+
)
112+
return false
113+
}
114+
103115
try {
104116
await this._authenticationProvider.authenticate({ connection: conn, auth, skipReAuth })
105117
return true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [https://neo4j.com]
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
export default class LivenessCheckProvider {
18+
constructor ({ connectionLivenessCheckTimeout }) {
19+
this._connectionLivenessCheckTimeout = connectionLivenessCheckTimeout
20+
}
21+
22+
/**
23+
* Checks connection liveness with configured params.
24+
*
25+
* @param {Connection} connection
26+
* @returns {Promise<true>} If liveness checks succeed, throws otherwise
27+
*/
28+
async check (connection) {
29+
if (this._isCheckDisabled || this._isNewlyCreatedConnection(connection)) {
30+
return true
31+
}
32+
33+
const idleFor = Date.now() - connection.idleTimestamp
34+
35+
if (this._connectionLivenessCheckTimeout === 0 ||
36+
idleFor > this._connectionLivenessCheckTimeout) {
37+
return await connection.resetAndFlush()
38+
.then(() => true)
39+
}
40+
41+
return true
42+
}
43+
44+
get _isCheckDisabled () {
45+
return this._connectionLivenessCheckTimeout == null || this._connectionLivenessCheckTimeout < 0
46+
}
47+
48+
_isNewlyCreatedConnection (connection) {
49+
return connection.authToken == null
50+
}
51+
}

packages/bolt-connection/src/connection/connection-channel.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export default class ChannelConnection extends Connection {
130130
this._id = idGenerator++
131131
this._address = address
132132
this._server = { address: address.asHostPort() }
133-
this.creationTimestamp = Date.now()
133+
this._creationTimestamp = Date.now()
134134
this._disableLosslessIntegers = disableLosslessIntegers
135135
this._ch = channel
136136
this._chunker = chunker
@@ -220,6 +220,18 @@ export default class ChannelConnection extends Connection {
220220
this._dbConnectionId = value
221221
}
222222

223+
set idleTimestamp (value) {
224+
this._idleTimestamp = value
225+
}
226+
227+
get idleTimestamp () {
228+
return this._idleTimestamp
229+
}
230+
231+
get creationTimestamp () {
232+
return this._creationTimestamp
233+
}
234+
223235
/**
224236
* Send initialization message.
225237
* @param {string} userAgent the user agent for this driver.

packages/bolt-connection/src/connection/connection-delegate.js

+12
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ export default class DelegateConnection extends Connection {
9393
this._delegate.version = value
9494
}
9595

96+
get creationTimestamp () {
97+
return this._delegate.creationTimestamp
98+
}
99+
100+
set idleTimestamp (value) {
101+
this._delegate.idleTimestamp = value
102+
}
103+
104+
get idleTimestamp () {
105+
return this._delegate.idleTimestamp
106+
}
107+
96108
isOpen () {
97109
return this._delegate.isOpen()
98110
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ export default class Connection extends CoreConnection {
5151
throw new Error('not implemented')
5252
}
5353

54+
get creationTimestamp () {
55+
throw new Error('not implemented')
56+
}
57+
58+
set idleTimestamp (value) {
59+
throw new Error('not implemented')
60+
}
61+
62+
get idleTimestamp () {
63+
throw new Error('not implemented')
64+
}
65+
5466
/**
5567
* @returns {BoltProtocol} the underlying bolt protocol assigned to this connection
5668
*/

packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js

+68-15
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Connection, DelegateConnection } from '../../src/connection'
2121
import { authTokenManagers, internal, newError, ServerInfo, staticAuthTokenManager } from 'neo4j-driver-core'
2222
import AuthenticationProvider from '../../src/connection-provider/authentication-provider'
2323
import { functional } from '../../src/lang'
24+
import LivenessCheckProvider from '../../src/connection-provider/liveness-check-provider'
2425

2526
const {
2627
serverAddress: { ServerAddress },
@@ -281,16 +282,23 @@ describe('constructor', () => {
281282
})
282283

283284
it('should register the release function into the connection', async () => {
284-
const { create } = setup()
285-
const releaseResult = { property: 'some property' }
286-
const release = jest.fn(() => releaseResult)
285+
jest.useFakeTimers()
286+
try {
287+
const { create } = setup()
288+
const releaseResult = { property: 'some property' }
289+
const release = jest.fn(() => releaseResult)
287290

288-
const connection = await create({}, server0, release)
291+
const connection = await create({}, server0, release)
292+
connection.idleTimestamp = -1234
289293

290-
const released = connection.release()
294+
const released = connection.release()
291295

292-
expect(released).toBe(releaseResult)
293-
expect(release).toHaveBeenCalledWith(server0, connection)
296+
expect(released).toBe(releaseResult)
297+
expect(release).toHaveBeenCalledWith(server0, connection)
298+
expect(connection.idleTimestamp).toBeCloseTo(Date.now())
299+
} finally {
300+
jest.useRealTimers()
301+
}
294302
})
295303

296304
it.each([
@@ -361,26 +369,27 @@ describe('constructor', () => {
361369
const connection = new FakeConnection(server0)
362370
connection.creationTimestamp = Date.now()
363371

364-
const { validateOnAcquire, authenticationProviderHook } = setup()
372+
const { validateOnAcquire, authenticationProviderHook, livenessCheckProviderHook } = setup()
365373

366374
await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(true)
367375

368376
expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({
369377
connection, auth
370378
})
379+
expect(livenessCheckProviderHook.check).toHaveBeenCalledWith(connection)
371380
})
372381

373382
it.each([
374383
null,
375384
undefined,
376385
{ scheme: 'bearer', credentials: 'token01' }
377-
])('should return true when connection is open and within the lifetime and authentication fails (auth=%o)', async (auth) => {
386+
])('should return false when connection is open and within the lifetime and authentication fails (auth=%o)', async (auth) => {
378387
const connection = new FakeConnection(server0)
379388
const error = newError('failed')
380389
const authenticationProvider = jest.fn(() => Promise.reject(error))
381390
connection.creationTimestamp = Date.now()
382391

383-
const { validateOnAcquire, authenticationProviderHook, log } = setup({ authenticationProvider })
392+
const { validateOnAcquire, authenticationProviderHook, log, livenessCheckProviderHook } = setup({ authenticationProvider })
384393

385394
await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(false)
386395

@@ -391,6 +400,7 @@ describe('constructor', () => {
391400
expect(log.debug).toHaveBeenCalledWith(
392401
`The connection ${connection.id} is not valid because of an error ${error.code} '${error.message}'`
393402
)
403+
expect(livenessCheckProviderHook.check).toHaveBeenCalledWith(connection)
394404
})
395405

396406
it.each([
@@ -401,45 +411,67 @@ describe('constructor', () => {
401411
const auth = {}
402412
connection.creationTimestamp = Date.now()
403413

404-
const { validateOnAcquire, authenticationProviderHook } = setup()
414+
const { validateOnAcquire, authenticationProviderHook, livenessCheckProviderHook } = setup()
405415

406416
await expect(validateOnAcquire({ auth, skipReAuth }, connection)).resolves.toBe(true)
407417

408418
expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({
409419
connection, auth, skipReAuth
410420
})
421+
expect(livenessCheckProviderHook.check).toHaveBeenCalledWith(connection)
422+
})
423+
424+
it('should return false when liveness checks fails', async () => {
425+
const connection = new FakeConnection(server0)
426+
connection.creationTimestamp = Date.now()
427+
const error = newError('#themessage', '#thecode')
428+
429+
const { validateOnAcquire, authenticationProviderHook, log, livenessCheckProviderHook } = setup({
430+
livenessCheckProvider: () => Promise.reject(error)
431+
})
432+
433+
await expect(validateOnAcquire({}, connection)).resolves.toBe(false)
434+
435+
expect(livenessCheckProviderHook.check).toBeCalledWith(connection)
436+
expect(log.debug).toBeCalledWith(
437+
`The connection ${connection.id} is not alive because of an error ${error.code} '${error.message}'`
438+
)
439+
expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled()
411440
})
412441

413442
it('should return false when connection is closed and within the lifetime', async () => {
414443
const connection = new FakeConnection(server0)
415444
connection.creationTimestamp = Date.now()
416445
await connection.close()
417446

418-
const { validateOnAcquire, authenticationProviderHook } = setup()
447+
const { validateOnAcquire, authenticationProviderHook, livenessCheckProviderHook } = setup()
419448

420449
await expect(validateOnAcquire({}, connection)).resolves.toBe(false)
421450
expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled()
451+
expect(livenessCheckProviderHook.check).not.toHaveBeenCalled()
422452
})
423453

424454
it('should return false when connection is open and out of the lifetime', async () => {
425455
const connection = new FakeConnection(server0)
426456
connection.creationTimestamp = Date.now() - 4000
427457

428-
const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 })
458+
const { validateOnAcquire, authenticationProviderHook, livenessCheckProviderHook } = setup({ maxConnectionLifetime: 3000 })
429459

430460
await expect(validateOnAcquire({}, connection)).resolves.toBe(false)
431461
expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled()
462+
expect(livenessCheckProviderHook.check).not.toHaveBeenCalled()
432463
})
433464

434465
it('should return false when connection is closed and out of the lifetime', async () => {
435466
const connection = new FakeConnection(server0)
436467
await connection.close()
437468
connection.creationTimestamp = Date.now() - 4000
438469

439-
const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 })
470+
const { validateOnAcquire, authenticationProviderHook, livenessCheckProviderHook } = setup({ maxConnectionLifetime: 3000 })
440471

441472
await expect(validateOnAcquire({}, connection)).resolves.toBe(false)
442473
expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled()
474+
expect(livenessCheckProviderHook.check).not.toHaveBeenCalled()
443475
})
444476
})
445477

@@ -492,14 +524,17 @@ describe('constructor', () => {
492524
})
493525
})
494526

495-
function setup ({ createConnection, authenticationProvider, maxConnectionLifetime } = {}) {
527+
function setup ({ createConnection, authenticationProvider, maxConnectionLifetime, livenessCheckProvider } = {}) {
496528
const newPool = jest.fn((...args) => new Pool(...args))
497529
const log = new Logger('debug', () => undefined)
498530
jest.spyOn(log, 'debug')
499531
const createChannelConnectionHook = createConnection || jest.fn(async (address) => new FakeConnection(address))
500532
const authenticationProviderHook = new AuthenticationProvider({ })
533+
const livenessCheckProviderHook = new LivenessCheckProvider({})
501534
jest.spyOn(authenticationProviderHook, 'authenticate')
502535
.mockImplementation(authenticationProvider || jest.fn(({ connection }) => Promise.resolve(connection)))
536+
jest.spyOn(livenessCheckProviderHook, 'check')
537+
.mockImplementation(livenessCheckProvider || jest.fn(() => Promise.resolve(true)))
503538
const provider = new DirectConnectionProvider({
504539
newPool,
505540
config: {
@@ -510,11 +545,13 @@ describe('constructor', () => {
510545
})
511546
provider._createChannelConnection = createChannelConnectionHook
512547
provider._authenticationProvider = authenticationProviderHook
548+
provider._livenessCheckProvider = livenessCheckProviderHook
513549
return {
514550
provider,
515551
...newPool.mock.calls[0][0],
516552
createChannelConnectionHook,
517553
authenticationProviderHook,
554+
livenessCheckProviderHook,
518555
log
519556
}
520557
}
@@ -812,6 +849,22 @@ class FakeConnection extends Connection {
812849
return this._supportsReAuth
813850
}
814851

852+
set creationTimestamp (value) {
853+
this._creationTimestamp = value
854+
}
855+
856+
get creationTimestamp () {
857+
return this._creationTimestamp
858+
}
859+
860+
set idleTimestamp (value) {
861+
this._idleTimestamp = value
862+
}
863+
864+
get idleTimestamp () {
865+
return this._idleTimestamp
866+
}
867+
815868
async close () {
816869
this._closed = true
817870
}

0 commit comments

Comments
 (0)