@juit/pgproxy-pool 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/connection.cjs +309 -0
- package/dist/connection.cjs.map +6 -0
- package/dist/connection.d.ts +112 -0
- package/dist/connection.mjs +273 -0
- package/dist/connection.mjs.map +6 -0
- package/dist/events.cjs +56 -0
- package/dist/events.cjs.map +6 -0
- package/dist/events.d.ts +16 -0
- package/dist/events.mjs +31 -0
- package/dist/events.mjs.map +6 -0
- package/dist/index.cjs +34 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.mjs +8 -0
- package/dist/index.mjs.map +6 -0
- package/dist/libpq.cjs +32 -0
- package/dist/libpq.cjs.map +6 -0
- package/dist/libpq.d.ts +6 -0
- package/dist/libpq.mjs +7 -0
- package/dist/libpq.mjs.map +6 -0
- package/dist/pool.cjs +486 -0
- package/dist/pool.cjs.map +6 -0
- package/dist/pool.d.ts +159 -0
- package/dist/pool.mjs +451 -0
- package/dist/pool.mjs.map +6 -0
- package/dist/queue.cjs +70 -0
- package/dist/queue.cjs.map +6 -0
- package/dist/queue.d.ts +7 -0
- package/dist/queue.mjs +45 -0
- package/dist/queue.mjs.map +6 -0
- package/package.json +47 -0
- package/src/connection.ts +494 -0
- package/src/events.ts +42 -0
- package/src/index.ts +17 -0
- package/src/libpq.ts +10 -0
- package/src/pool.ts +683 -0
- package/src/queue.ts +47 -0
package/src/pool.ts
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/* eslint-disable no-constant-condition */
|
|
2
|
+
/* eslint-disable no-cond-assign */
|
|
3
|
+
import assert from 'node:assert'
|
|
4
|
+
|
|
5
|
+
import { Connection, convertOptions } from './connection'
|
|
6
|
+
import { Emitter } from './events'
|
|
7
|
+
|
|
8
|
+
import type { ConnectionOptions } from './connection'
|
|
9
|
+
import type { Logger } from './index'
|
|
10
|
+
|
|
11
|
+
/* ========================================================================== *
|
|
12
|
+
* INTERNALS *
|
|
13
|
+
* ========================================================================== */
|
|
14
|
+
|
|
15
|
+
/** Parse a number from an environment variable, or return the default */
|
|
16
|
+
function parseEnvNumber(variable: string, defaultValue: number): number {
|
|
17
|
+
const string = process.env[variable]
|
|
18
|
+
if (string == null) return defaultValue
|
|
19
|
+
const value = parseFloat(string)
|
|
20
|
+
if (isNaN(value)) throw new Error(`Invalid value "${string}" for environment variable "${variable}"`)
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Parse a boolean from an environment variable, or return the default */
|
|
25
|
+
function parseEnvBoolean(variable: string, defaultValue: boolean): boolean {
|
|
26
|
+
const string = process.env[variable]
|
|
27
|
+
if (string == null) return defaultValue
|
|
28
|
+
const value = string.toLowerCase()
|
|
29
|
+
if (value === 'false') return false
|
|
30
|
+
if (value === 'true') return true
|
|
31
|
+
throw new Error(`Invalid value "${string}" for environment variable "${variable}"`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A deferred/unwrapped {@link Promise} handling connection requests */
|
|
35
|
+
class ConnectionRequest {
|
|
36
|
+
private _resolve!: (connection: Connection) => void
|
|
37
|
+
private _reject!: (error: Error) => void
|
|
38
|
+
private _promise: Promise<Connection>
|
|
39
|
+
private _timeout: NodeJS.Timeout
|
|
40
|
+
private _pending: boolean = true
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a new {@link ConnectionRequest} with a timeout, after which the
|
|
44
|
+
* request will be automatically rejected.
|
|
45
|
+
*/
|
|
46
|
+
constructor(timeout: number) {
|
|
47
|
+
this._promise = new Promise((resolve, reject) => {
|
|
48
|
+
this._resolve = resolve
|
|
49
|
+
this._reject = reject
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
this._timeout = setTimeout(() => {
|
|
53
|
+
this.reject(new Error(`Timeout of ${timeout} ms reached acquiring connection`))
|
|
54
|
+
}, timeout).unref()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Return the {@link Promise} to the {@link Connection} */
|
|
58
|
+
get promise(): Promise<Connection> {
|
|
59
|
+
return this._promise
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Determine whether this request is still pending or not */
|
|
63
|
+
get pending(): boolean {
|
|
64
|
+
return this._pending
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Resolve this instance's {@link Promise} with a {@link Connection} */
|
|
68
|
+
resolve(connection: Connection): void {
|
|
69
|
+
clearTimeout(this._timeout)
|
|
70
|
+
if (this._pending) this._resolve(connection)
|
|
71
|
+
this._pending = false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Reject this instance's {@link Promise} with an {@link Error} */
|
|
75
|
+
reject(error: Error): void {
|
|
76
|
+
clearTimeout(this._timeout)
|
|
77
|
+
if (this._pending) this._reject(error)
|
|
78
|
+
this._pending = false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ========================================================================== *
|
|
83
|
+
* TYPES *
|
|
84
|
+
* ========================================================================== */
|
|
85
|
+
|
|
86
|
+
/** Configuration for our {@link ConnectionPool}. */
|
|
87
|
+
export interface ConnectionPoolConfig {
|
|
88
|
+
/**
|
|
89
|
+
* The minimum number of connections to keep in the pool
|
|
90
|
+
*
|
|
91
|
+
* * _default_: `0`
|
|
92
|
+
* * _environment varaible_: `PGPOOLMINSIZE`
|
|
93
|
+
*/
|
|
94
|
+
minimumPoolSize?: number
|
|
95
|
+
/**
|
|
96
|
+
* The maximum number of connections to keep in the pool
|
|
97
|
+
*
|
|
98
|
+
* * _default_: `20` more than `minimumPoolSize`
|
|
99
|
+
* * _environment varaible_: `PGPOOLMAXSIZE`
|
|
100
|
+
*/
|
|
101
|
+
maximumPoolSize?: number
|
|
102
|
+
/**
|
|
103
|
+
* The maximum number of idle connections that can be sitting in the pool.
|
|
104
|
+
*
|
|
105
|
+
* * _default_: the average between `minimumPoolSize` and `maximumPoolSize`
|
|
106
|
+
* * _environment varaible_: `PGPOOLIDLECONN`
|
|
107
|
+
*/
|
|
108
|
+
maximumIdleConnections?: number
|
|
109
|
+
/**
|
|
110
|
+
* The number of seconds after which an `acquire()` call will fail
|
|
111
|
+
*
|
|
112
|
+
* * _default_: `30` sec.
|
|
113
|
+
* * _environment varaible_: `PGPOOLACQUIRETIMEOUT`
|
|
114
|
+
*/
|
|
115
|
+
acquireTimeout?: number
|
|
116
|
+
/**
|
|
117
|
+
* The maximum number of seconds a connection can be borrowed for
|
|
118
|
+
*
|
|
119
|
+
* * _default_: `120` sec.
|
|
120
|
+
* * _environment varaible_: `PGPOOLBORROWTIMEOUT`
|
|
121
|
+
*/
|
|
122
|
+
borrowTimeout?: number
|
|
123
|
+
/**
|
|
124
|
+
* The number of seconds to wait after the creation of a connection failed
|
|
125
|
+
*
|
|
126
|
+
* * _default_: `5` sec.
|
|
127
|
+
* * _environment varaible_: `PGPOOLRETRYINTERVAL`
|
|
128
|
+
*/
|
|
129
|
+
retryInterval?: number
|
|
130
|
+
/**
|
|
131
|
+
* Whether to validate connections on borrow or not
|
|
132
|
+
*
|
|
133
|
+
* * _default_: `true`.
|
|
134
|
+
* * _environment varaible_: `PGPOOLVALIDATEONBORROW`
|
|
135
|
+
*/
|
|
136
|
+
validateOnBorrow?: boolean
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Constructor options for our {@link ConnectionPool} */
|
|
140
|
+
export interface ConnectionPoolOptions extends ConnectionPoolConfig, ConnectionOptions {
|
|
141
|
+
/* Nothing else */
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Statistical informations about a {@link ConnectionPool} */
|
|
145
|
+
export interface ConnectionPoolStats {
|
|
146
|
+
/** The number of {@link Connection}s currently available in the pool */
|
|
147
|
+
available: number,
|
|
148
|
+
/** The number of {@link Connection}s currently borrowed out by the pool */
|
|
149
|
+
borrowed: number,
|
|
150
|
+
/** The number of {@link Connection}s currently connecting */
|
|
151
|
+
connecting: number,
|
|
152
|
+
/** The total number of {@link Connection}s managed by the pool */
|
|
153
|
+
total: number,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Connection callback for events */
|
|
157
|
+
type ConnectionEvictor = (forced?: true) => unknown
|
|
158
|
+
|
|
159
|
+
/** Events generated by our {@link ConnectionPool} */
|
|
160
|
+
interface ConnectionPoolEvents {
|
|
161
|
+
started: () => unknown,
|
|
162
|
+
stopped: () => unknown,
|
|
163
|
+
|
|
164
|
+
/** Emitted after the connection has been created, connected and adopted */
|
|
165
|
+
connection_created: (connection: Connection) => unknown,
|
|
166
|
+
/** Emitted after the connection has been evicted and destroyed */
|
|
167
|
+
connection_destroyed: (connection: Connection) => unknown,
|
|
168
|
+
/** Emitted when a created connection can not be connected */
|
|
169
|
+
connection_aborted: (connection: Connection) => unknown,
|
|
170
|
+
/** Emitted when a connection has been acquired */
|
|
171
|
+
connection_acquired: (connection: Connection) => unknown,
|
|
172
|
+
/** Emitted when a connection has been released */
|
|
173
|
+
connection_released: (connection: Connection) => unknown,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ========================================================================== *
|
|
177
|
+
* CONNECTION POOL *
|
|
178
|
+
* ========================================================================== */
|
|
179
|
+
|
|
180
|
+
export class ConnectionPool extends Emitter<ConnectionPoolEvents> {
|
|
181
|
+
/** Borrowed connections mapped to their borrow timeout */
|
|
182
|
+
private readonly _borrowed = new Map<Connection, NodeJS.Timeout>()
|
|
183
|
+
/** Array of all _available_ connections (that is, not borrowed out) */
|
|
184
|
+
private readonly _available: Connection[] = []
|
|
185
|
+
/** Array of all pending {@link ConnectionRequest}s */
|
|
186
|
+
private readonly _pending: ConnectionRequest[] = []
|
|
187
|
+
/** All connections mapped to their evictor callback handler */
|
|
188
|
+
private readonly _connections = new Map<Connection, ConnectionEvictor>()
|
|
189
|
+
/** A {@link WeakMap} of connections already evicted by this pool */
|
|
190
|
+
private readonly _evicted = new WeakSet<Connection>()
|
|
191
|
+
|
|
192
|
+
/** The minimum number of connections to keep in the pool */
|
|
193
|
+
private readonly _minimumPoolSize: number
|
|
194
|
+
/** The maximum number of connections to keep in the pool */
|
|
195
|
+
private readonly _maximumPoolSize: number
|
|
196
|
+
/** The maximum number of idle connections that can be sitting in the pool */
|
|
197
|
+
private readonly _maximumIdleConnections: number
|
|
198
|
+
/** The number of *milliseconds* after which an `acquire()` call will fail */
|
|
199
|
+
private readonly _acquireTimeoutMs: number
|
|
200
|
+
/** The maximum number of *milliseconds* a connection can be borrowed for */
|
|
201
|
+
private readonly _borrowTimeoutMs: number
|
|
202
|
+
/** The number of *milliseconds* to wait after the creation of a connection failed */
|
|
203
|
+
private readonly _retryIntervalMs: number
|
|
204
|
+
/** Whether to validate connections on borrow or not */
|
|
205
|
+
private readonly _validateOnBorrow: boolean
|
|
206
|
+
/** The {@link ConnectionOptions} converted into a string for `LibPQ` */
|
|
207
|
+
private readonly _connectionOptions: string
|
|
208
|
+
|
|
209
|
+
/** Indicator on whether this {@link ConnectionPool} was started or not */
|
|
210
|
+
private _started: boolean = false
|
|
211
|
+
/** Indicator on whether this {@link ConnectionPool} is starting or not */
|
|
212
|
+
private _starting: boolean = false
|
|
213
|
+
|
|
214
|
+
/** Create a new {@link ConnectionPool} */
|
|
215
|
+
constructor(logger: Logger, options?: ConnectionPoolOptions)
|
|
216
|
+
constructor(logger: Logger, options: ConnectionPoolOptions = {}) {
|
|
217
|
+
super(logger)
|
|
218
|
+
|
|
219
|
+
const {
|
|
220
|
+
minimumPoolSize = parseEnvNumber('PGPOOLMINSIZE', 0),
|
|
221
|
+
maximumPoolSize = parseEnvNumber('PGPOOLMAXSIZE', minimumPoolSize + 20),
|
|
222
|
+
maximumIdleConnections = parseEnvNumber('PGPOOLIDLECONN', (maximumPoolSize + minimumPoolSize) / 2),
|
|
223
|
+
acquireTimeout = parseEnvNumber('PGPOOLACQUIRETIMEOUT', 30),
|
|
224
|
+
borrowTimeout = parseEnvNumber('PGPOOLBORROWTIMEOUT', 120),
|
|
225
|
+
retryInterval = parseEnvNumber('PGPOOLRETRYINTERVAL', 5),
|
|
226
|
+
validateOnBorrow = parseEnvBoolean('PGPOOLVALIDATEONBORROW', true),
|
|
227
|
+
...connectionOptions
|
|
228
|
+
} = options
|
|
229
|
+
|
|
230
|
+
this._minimumPoolSize = Math.round(minimumPoolSize)
|
|
231
|
+
this._maximumPoolSize = Math.round(maximumPoolSize)
|
|
232
|
+
this._maximumIdleConnections = Math.ceil(maximumIdleConnections)
|
|
233
|
+
this._acquireTimeoutMs = Math.round(acquireTimeout * 1000)
|
|
234
|
+
this._borrowTimeoutMs = Math.round(borrowTimeout * 1000)
|
|
235
|
+
this._retryIntervalMs = Math.round(retryInterval * 1000)
|
|
236
|
+
this._validateOnBorrow = validateOnBorrow
|
|
237
|
+
|
|
238
|
+
assert(this._minimumPoolSize >= 0, `Invalid minimum pool size: ${this._minimumPoolSize}`)
|
|
239
|
+
assert(this._maximumPoolSize >= 1, `Invalid maximum pool size: ${this._maximumPoolSize}`)
|
|
240
|
+
assert(this._maximumIdleConnections >= 0, `Invalid maximum idle connections: ${this._maximumIdleConnections}`)
|
|
241
|
+
assert(this._acquireTimeoutMs > 0, `Invalid acquire timeout: ${this._acquireTimeoutMs} ms`)
|
|
242
|
+
assert(this._borrowTimeoutMs > 0, `Invalid borrow timeout: ${this._borrowTimeoutMs} ms`)
|
|
243
|
+
assert(this._retryIntervalMs > 0, `Invalid retry interval: ${this._retryIntervalMs} ms`)
|
|
244
|
+
|
|
245
|
+
assert(this._minimumPoolSize <= this._maximumPoolSize,
|
|
246
|
+
`The minimum pool size ${this._minimumPoolSize} must less or equal to the maximum pool size ${this._maximumPoolSize}`)
|
|
247
|
+
assert(this._minimumPoolSize <= this._maximumIdleConnections,
|
|
248
|
+
`The minimum pool size ${this._minimumPoolSize} must less or equal to the maximum number of idle connections ${this._maximumIdleConnections}`)
|
|
249
|
+
assert(this._maximumIdleConnections <= this._maximumPoolSize,
|
|
250
|
+
`The maximum number of idle connections ${this._maximumIdleConnections} must less or equal to the maximum pool size ${this._maximumPoolSize}`)
|
|
251
|
+
|
|
252
|
+
this._connectionOptions = convertOptions(connectionOptions)
|
|
253
|
+
this._logger = logger
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Statistical informations about a {@link ConnectionPool} */
|
|
257
|
+
get stats(): ConnectionPoolStats {
|
|
258
|
+
const available = this._available.length
|
|
259
|
+
const borrowed = this._borrowed.size
|
|
260
|
+
const total = this._connections.size
|
|
261
|
+
const connecting = total - (available + borrowed)
|
|
262
|
+
return { available, borrowed, connecting, total }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Returns a flag indicating whether this pool is running or not */
|
|
266
|
+
get running(): boolean {
|
|
267
|
+
return this._started || this._starting
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Returns the running configuration of this instance */
|
|
271
|
+
get configuration(): Required<ConnectionPoolConfig> {
|
|
272
|
+
return {
|
|
273
|
+
minimumPoolSize: this._minimumPoolSize,
|
|
274
|
+
maximumPoolSize: this._maximumPoolSize,
|
|
275
|
+
maximumIdleConnections: this._maximumIdleConnections,
|
|
276
|
+
acquireTimeout: this._acquireTimeoutMs / 1000,
|
|
277
|
+
borrowTimeout: this._borrowTimeoutMs / 1000,
|
|
278
|
+
retryInterval: this._retryIntervalMs / 1000,
|
|
279
|
+
validateOnBorrow: this._validateOnBorrow,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* ===== CONNECTION MANAGEMENT ============================================ */
|
|
284
|
+
|
|
285
|
+
/* These methods are protected, as they can be overridden to provide more
|
|
286
|
+
* specialized versions of connections */
|
|
287
|
+
|
|
288
|
+
/** Create a connection */
|
|
289
|
+
protected _create(logger: Logger, params: string): Connection {
|
|
290
|
+
return new Connection(logger, params)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Validate a connection by issuing a super-simple statement */
|
|
294
|
+
protected async _validate(connection: Connection): Promise<boolean> {
|
|
295
|
+
if (! connection.connected) return false
|
|
296
|
+
if (! this._validateOnBorrow) return true
|
|
297
|
+
|
|
298
|
+
const start = process.hrtime.bigint()
|
|
299
|
+
try {
|
|
300
|
+
this._logger.debug(`Validating connection "${connection.id}"`)
|
|
301
|
+
const result = await connection.query('SELECT now()')
|
|
302
|
+
return result.rowCount === 1
|
|
303
|
+
} catch (error: any) {
|
|
304
|
+
this._logger.error(`Error validating connection "${connection.id}":`, error)
|
|
305
|
+
return false
|
|
306
|
+
} finally {
|
|
307
|
+
const time = process.hrtime.bigint() - start
|
|
308
|
+
const ms = Math.floor(Number(time) / 10000) / 100
|
|
309
|
+
this._logger.debug(`Connection "${connection.id}" validated in ${ms} ms`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Recycle a connection rolling back any running transaction */
|
|
314
|
+
protected async _recycle(connection: Connection): Promise<boolean> {
|
|
315
|
+
if (! connection.connected) return false
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const result = await connection.query('SELECT pg_current_xact_id_if_assigned() IS NOT NULL')
|
|
319
|
+
if (result.rows[0]?.[0] === 't') {
|
|
320
|
+
this._logger.warn(`Rolling back transaction recycling connection "${connection.id}"`)
|
|
321
|
+
await connection.query('ROLLBACK')
|
|
322
|
+
}
|
|
323
|
+
return true
|
|
324
|
+
} catch (error: any) {
|
|
325
|
+
this._logger.error(`Error recycling connection "${connection.id}":`, error)
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* ===== CONNECTION / POOL INTERACTION ==================================== */
|
|
331
|
+
|
|
332
|
+
/** Adopt a connection tying it to our events */
|
|
333
|
+
private _adopt(connection: Connection): Connection {
|
|
334
|
+
/* Dispose of the connection when the pool is stopped */
|
|
335
|
+
const destroyer = (): void => {
|
|
336
|
+
connection.off('destroyed', evictor)
|
|
337
|
+
this._evict(connection)
|
|
338
|
+
}
|
|
339
|
+
this.once('stopped', destroyer)
|
|
340
|
+
|
|
341
|
+
/* Evict the connection when the connection is destroyed */
|
|
342
|
+
const evictor = (forced?: true): void => {
|
|
343
|
+
this.off('stopped', destroyer)
|
|
344
|
+
|
|
345
|
+
/* Here "force" is true when called from "_evict" below... in this case
|
|
346
|
+
we only want to de-register the destroyer, without ending in a loop */
|
|
347
|
+
if (forced) return
|
|
348
|
+
|
|
349
|
+
this._evict(connection)
|
|
350
|
+
this._runCreateLoop()
|
|
351
|
+
}
|
|
352
|
+
connection.once('destroyed', evictor)
|
|
353
|
+
|
|
354
|
+
/* Remember this connection, always... */
|
|
355
|
+
this._connections.set(connection, evictor)
|
|
356
|
+
return connection
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Destroy a connection, it will be wiped from this pool */
|
|
360
|
+
private _evict(connection: Connection, aborted = false): void {
|
|
361
|
+
const evictor = this._connections.get(connection)
|
|
362
|
+
if (! evictor) {
|
|
363
|
+
this._logger.warn(`Attempting to evict non adopted connection ${connection.id}`)
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* Make sure we don't re-invoke ourselves */
|
|
368
|
+
this._connections.delete(connection)
|
|
369
|
+
connection.off('destroyed', evictor)
|
|
370
|
+
|
|
371
|
+
/* Make sure that we deregister from the pool "stopped" event */
|
|
372
|
+
evictor(true)
|
|
373
|
+
|
|
374
|
+
/* coverage ignore catch */
|
|
375
|
+
try {
|
|
376
|
+
this._logger.debug(`Destroying connection "${connection.id}"`)
|
|
377
|
+
|
|
378
|
+
/* Wipe an borrowing details if the connection is borrowed */
|
|
379
|
+
clearTimeout(this._borrowed.get(connection))
|
|
380
|
+
this._borrowed.delete(connection)
|
|
381
|
+
|
|
382
|
+
/* Remove from the available pool, if found there */
|
|
383
|
+
const index = this._available.indexOf(connection)
|
|
384
|
+
if (index >= 0) this._available.splice(index)
|
|
385
|
+
|
|
386
|
+
/* If we know this connection, force disconnection */
|
|
387
|
+
connection.destroy()
|
|
388
|
+
this._emit(aborted ? 'connection_aborted' : 'connection_destroyed', connection)
|
|
389
|
+
} catch (error) {
|
|
390
|
+
this._logger.error(`Error destroying connection "${connection.id}"`)
|
|
391
|
+
} finally {
|
|
392
|
+
this._evicted.add(connection)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* ===== RUN LOOPS ======================================================== */
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Run the create connection loop.
|
|
400
|
+
*
|
|
401
|
+
* This loop simply creates connections, connects them, sets up the various
|
|
402
|
+
* event handler (on disconnect) and simply adds them to the available array.
|
|
403
|
+
*/
|
|
404
|
+
private _runCreateLoop(): void {
|
|
405
|
+
/* coverage ignore if */
|
|
406
|
+
if (! this._started) return
|
|
407
|
+
|
|
408
|
+
Promise.resolve().then(async () => {
|
|
409
|
+
while (this._started) {
|
|
410
|
+
/* Do we need to (or should we) create a new connection? We don't want
|
|
411
|
+
* to run in a while loop, as if "connect" fails, we want to delay the
|
|
412
|
+
* retrial of the amount specified in the pool construction options */
|
|
413
|
+
const connections = this._connections.size
|
|
414
|
+
const available = this._available.length
|
|
415
|
+
const pending = this._pending.length
|
|
416
|
+
|
|
417
|
+
if ((available && (connections >= this._minimumPoolSize)) || // enough available for minimum pool size
|
|
418
|
+
((! pending) && (available >= this._maximumIdleConnections)) || // enough maximum idle connections
|
|
419
|
+
(connections >= this._maximumPoolSize)) { // never go over the number of maximum pool size
|
|
420
|
+
break
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* ===== STEP 1: create a connection ================================== */
|
|
424
|
+
|
|
425
|
+
let connection: Connection
|
|
426
|
+
try {
|
|
427
|
+
connection = this._create(this._logger, this._connectionOptions)
|
|
428
|
+
this._adopt(connection)
|
|
429
|
+
} catch (error) {
|
|
430
|
+
const retry = `retrying in ${this._retryIntervalMs} ms`
|
|
431
|
+
this._logger.error(`Error creating pooled connection, ${retry}:`, error)
|
|
432
|
+
|
|
433
|
+
/* Run the create loop, again, but only our retry interval has elapsed */
|
|
434
|
+
await new Promise((resolve) => setTimeout(resolve, this._retryIntervalMs))
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* ===== STEP 2: connect the connection =============================== */
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
await connection.connect()
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const retry = `retrying in ${this._retryIntervalMs} ms`
|
|
444
|
+
this._logger.error(`Error connecting "${connection.id}", ${retry}:`, error)
|
|
445
|
+
this._evict(connection, true)
|
|
446
|
+
|
|
447
|
+
/* Run the create loop, again, but only our retry interval has elapsed */
|
|
448
|
+
await new Promise((resolve) => setTimeout(resolve, this._retryIntervalMs))
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* coverage ignore else // The pool might have been stopped while, but
|
|
453
|
+
* in this case, the `connect()` method above will throw saying that
|
|
454
|
+
* the connection has been aborted and eviction will run in the `catch`
|
|
455
|
+
* statement above... This `if` / `else` is here as a fail-safe... */
|
|
456
|
+
if (this._started) this._available.push(connection)
|
|
457
|
+
else this._evict(connection, true)
|
|
458
|
+
|
|
459
|
+
/* Run our borrow loops and assign connnections to pending requests */
|
|
460
|
+
this._runBorrowLoop()
|
|
461
|
+
|
|
462
|
+
/* We have created a connection in this pool */
|
|
463
|
+
this._emit('connection_created', connection)
|
|
464
|
+
}
|
|
465
|
+
}).catch(/* coverage ignore next */ (error) => {
|
|
466
|
+
const retry = `retrying in ${this._retryIntervalMs} ms`
|
|
467
|
+
this._logger.error(`Error in create loop, ${retry}:`, error)
|
|
468
|
+
setTimeout(() => this._runCreateLoop(), this._retryIntervalMs)
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Run the borrow connection loop.
|
|
474
|
+
*
|
|
475
|
+
* This loop looks at all the pending connection requests, and fullfills them
|
|
476
|
+
* with a connection from the available array. If no connections are available
|
|
477
|
+
* then it simply triggers the create loop.
|
|
478
|
+
*/
|
|
479
|
+
private _runBorrowLoop(): void {
|
|
480
|
+
/* coverage ignore if */
|
|
481
|
+
if (! this._started) return
|
|
482
|
+
|
|
483
|
+
Promise.resolve().then(async () =>{
|
|
484
|
+
let request: ConnectionRequest | undefined
|
|
485
|
+
while (this._started && (request = this._pending.splice(0, 1)[0])) {
|
|
486
|
+
/* Check if a connection is available, if not, run the create loop */
|
|
487
|
+
const connection = this._available.splice(0, 1)[0]
|
|
488
|
+
if (! connection) {
|
|
489
|
+
if (request.pending) this._pending.unshift(request)
|
|
490
|
+
return this._runCreateLoop()
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* This request might not be pending, it might have timed out */
|
|
494
|
+
if (! request.pending) {
|
|
495
|
+
if (this._available.length >= this._maximumIdleConnections) {
|
|
496
|
+
this._evict(connection)
|
|
497
|
+
} else {
|
|
498
|
+
this._available.push(connection)
|
|
499
|
+
}
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
/* If a connection is available, it should be validated on borrow */
|
|
505
|
+
const valid = await this._validate(connection)
|
|
506
|
+
|
|
507
|
+
/* The pool might have been stopped while validating, simply return
|
|
508
|
+
* and let the "stopped" event handler do its job */
|
|
509
|
+
if (! this._started) {
|
|
510
|
+
request.reject(new Error(`Pool stopped while validatin connection ${connection.id}`))
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/* The connection was not valid, disconnect it and try again */
|
|
515
|
+
if (! valid) {
|
|
516
|
+
/* Any pending request goes back at the beginning of the queue */
|
|
517
|
+
if (request.pending) this._pending.unshift(request)
|
|
518
|
+
this._evict(connection) // will trigger the "disconnected" event
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* While validating, the request might have been timed out */
|
|
523
|
+
if (! request.pending) {
|
|
524
|
+
/* If the request is not pending anymore, just release this
|
|
525
|
+
* connection. This might trigger an extra validation/recycle,
|
|
526
|
+
* but it's definitely better than throwing this away */
|
|
527
|
+
this.release(connection)
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* The connection is valid, and the request is pending. Borrow out
|
|
532
|
+
* this connection to fullfill the request, after setting up our
|
|
533
|
+
* borrowing timeout */
|
|
534
|
+
const timeout = setTimeout(() => {
|
|
535
|
+
this._logger.error(`Connection "${connection.id}" borrowed for too long`)
|
|
536
|
+
this._evict(connection)
|
|
537
|
+
}, this._borrowTimeoutMs).unref()
|
|
538
|
+
|
|
539
|
+
/* Remember this timeout in our borrow list */
|
|
540
|
+
this._borrowed.set(connection, timeout)
|
|
541
|
+
|
|
542
|
+
/* Lift-off! */
|
|
543
|
+
this._emit('connection_acquired', connection)
|
|
544
|
+
request.resolve(connection)
|
|
545
|
+
}
|
|
546
|
+
}).catch(/* coverage ignore next */ (error) => {
|
|
547
|
+
const retry = `retrying in ${this._retryIntervalMs} ms`
|
|
548
|
+
this._logger.error(`Error in borrow loop, ${retry}:`, error)
|
|
549
|
+
setTimeout(() => this._runBorrowLoop(), this._retryIntervalMs)
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* ===== CONNECTION LIFECYCLE ============================================= */
|
|
554
|
+
|
|
555
|
+
/** Acquire a {@link Connection} from this {@link ConnectionPool} */
|
|
556
|
+
acquire(): Promise<Connection> {
|
|
557
|
+
assert(this._started, 'Connection pool not started')
|
|
558
|
+
|
|
559
|
+
/* Add a new entry to our pending connection requests and run the loop */
|
|
560
|
+
const deferred = new ConnectionRequest(this._acquireTimeoutMs)
|
|
561
|
+
this._pending.push(deferred)
|
|
562
|
+
this._runBorrowLoop()
|
|
563
|
+
|
|
564
|
+
/* Return the deferred connection's promise */
|
|
565
|
+
return deferred.promise
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Release a {@link Connection} back to this {@link ConnectionPool} */
|
|
569
|
+
release(connection: Connection): void {
|
|
570
|
+
/* Check if this pool has once held the connection */
|
|
571
|
+
if (this._evicted.has(connection)) return
|
|
572
|
+
|
|
573
|
+
/* Ensure this is _our_ connection */
|
|
574
|
+
assert(this._connections.has(connection), `Connection "${connection.id}" not owned by this pool`)
|
|
575
|
+
|
|
576
|
+
Promise.resolve().then(async () => {
|
|
577
|
+
this._logger.debug(`Releasing connection "${connection.id}"`)
|
|
578
|
+
|
|
579
|
+
/* Clear up any borrow timeout, and remove from borrowed */
|
|
580
|
+
clearTimeout(this._borrowed.get(connection))
|
|
581
|
+
|
|
582
|
+
/* If the connection is not connected, discard it */
|
|
583
|
+
if (! connection.connected) {
|
|
584
|
+
this._logger.info(`Disconnected connection "${connection.id}" discarded`)
|
|
585
|
+
this._evict(connection)
|
|
586
|
+
|
|
587
|
+
/* If we have enough available connections, discard it */
|
|
588
|
+
} else if (this._available.length >= this._maximumIdleConnections) {
|
|
589
|
+
this._logger.info(`Extra connection "${connection.id}" discarded`)
|
|
590
|
+
this._evict(connection)
|
|
591
|
+
|
|
592
|
+
/* If the connection is not valid, discard it */
|
|
593
|
+
} else if (! await this._recycle(connection)) {
|
|
594
|
+
this._logger.info(`Non-validated connection "${connection.id}" discarded`)
|
|
595
|
+
this._evict(connection)
|
|
596
|
+
|
|
597
|
+
/* If the connection is valid, try to recycle it */
|
|
598
|
+
} else {
|
|
599
|
+
this._logger.debug(`Connection "${connection.id}" released`)
|
|
600
|
+
this._borrowed.delete(connection) // delete from the borrow list
|
|
601
|
+
this._available.push(connection) // add to the available list
|
|
602
|
+
this._emit('connection_released', connection)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/* Any error might come from trying to validate/recycle the connection */
|
|
606
|
+
}).catch((error) => {
|
|
607
|
+
this._logger.error(`Error releasing connection "${connection.id}":`, error)
|
|
608
|
+
this._evict(connection)
|
|
609
|
+
|
|
610
|
+
/* Regardless of whatever happened, always run our borrow loop */
|
|
611
|
+
}).finally(() => this._runBorrowLoop())
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* ===== POOL LIFECYCLE =================================================== */
|
|
615
|
+
|
|
616
|
+
/** Start this {@link ConnectionPool} validating an initial connection */
|
|
617
|
+
async start(): Promise<this> {
|
|
618
|
+
if (this._started || this._starting) return this
|
|
619
|
+
|
|
620
|
+
this._logger.debug('Starting connection pool')
|
|
621
|
+
this._starting = true
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
/* Create the initial connection */
|
|
625
|
+
const connection = this._create(this._logger, this._connectionOptions)
|
|
626
|
+
await connection.connect()
|
|
627
|
+
|
|
628
|
+
/* Connect and alidate the initial connection */
|
|
629
|
+
const valid = await this._validate(connection)
|
|
630
|
+
assert(valid, `Unable to validate initial connection "${connection.id}"`)
|
|
631
|
+
this._logger.debug(`Initial connection "${connection.id}" validated`)
|
|
632
|
+
|
|
633
|
+
/* We have a valid connection: adopt it and mark ourselves as started,
|
|
634
|
+
* before sending out events to our listeners */
|
|
635
|
+
this._adopt(connection)
|
|
636
|
+
this._started = true
|
|
637
|
+
|
|
638
|
+
this._emit('started')
|
|
639
|
+
this._emit('connection_created', connection)
|
|
640
|
+
|
|
641
|
+
/* If we can keep idle connections, remember the initial one */
|
|
642
|
+
if (this._maximumIdleConnections > 0) {
|
|
643
|
+
this._available.push(connection)
|
|
644
|
+
} else {
|
|
645
|
+
this._evict(connection)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* Run our create loop to create all needed (minimum) connections */
|
|
649
|
+
this._runCreateLoop()
|
|
650
|
+
return this
|
|
651
|
+
} finally {
|
|
652
|
+
this._starting = false
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Stop this {@link ConnectionPool} and disconnect all connections. */
|
|
657
|
+
stop(): void {
|
|
658
|
+
if (! this._started) return
|
|
659
|
+
this._started = false
|
|
660
|
+
|
|
661
|
+
const connections = `${this._connections.size} connections`
|
|
662
|
+
const requests = `${this._pending.length} pending requests`
|
|
663
|
+
this._logger.info(`Stopping connection pool with ${connections} and ${requests}`)
|
|
664
|
+
|
|
665
|
+
/* Reject any pending acquisition */
|
|
666
|
+
for (const pending of this._pending) {
|
|
667
|
+
pending.reject(new Error('Connection pool stopped'))
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* Clean up our internal lists */
|
|
671
|
+
this._available.splice(0, Number.MAX_SAFE_INTEGER)
|
|
672
|
+
this._borrowed.clear()
|
|
673
|
+
|
|
674
|
+
/* Let the "stopped" event handler close up all connections. Note that this
|
|
675
|
+
* is _synchronous_. We register an evictor on "stopped" which directly
|
|
676
|
+
* invokes this pool's "_destroy()"... Errors will simply be logged */
|
|
677
|
+
try {
|
|
678
|
+
this._emit('stopped')
|
|
679
|
+
} finally {
|
|
680
|
+
this._connections.clear()
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|