Skip to content

Commit 34a16ca

Browse files
bigmontzrobsdedude
andauthored
Connection Hint: supporting connection.recv_timeout_seconds (#761)
This value defines the time the client should wait for a response on a connection before it times out. Co-authored-by: Rouven Bauer <[email protected]> Co-authored-by: Robsdedude <[email protected]>
1 parent 843e790 commit 34a16ca

16 files changed

+532
-96
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ export default class WebSocketChannel {
171171
})
172172
}
173173

174+
/**
175+
* Setup the receive timeout for the channel.
176+
*
177+
* Not supported for the browser channel.
178+
*
179+
* @param {number} receiveTimeout The amount of time the channel will keep without receive any data before timeout (ms)
180+
* @returns {void}
181+
*/
182+
setupReceiveTimeout (receiveTimeout) {}
183+
174184
/**
175185
* Set connection timeout on the given WebSocket, if configured.
176186
* @return {number} the timeout id or null.

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ const TrustStrategy = {
148148
* @param {function} onFailure - callback to execute on connection failure.
149149
* @return {*} socket connection.
150150
*/
151-
function connect (config, onSuccess, onFailure = () => null) {
151+
function _connect (config, onSuccess, onFailure = () => null) {
152152
const trustStrategy = trustStrategyName(config)
153153
if (!isEncrypted(config)) {
154154
const socket = net.connect(
@@ -230,7 +230,7 @@ export default class NodeChannel {
230230
* Create new instance
231231
* @param {ChannelConfig} config - configuration for this channel.
232232
*/
233-
constructor (config) {
233+
constructor (config, connect = _connect) {
234234
const self = this
235235

236236
this.id = _CONNECTION_IDGEN++
@@ -305,12 +305,12 @@ export default class NodeChannel {
305305
_setupConnectionTimeout (config, socket) {
306306
const timeout = config.connectionTimeout
307307
if (timeout) {
308-
socket.on('connect', () => {
308+
const connectListener = () => {
309309
// connected - clear connection timeout
310310
socket.setTimeout(0)
311-
})
311+
}
312312

313-
socket.on('timeout', () => {
313+
const timeoutListener = () => {
314314
// timeout fired - not connected within configured time. cancel timeout and destroy socket
315315
socket.setTimeout(0)
316316
socket.destroy(
@@ -319,12 +319,43 @@ export default class NodeChannel {
319319
config.connectionErrorCode
320320
)
321321
)
322-
})
322+
}
323+
324+
socket.on('connect', connectListener)
325+
socket.on('timeout', timeoutListener)
326+
327+
this._removeConnectionTimeoutListeners = () => {
328+
this._conn.off('connect', connectListener)
329+
this._conn.off('timeout', timeoutListener)
330+
}
323331

324332
socket.setTimeout(timeout)
325333
}
326334
}
327335

336+
/**
337+
* Setup the receive timeout for the channel.
338+
*
339+
* @param {number} receiveTimeout How long the channel will wait for receiving data before timing out (ms)
340+
* @returns {void}
341+
*/
342+
setupReceiveTimeout (receiveTimeout) {
343+
if (this._removeConnectionTimeoutListeners) {
344+
this._removeConnectionTimeoutListeners()
345+
}
346+
347+
this._conn.on('timeout', () => {
348+
this._conn.destroy(
349+
newError(
350+
`Connection lost. Server didn't respond in ${receiveTimeout}ms`,
351+
this._connectionErrorCode
352+
)
353+
)
354+
})
355+
356+
this._conn.setTimeout(receiveTimeout)
357+
}
358+
328359
/**
329360
* Write the passed in buffer to connection
330361
* @param {NodeBuffer} buffer - Buffer to write

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919

2020
import { Chunker, Dechunker, ChannelConfig, Channel } from '../channel'
21-
import { newError, error, json, internal } from 'neo4j-driver-core'
21+
import { newError, error, json, internal, toNumber } from 'neo4j-driver-core'
2222
import Connection from './connection'
2323
import Bolt from '../bolt'
2424

@@ -198,6 +198,28 @@ export default class ChannelConnection extends Connection {
198198
if (!this.databaseId) {
199199
this.databaseId = dbConnectionId
200200
}
201+
202+
if (metadata.hints) {
203+
const receiveTimeoutRaw =
204+
metadata.hints['connection.recv_timeout_seconds']
205+
if (
206+
receiveTimeoutRaw !== null &&
207+
receiveTimeoutRaw !== undefined
208+
) {
209+
const receiveTimeoutInSeconds = toNumber(receiveTimeoutRaw)
210+
if (
211+
Number.isInteger(receiveTimeoutInSeconds) &&
212+
receiveTimeoutInSeconds > 0
213+
) {
214+
this._ch.setupReceiveTimeout(receiveTimeoutInSeconds * 1000)
215+
} else {
216+
this._log.info(
217+
`Server located at ${this._address} supplied an invalid connection receive timeout value (${receiveTimeoutInSeconds}). ` +
218+
'Please, verify the server configuration and status because this can be the symptom of a bigger issue.'
219+
)
220+
}
221+
}
222+
}
201223
}
202224
resolve(self)
203225
}

bolt-connection/src/rediscovery/routing-table.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,21 @@ const MIN_ROUTERS = 1
3737
* The routing table object used to determine the role of the servers in the driver.
3838
*/
3939
export default class RoutingTable {
40-
constructor ({ database, routers, readers, writers, expirationTime } = {}) {
40+
constructor ({
41+
database,
42+
routers,
43+
readers,
44+
writers,
45+
expirationTime,
46+
ttl
47+
} = {}) {
4148
this.database = database
4249
this.databaseName = database || 'default database'
4350
this.routers = routers || []
4451
this.readers = readers || []
4552
this.writers = writers || []
4653
this.expirationTime = expirationTime || int(0)
54+
this.ttl = ttl
4755
}
4856

4957
/**
@@ -139,6 +147,7 @@ export function createValidRoutingTable (
139147
routerAddress,
140148
rawRoutingTable
141149
) {
150+
const ttl = rawRoutingTable.ttl
142151
const expirationTime = calculateExpirationTime(rawRoutingTable, routerAddress)
143152
const { routers, readers, writers } = parseServers(
144153
rawRoutingTable,
@@ -153,7 +162,8 @@ export function createValidRoutingTable (
153162
routers,
154163
readers,
155164
writers,
156-
expirationTime
165+
expirationTime,
166+
ttl
157167
})
158168
}
159169

test/internal/browser/browser-channel.test.js renamed to bolt-connection/test/channel/browser/browser-channel.test.js

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
* limitations under the License.
1818
*/
1919

20-
import WebSocketChannel from '../../../bolt-connection/lib/channel/browser/browser-channel'
21-
import ChannelConfig from '../../../bolt-connection/lib/channel/channel-config'
20+
import WebSocketChannel from '../../../src/channel/browser/browser-channel'
21+
import ChannelConfig from '../../../src/channel/channel-config'
2222
import { error, internal } from 'neo4j-driver-core'
23-
import { setTimeoutMock } from '../timers-util'
23+
import { setTimeoutMock } from '../../timers-util'
2424

2525
const {
2626
serverAddress: { ServerAddress },
@@ -35,7 +35,7 @@ const WS_CLOSING = 2
3535
const WS_CLOSED = 3
3636

3737
/* eslint-disable no-global-assign */
38-
describe('#unit WebSocketChannel', () => {
38+
describe('WebSocketChannel', () => {
3939
let webSocketChannel
4040

4141
afterEach(async () => {
@@ -173,7 +173,7 @@ describe('#unit WebSocketChannel', () => {
173173
createWebSocketFactory(WS_CLOSED)
174174
)
175175

176-
await expectAsync(channel.close()).toBeResolved()
176+
await expect(channel.close()).resolves.not.toThrow()
177177
})
178178

179179
it('should resolve close when websocket is closed', async () => {
@@ -186,7 +186,7 @@ describe('#unit WebSocketChannel', () => {
186186
createWebSocketFactory(WS_OPEN)
187187
)
188188

189-
await expectAsync(channel.close()).toBeResolved()
189+
await expect(channel.close()).resolves.not.toThrow()
190190
})
191191

192192
function testFallbackToLiteralIPv6 (boltAddress, expectedWsAddress) {
@@ -294,6 +294,39 @@ describe('#unit WebSocketChannel', () => {
294294
}
295295
})
296296

297+
describe('.setupReceiveTimeout()', () => {
298+
beforeEach(() => {
299+
const address = ServerAddress.fromUrl('http://localhost:8989')
300+
const channelConfig = new ChannelConfig(
301+
address,
302+
{ connectionTimeout: 0 },
303+
SERVICE_UNAVAILABLE
304+
)
305+
webSocketChannel = new WebSocketChannel(
306+
channelConfig,
307+
undefined,
308+
createWebSocketFactory(WS_OPEN)
309+
)
310+
})
311+
312+
it('should exists', () => {
313+
expect(webSocketChannel).toHaveProperty('setupReceiveTimeout')
314+
expect(typeof webSocketChannel.setupReceiveTimeout).toBe('function')
315+
})
316+
317+
it('should not setTimeout', () => {
318+
const fakeSetTimeout = setTimeoutMock.install()
319+
try {
320+
webSocketChannel.setupReceiveTimeout()
321+
322+
expect(fakeSetTimeout._timeoutIdCounter).toEqual(0)
323+
expect(webSocketChannel._connectionTimeoutId).toBe(null)
324+
} finally {
325+
fakeSetTimeout.uninstall()
326+
}
327+
})
328+
})
329+
297330
function createWebSocketFactory (readyState) {
298331
const ws = {}
299332
ws.readyState = readyState

test/internal/browser/browser-host-name-resolver.test.js renamed to bolt-connection/test/channel/browser/browser-host-name-resolver.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* limitations under the License.
1818
*/
1919

20-
import BrowserHostNameResolver from '../../../bolt-connection/lib/channel/browser/browser-host-name-resolver'
20+
import BrowserHostNameResolver from '../../../src/channel/browser/browser-host-name-resolver'
2121

2222
describe('#unit BrowserHostNameResolver', () => {
2323
it('should resolve given address to itself', done => {

0 commit comments

Comments
 (0)