@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/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
+ }