Skip to content

Commit 13e9238

Browse files
authored
Make connection acquisition timeout also consider the connection creation time (#877)
The connection acquisition timeout must account for the whole acquisition execution time, whether a new connection is created, an idle connection is picked up instead or we need to wait until the full pool depletes. In particular, the connection acquisition timeout (CAT) has precedence over the socket connection timeout (SCT). If the SCT is set to 2 hours and CAT to 50ms, the connection acquisition should time out after 50ms (as usual, these evil cats win), even if the connection is successfully created within the SCT period. Note: if SCT is larger than or equal to CAT, a warning should be issued, as this is likely a misconfiguration (and big SCT values won't have any effect on connection acquisitions anyway).
1 parent 92fbe79 commit 13e9238

File tree

10 files changed

+173
-85
lines changed

10 files changed

+173
-85
lines changed

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

+1-19
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ const {
2525

2626
const { SERVICE_UNAVAILABLE } = error
2727

28-
const DEFAULT_CONNECTION_TIMEOUT_MILLIS = 30000 // 30 seconds by default
29-
3028
const ALLOWED_VALUES_ENCRYPTED = [
3129
null,
3230
undefined,
@@ -58,7 +56,7 @@ export default class ChannelConfig {
5856
this.trustedCertificates = extractTrustedCertificates(driverConfig)
5957
this.knownHostsPath = extractKnownHostsPath(driverConfig)
6058
this.connectionErrorCode = connectionErrorCode || SERVICE_UNAVAILABLE
61-
this.connectionTimeout = extractConnectionTimeout(driverConfig)
59+
this.connectionTimeout = driverConfig.connectionTimeout
6260
}
6361
}
6462

@@ -90,19 +88,3 @@ function extractKnownHostsPath (driverConfig) {
9088
return driverConfig.knownHosts || null
9189
}
9290

93-
function extractConnectionTimeout (driverConfig) {
94-
const configuredTimeout = parseInt(driverConfig.connectionTimeout, 10)
95-
if (configuredTimeout === 0) {
96-
// timeout explicitly configured to 0
97-
return null
98-
} else if (configuredTimeout && configuredTimeout < 0) {
99-
// timeout explicitly configured to a negative value
100-
return null
101-
} else if (!configuredTimeout) {
102-
// timeout not configured, use default value
103-
return DEFAULT_CONNECTION_TIMEOUT_MILLIS
104-
} else {
105-
// timeout configured, use the provided value
106-
return configuredTimeout
107-
}
108-
}

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

+44-41
Original file line numberDiff line numberDiff line change
@@ -74,51 +74,45 @@ class Pool {
7474
* @return {Object} resource that is ready to use.
7575
*/
7676
acquire (address) {
77-
return this._acquire(address).then(resource => {
78-
const key = address.asKey()
77+
const key = address.asKey()
7978

80-
if (resource) {
81-
// New or existing resource acquired
82-
return resource
83-
}
79+
// We're out of resources and will try to acquire later on when an existing resource is released.
80+
const allRequests = this._acquireRequests
81+
const requests = allRequests[key]
82+
if (!requests) {
83+
allRequests[key] = []
84+
}
85+
return new Promise((resolve, reject) => {
86+
let request
8487

85-
// We're out of resources and will try to acquire later on when an existing resource is released.
86-
const allRequests = this._acquireRequests
87-
const requests = allRequests[key]
88-
if (!requests) {
89-
allRequests[key] = []
90-
}
88+
const timeoutId = setTimeout(() => {
89+
// acquisition timeout fired
9190

92-
return new Promise((resolve, reject) => {
93-
let request
94-
95-
const timeoutId = setTimeout(() => {
96-
// acquisition timeout fired
97-
98-
// remove request from the queue of pending requests, if it's still there
99-
// request might've been taken out by the release operation
100-
const pendingRequests = allRequests[key]
101-
if (pendingRequests) {
102-
allRequests[key] = pendingRequests.filter(item => item !== request)
103-
}
104-
105-
if (request.isCompleted()) {
106-
// request already resolved/rejected by the release operation; nothing to do
107-
} else {
108-
// request is still pending and needs to be failed
109-
const activeCount = this.activeResourceCount(address)
110-
const idleCount = this.has(address) ? this._pools[key].length : 0
111-
request.reject(
112-
newError(
113-
`Connection acquisition timed out in ${this._acquisitionTimeout} ms. Pool status: Active conn count = ${activeCount}, Idle conn count = ${idleCount}.`
114-
)
91+
// remove request from the queue of pending requests, if it's still there
92+
// request might've been taken out by the release operation
93+
const pendingRequests = allRequests[key]
94+
if (pendingRequests) {
95+
allRequests[key] = pendingRequests.filter(item => item !== request)
96+
}
97+
98+
if (request.isCompleted()) {
99+
// request already resolved/rejected by the release operation; nothing to do
100+
} else {
101+
// request is still pending and needs to be failed
102+
const activeCount = this.activeResourceCount(address)
103+
const idleCount = this.has(address) ? this._pools[key].length : 0
104+
request.reject(
105+
newError(
106+
`Connection acquisition timed out in ${this._acquisitionTimeout} ms. Pool status: Active conn count = ${activeCount}, Idle conn count = ${idleCount}.`
115107
)
116-
}
117-
}, this._acquisitionTimeout)
108+
)
109+
}
110+
}, this._acquisitionTimeout)
118111

119-
request = new PendingRequest(key, resolve, reject, timeoutId, this._log)
120-
allRequests[key].push(request)
121-
})
112+
request = new PendingRequest(key, resolve, reject, timeoutId, this._log)
113+
allRequests[key].push(request)
114+
115+
this._processPendingAcquireRequests(address)
122116
})
123117
}
124118

@@ -315,7 +309,7 @@ class Pool {
315309
_processPendingAcquireRequests (address) {
316310
const key = address.asKey()
317311
const requests = this._acquireRequests[key]
318-
const poolState = this._poolState[key]
312+
const poolState = this._poolState[key] || new PoolState()
319313
if (requests) {
320314
const pendingRequest = requests.shift() // pop a pending acquire request
321315

@@ -339,6 +333,15 @@ class Pool {
339333
// request is still pending and can be resolved with the newly acquired resource
340334
pendingRequest.resolve(resource) // resolve the pending request with the acquired resource
341335
}
336+
} else {
337+
// failed to acquire a valid resource from the pool
338+
// return the pending request back to the pool
339+
if (!pendingRequest.isCompleted()) {
340+
if (!this._acquireRequests[key]) {
341+
this._acquireRequests[key] = []
342+
}
343+
this._acquireRequests[key].unshift(pendingRequest)
344+
}
342345
}
343346
})
344347
} else {

packages/bolt-connection/test/channel/node/node-channel.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ describe('NodeChannel', () => {
110110
})
111111
})
112112

113-
function createMockedChannel (connected, config = {}) {
113+
function createMockedChannel (connected, config = { connectionTimeout: 30000 }) {
114114
let endCallback = null
115115
const address = ServerAddress.fromUrl('bolt://localhost:9999')
116116
const channelConfig = new ChannelConfig(address, config, SERVICE_UNAVAILABLE)

packages/bolt-connection/test/pool/pool.test.js

+41
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,40 @@ describe('#unit Pool', () => {
877877
expect(resource1.observer).toBeFalsy()
878878
expect(resource2.observer).toBeFalsy()
879879
})
880+
881+
it('should thrown aquisition timeout exception if resource takes longer to be created', async () => {
882+
const address = ServerAddress.fromUrl('bolt://localhost:7687')
883+
const acquisitionTimeout = 1000
884+
let counter = 0
885+
886+
const pool = new Pool({
887+
create: (server, release) =>
888+
new Promise(resolve => setTimeout(
889+
() => resolve(new Resource(server, counter++, release))
890+
, acquisitionTimeout + 10)),
891+
destroy: res => Promise.resolve(),
892+
validate: resourceValidOnlyOnceValidationFunction,
893+
config: new PoolConfig(1, acquisitionTimeout)
894+
})
895+
896+
try {
897+
await pool.acquire(address)
898+
fail('should have thrown')
899+
} catch (e) {
900+
expect(e).toEqual(
901+
newError(
902+
`Connection acquisition timed out in ${acquisitionTimeout} ms. `
903+
+ 'Pool status: Active conn count = 0, Idle conn count = 0.'
904+
)
905+
)
906+
907+
const numberOfIdleResourceAfterResourceGetCreated = await new Promise(resolve =>
908+
setTimeout(() => resolve(idleResources(pool, address)), 11))
909+
910+
expect(numberOfIdleResourceAfterResourceGetCreated).toEqual(1)
911+
expect(counter).toEqual(1)
912+
}
913+
})
880914
})
881915

882916
function expectNoPendingAcquisitionRequests (pool) {
@@ -895,6 +929,13 @@ function expectNoIdleResources (pool, address) {
895929
}
896930
}
897931

932+
function idleResources (pool, address) {
933+
if (pool.has(address)) {
934+
return pool._pools[address.asKey()].length
935+
}
936+
return undefined
937+
}
938+
898939
function expectNumberOfAcquisitionRequests (pool, address, expectedNumber) {
899940
expect(pool._acquireRequests[address.asKey()].length).toEqual(expectedNumber)
900941
}

packages/core/src/driver.ts

+37-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
ACCESS_MODE_READ,
2727
ACCESS_MODE_WRITE,
2828
FETCH_ALL,
29+
DEFAULT_CONNECTION_TIMEOUT_MILLIS,
2930
DEFAULT_POOL_ACQUISITION_TIMEOUT,
3031
DEFAULT_POOL_MAX_SIZE
3132
} from './internal/constants'
@@ -131,12 +132,15 @@ class Driver {
131132
createSession: CreateSession = args => new Session(args)
132133
) {
133134
sanitizeConfig(config)
134-
validateConfig(config)
135+
136+
const log = Logger.create(config)
137+
138+
validateConfig(config, log)
135139

136140
this._id = idGenerator++
137141
this._meta = meta
138142
this._config = config
139-
this._log = Logger.create(config)
143+
this._log = log;
140144
this._createConnectionProvider = createConnectonProvider
141145
this._createSession = createSession
142146

@@ -366,13 +370,22 @@ class Driver {
366370
* @private
367371
* @returns {Object} the given config.
368372
*/
369-
function validateConfig(config: any): any {
373+
function validateConfig(config: any, log: Logger): any {
370374
const resolver = config.resolver
371375
if (resolver && typeof resolver !== 'function') {
372376
throw new TypeError(
373377
`Configured resolver should be a function. Got: ${resolver}`
374378
)
375379
}
380+
381+
if (config.connectionAcquisitionTimeout < config.connectionTimeout) {
382+
log.warn(
383+
'Configuration for "connectionAcquisitionTimeout" should be greater than ' +
384+
'or equal to "connectionTimeout". Otherwise, the connection acquisition ' +
385+
'timeout will take precedence for over the connection timeout in scenarios ' +
386+
'where a new connection is created while it is acquired'
387+
)
388+
}
376389
return config
377390
}
378391

@@ -396,6 +409,7 @@ function sanitizeConfig(config: any) {
396409
config.fetchSize,
397410
DEFAULT_FETCH_SIZE
398411
)
412+
config.connectionTimeout = extractConnectionTimeout(config)
399413
}
400414

401415
/**
@@ -431,6 +445,26 @@ function validateFetchSizeValue(
431445
}
432446
}
433447

448+
/**
449+
* @private
450+
*/
451+
function extractConnectionTimeout (config: any): number|null {
452+
const configuredTimeout = parseInt(config.connectionTimeout, 10)
453+
if (configuredTimeout === 0) {
454+
// timeout explicitly configured to 0
455+
return null
456+
} else if (configuredTimeout && configuredTimeout < 0) {
457+
// timeout explicitly configured to a negative value
458+
return null
459+
} else if (!configuredTimeout) {
460+
// timeout not configured, use default value
461+
return DEFAULT_CONNECTION_TIMEOUT_MILLIS
462+
} else {
463+
// timeout configured, use the provided value
464+
return configuredTimeout
465+
}
466+
}
467+
434468
/**
435469
* @private
436470
* @returns {ConfiguredCustomResolver} new custom resolver that wraps the passed-in resolver function.

packages/core/src/internal/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
const FETCH_ALL = -1
2121
const DEFAULT_POOL_ACQUISITION_TIMEOUT = 60 * 1000 // 60 seconds
2222
const DEFAULT_POOL_MAX_SIZE = 100
23+
const DEFAULT_CONNECTION_TIMEOUT_MILLIS = 30000 // 30 seconds by default
2324

2425
const ACCESS_MODE_READ: 'READ' = 'READ'
2526
const ACCESS_MODE_WRITE: 'WRITE' = 'WRITE'
@@ -37,6 +38,7 @@ export {
3738
FETCH_ALL,
3839
ACCESS_MODE_READ,
3940
ACCESS_MODE_WRITE,
41+
DEFAULT_CONNECTION_TIMEOUT_MILLIS,
4042
DEFAULT_POOL_ACQUISITION_TIMEOUT,
4143
DEFAULT_POOL_MAX_SIZE,
4244
BOLT_PROTOCOL_V1,

packages/core/test/driver.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Driver from '../src/driver'
2121
import { Bookmarks } from '../src/internal/bookmarks'
2222
import { Logger } from '../src/internal/logger'
2323
import { ConfiguredCustomResolver } from '../src/internal/resolver'
24+
import { LogLevel } from '../src/types'
2425

2526
describe('Driver', () => {
2627
let driver: Driver | null
@@ -155,6 +156,44 @@ describe('Driver', () => {
155156
expect(driver.isEncrypted()).toEqual(expectedValue)
156157
})
157158

159+
it.each([
160+
[{ connectionTimeout: 30000, connectionAcquisitionTimeout: 60000 }, true],
161+
[{ connectionTimeout: null, connectionAcquisitionTimeout: 60000 }, true],
162+
[{ connectionTimeout: 30000, connectionAcquisitionTimeout: null }, true],
163+
[{ connectionTimeout: null, connectionAcquisitionTimeout: null }, true],
164+
[{ connectionAcquisitionTimeout: 60000 }, true],
165+
[{ connectionTimeout: 30000 }, true],
166+
[{}, true],
167+
[{ connectionTimeout: 30000, connectionAcquisitionTimeout: 20000 }, false],
168+
[{ connectionAcquisitionTimeout: 20000 }, false],
169+
[{ connectionTimeout: 70000 }, false],
170+
// No connection timeouts should be considered valid, since it means
171+
// the user doesn't case about the connection timeout at all.
172+
[{ connectionTimeout: 0, connectionAcquisitionTimeout: 2000 }, true],
173+
[{ connectionTimeout: -1, connectionAcquisitionTimeout: 2000 }, true],
174+
])('should emit warning if `connectionAcquisitionTimeout` and `connectionTimeout` are conflicting. [%o} ', async (config, valid) => {
175+
const logging = {
176+
level: 'warn' as LogLevel,
177+
logger: jest.fn()
178+
}
179+
180+
const driver = new Driver(META_INFO, { ...config, logging }, mockCreateConnectonProvider(new ConnectionProvider()), createSession)
181+
182+
if (valid) {
183+
expect(logging.logger).not.toHaveBeenCalled()
184+
} else {
185+
expect(logging.logger).toHaveBeenCalledWith(
186+
'warn',
187+
'Configuration for "connectionAcquisitionTimeout" should be greater than ' +
188+
'or equal to "connectionTimeout". Otherwise, the connection acquisition ' +
189+
'timeout will take precedence for over the connection timeout in scenarios ' +
190+
'where a new connection is created while it is acquired'
191+
)
192+
}
193+
194+
await driver.close()
195+
})
196+
158197
function mockCreateConnectonProvider(connectionProvider: ConnectionProvider) {
159198
return (
160199
id: number,
@@ -172,6 +211,7 @@ describe('Driver', () => {
172211
fetchSize: 1000,
173212
maxConnectionLifetime: 3600000,
174213
maxConnectionPoolSize: 100,
214+
connectionTimeout: 30000,
175215
},
176216
connectionProvider,
177217
database: '',

packages/neo4j-driver/test/driver.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ describe('#integration driver', () => {
170170
// Given
171171
const config = {
172172
maxConnectionPoolSize: 2,
173-
connectionAcquisitionTimeout: 0,
173+
connectionAcquisitionTimeout: 1000,
174174
encrypted: false
175175
}
176176
driver = neo4j.driver(

0 commit comments

Comments
 (0)