node-firebird 2.7.0 → 2.8.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 +39 -0
- package/lib/pool.d.ts +43 -1
- package/lib/pool.js +124 -7
- package/lib/types.d.ts +28 -0
- package/package.json +1 -1
- package/src/pool.ts +126 -6
- package/src/types.ts +33 -0
package/README.md
CHANGED
|
@@ -217,6 +217,44 @@ pool.get(function (err, db) {
|
|
|
217
217
|
pool.destroy();
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
+
#### Pool events and metrics
|
|
221
|
+
|
|
222
|
+
The pool is an `EventEmitter` and exposes live counters, following the
|
|
223
|
+
`pg.Pool` conventions:
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
const pool = Firebird.pool(10, {
|
|
227
|
+
...options,
|
|
228
|
+
idleTimeoutMillis: 30000, // close connections idle for 30s…
|
|
229
|
+
min: 2, // …but always keep 2 alive
|
|
230
|
+
connectTimeout: 5000,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
pool.on('connect', (db) => console.log('new server connection'));
|
|
234
|
+
pool.on('acquire', (db) => console.log('connection handed to a caller'));
|
|
235
|
+
pool.on('release', (db) => console.log('connection returned to the pool'));
|
|
236
|
+
pool.on('remove', (db) => console.log('connection closed & removed'));
|
|
237
|
+
pool.on('error', (err, db) => console.error('background pool error', err));
|
|
238
|
+
|
|
239
|
+
// live metrics — e.g. for a /health endpoint or periodic monitoring
|
|
240
|
+
console.log({
|
|
241
|
+
total: pool.totalCount, // physical connections (idle + in use)
|
|
242
|
+
idle: pool.idleCount, // available in the pool
|
|
243
|
+
active: pool.activeCount, // handed out to callers
|
|
244
|
+
waiting: pool.waitingCount, // get() calls queued for a free slot
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- `idleTimeoutMillis` closes connections that sat idle in the pool for that
|
|
249
|
+
long, never shrinking below `min` — long-lived pools no longer hold every
|
|
250
|
+
connection they ever created (issue [#329](https://github.com/hgourvest/node-firebird/issues/329)).
|
|
251
|
+
The sweep also evicts idle connections whose socket has died, so callers
|
|
252
|
+
don't receive them after a server restart (issue [#343](https://github.com/hgourvest/node-firebird/issues/343)).
|
|
253
|
+
- `error` is a background-error channel (idle eviction failures and the
|
|
254
|
+
like); unlike a plain `EventEmitter`, it is only emitted when a listener
|
|
255
|
+
is attached, so existing applications keep working unchanged.
|
|
256
|
+
- Metrics are plain getters — reading them has no side effects.
|
|
257
|
+
|
|
220
258
|
#### Advanced Pooling Features
|
|
221
259
|
|
|
222
260
|
The pool implementation includes several safeguards for reliability:
|
|
@@ -224,6 +262,7 @@ The pool implementation includes several safeguards for reliability:
|
|
|
224
262
|
1. **Connection Timeout**: Use `options.connectTimeout` to prevent the pool from hanging if a server accepts the TCP connection but fails to respond to the Firebird wire protocol (e.g., during high load or authentication stalls).
|
|
225
263
|
2. **Pool Destruction**: Calling `pool.destroy()` now immediately drains the `pending` queue, notifying all waiting callers with an error. It also prevents any further `pool.get()` calls.
|
|
226
264
|
3. **Slot Recovery**: If a connection attempt times out, the pool slot is correctly freed so subsequent requests can be served. Late-arriving connections are automatically discarded to prevent resource leaks.
|
|
265
|
+
4. **Idle Reaping & Health**: `idleTimeoutMillis`/`min` shrink the pool when traffic drops and evict dead idle connections (see [Pool events and metrics](#pool-events-and-metrics)).
|
|
227
266
|
|
|
228
267
|
#### Pool Lifecycle State Diagram
|
|
229
268
|
|
package/lib/pool.d.ts
CHANGED
|
@@ -3,19 +3,61 @@
|
|
|
3
3
|
* Simple Pooling
|
|
4
4
|
*
|
|
5
5
|
***************************************/
|
|
6
|
+
import Events from 'events';
|
|
6
7
|
import type { Callback } from './callback';
|
|
7
8
|
type AttachFn = (options: any, callback: Callback) => void;
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Connection pool with pg.Pool-style observability.
|
|
11
|
+
*
|
|
12
|
+
* Events (all optional to listen to):
|
|
13
|
+
* 'connect' (db) — a new physical connection was established
|
|
14
|
+
* 'acquire' (db) — a connection was handed to a caller
|
|
15
|
+
* 'release' (db) — a connection was returned to the idle pool
|
|
16
|
+
* 'remove' (db) — a physical connection left the pool for good
|
|
17
|
+
* 'error' (err, db) — a background error (idle eviction, reaper detach);
|
|
18
|
+
* only emitted when a listener is attached, so
|
|
19
|
+
* existing applications never crash on it
|
|
20
|
+
*
|
|
21
|
+
* Metrics: totalCount, idleCount, activeCount, waitingCount.
|
|
22
|
+
*
|
|
23
|
+
* Options: max (factory argument), options.min (floor the reaper never
|
|
24
|
+
* shrinks below), options.idleTimeoutMillis (close idle connections after
|
|
25
|
+
* this many ms; 0/absent = never), options.connectTimeout.
|
|
26
|
+
*/
|
|
27
|
+
declare class Pool extends Events.EventEmitter {
|
|
9
28
|
attach: AttachFn;
|
|
10
29
|
internaldb: any[];
|
|
11
30
|
pooldb: any[];
|
|
12
31
|
dbinuse: number;
|
|
13
32
|
_creating: number;
|
|
14
33
|
max: number;
|
|
34
|
+
min: number;
|
|
35
|
+
idleTimeoutMillis: number;
|
|
15
36
|
pending: Callback[];
|
|
16
37
|
options: any;
|
|
17
38
|
_destroyed: boolean;
|
|
39
|
+
_reaper: NodeJS.Timeout | null;
|
|
18
40
|
constructor(attach: AttachFn, max: number, options: any);
|
|
41
|
+
/** Physical connections owned by the pool (idle + in use). */
|
|
42
|
+
get totalCount(): number;
|
|
43
|
+
/** Connections sitting idle in the pool. */
|
|
44
|
+
get idleCount(): number;
|
|
45
|
+
/** Connections currently handed out to callers. */
|
|
46
|
+
get activeCount(): number;
|
|
47
|
+
/** get() requests queued for a free slot. */
|
|
48
|
+
get waitingCount(): number;
|
|
49
|
+
/** True when the connection can no longer be used. */
|
|
50
|
+
_isDead(db: any): boolean;
|
|
51
|
+
/** Drop a physical connection from the pool's books and emit 'remove'. */
|
|
52
|
+
_forget(db: any): void;
|
|
53
|
+
/** Emit 'error' only when someone listens — never crash the app. */
|
|
54
|
+
_emitError(err: any, db?: any): void;
|
|
55
|
+
/**
|
|
56
|
+
* Idle sweep: evict dead idle connections immediately and close healthy
|
|
57
|
+
* ones that have been idle longer than idleTimeoutMillis, keeping at
|
|
58
|
+
* least `min` physical connections. (issue #329)
|
|
59
|
+
*/
|
|
60
|
+
_reap(): void;
|
|
19
61
|
get(callback: Callback): this;
|
|
20
62
|
check(): this;
|
|
21
63
|
destroy(callback?: (err?: any) => void): void;
|
package/lib/pool.js
CHANGED
|
@@ -4,18 +4,124 @@
|
|
|
4
4
|
* Simple Pooling
|
|
5
5
|
*
|
|
6
6
|
***************************************/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
const events_1 = __importDefault(require("events"));
|
|
7
11
|
const callback_1 = require("./callback");
|
|
8
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Connection pool with pg.Pool-style observability.
|
|
14
|
+
*
|
|
15
|
+
* Events (all optional to listen to):
|
|
16
|
+
* 'connect' (db) — a new physical connection was established
|
|
17
|
+
* 'acquire' (db) — a connection was handed to a caller
|
|
18
|
+
* 'release' (db) — a connection was returned to the idle pool
|
|
19
|
+
* 'remove' (db) — a physical connection left the pool for good
|
|
20
|
+
* 'error' (err, db) — a background error (idle eviction, reaper detach);
|
|
21
|
+
* only emitted when a listener is attached, so
|
|
22
|
+
* existing applications never crash on it
|
|
23
|
+
*
|
|
24
|
+
* Metrics: totalCount, idleCount, activeCount, waitingCount.
|
|
25
|
+
*
|
|
26
|
+
* Options: max (factory argument), options.min (floor the reaper never
|
|
27
|
+
* shrinks below), options.idleTimeoutMillis (close idle connections after
|
|
28
|
+
* this many ms; 0/absent = never), options.connectTimeout.
|
|
29
|
+
*/
|
|
30
|
+
class Pool extends events_1.default.EventEmitter {
|
|
9
31
|
constructor(attach, max, options) {
|
|
32
|
+
super();
|
|
10
33
|
this.attach = attach;
|
|
11
34
|
this.internaldb = []; // connections created by the pool (for destroy)
|
|
12
35
|
this.pooldb = []; // connections available in the pool (idle)
|
|
13
36
|
this.dbinuse = 0; // connections currently handed out to callers
|
|
14
37
|
this._creating = 0; // connections currently being created (attach in flight)
|
|
15
38
|
this.max = max || 4;
|
|
39
|
+
this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
|
|
40
|
+
this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
|
|
16
41
|
this.pending = []; // callbacks waiting for a free slot
|
|
17
42
|
this.options = options;
|
|
18
43
|
this._destroyed = false; // true after destroy() — prevents further use
|
|
44
|
+
this._reaper = null;
|
|
45
|
+
if (this.idleTimeoutMillis) {
|
|
46
|
+
var self = this;
|
|
47
|
+
// Sweep at half the idle timeout (bounded to 100ms..30s) so a
|
|
48
|
+
// connection lives at most ~1.5x idleTimeoutMillis. unref() keeps
|
|
49
|
+
// the timer from holding the process open.
|
|
50
|
+
var interval = Math.min(Math.max(this.idleTimeoutMillis / 2, 100), 30000);
|
|
51
|
+
this._reaper = setInterval(function () { self._reap(); }, interval);
|
|
52
|
+
if (this._reaper.unref)
|
|
53
|
+
this._reaper.unref();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Physical connections owned by the pool (idle + in use). */
|
|
57
|
+
get totalCount() {
|
|
58
|
+
return this.internaldb.length;
|
|
59
|
+
}
|
|
60
|
+
/** Connections sitting idle in the pool. */
|
|
61
|
+
get idleCount() {
|
|
62
|
+
return this.pooldb.length;
|
|
63
|
+
}
|
|
64
|
+
/** Connections currently handed out to callers. */
|
|
65
|
+
get activeCount() {
|
|
66
|
+
return this.dbinuse;
|
|
67
|
+
}
|
|
68
|
+
/** get() requests queued for a free slot. */
|
|
69
|
+
get waitingCount() {
|
|
70
|
+
return this.pending.length;
|
|
71
|
+
}
|
|
72
|
+
/** True when the connection can no longer be used. */
|
|
73
|
+
_isDead(db) {
|
|
74
|
+
return !db.connection || db.connection._isClosed || db.connection._isDetach ||
|
|
75
|
+
!db.connection._socket || db.connection._socket.destroyed;
|
|
76
|
+
}
|
|
77
|
+
/** Drop a physical connection from the pool's books and emit 'remove'. */
|
|
78
|
+
_forget(db) {
|
|
79
|
+
var idx = this.internaldb.indexOf(db);
|
|
80
|
+
if (idx !== -1)
|
|
81
|
+
this.internaldb.splice(idx, 1);
|
|
82
|
+
idx = this.pooldb.indexOf(db);
|
|
83
|
+
if (idx !== -1)
|
|
84
|
+
this.pooldb.splice(idx, 1);
|
|
85
|
+
this.emit('remove', db);
|
|
86
|
+
}
|
|
87
|
+
/** Emit 'error' only when someone listens — never crash the app. */
|
|
88
|
+
_emitError(err, db) {
|
|
89
|
+
if (this.listenerCount('error') > 0)
|
|
90
|
+
this.emit('error', err, db);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Idle sweep: evict dead idle connections immediately and close healthy
|
|
94
|
+
* ones that have been idle longer than idleTimeoutMillis, keeping at
|
|
95
|
+
* least `min` physical connections. (issue #329)
|
|
96
|
+
*/
|
|
97
|
+
_reap() {
|
|
98
|
+
if (this._destroyed)
|
|
99
|
+
return;
|
|
100
|
+
var self = this;
|
|
101
|
+
var now = Date.now();
|
|
102
|
+
// iterate over a copy — we splice from pooldb while walking
|
|
103
|
+
this.pooldb.slice().forEach(function (db) {
|
|
104
|
+
if (self._isDead(db)) {
|
|
105
|
+
self._forget(db);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (self.internaldb.length <= self.min)
|
|
109
|
+
return;
|
|
110
|
+
var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
|
|
111
|
+
if (now - idleSince < self.idleTimeoutMillis)
|
|
112
|
+
return;
|
|
113
|
+
self._forget(db);
|
|
114
|
+
db.connection._pooled = false;
|
|
115
|
+
try {
|
|
116
|
+
db.detach(function (err) {
|
|
117
|
+
if (err)
|
|
118
|
+
self._emitError(err, db);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
self._emitError(e, db);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
19
125
|
}
|
|
20
126
|
get(callback) {
|
|
21
127
|
// [Fix 2] Reject immediately if the pool has already been destroyed.
|
|
@@ -41,16 +147,15 @@ class Pool {
|
|
|
41
147
|
if (self.pooldb.length) {
|
|
42
148
|
var db = self.pooldb.shift();
|
|
43
149
|
// Discard connections that have been closed or destroyed while idle
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
if (idx !== -1)
|
|
47
|
-
self.internaldb.splice(idx, 1);
|
|
150
|
+
if (self._isDead(db)) {
|
|
151
|
+
self._forget(db);
|
|
48
152
|
self.pending.unshift(cb);
|
|
49
153
|
setImmediate(function () { self.check(); });
|
|
50
154
|
return self;
|
|
51
155
|
}
|
|
52
156
|
// Idle connection available — hand it out immediately.
|
|
53
157
|
self.dbinuse++;
|
|
158
|
+
self.emit('acquire', db);
|
|
54
159
|
cb(null, db);
|
|
55
160
|
}
|
|
56
161
|
else {
|
|
@@ -113,13 +218,20 @@ class Pool {
|
|
|
113
218
|
if (self.pooldb.indexOf(db) !== -1 || self.internaldb.indexOf(db) === -1)
|
|
114
219
|
return;
|
|
115
220
|
// if not usable don't put it back in the pool
|
|
116
|
-
if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false)
|
|
221
|
+
if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false) {
|
|
117
222
|
self.internaldb.splice(self.internaldb.indexOf(db), 1);
|
|
118
|
-
|
|
223
|
+
self.emit('remove', db);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
db.__poolIdleSince = Date.now();
|
|
119
227
|
self.pooldb.push(db);
|
|
228
|
+
self.emit('release', db);
|
|
229
|
+
}
|
|
120
230
|
self.dbinuse--;
|
|
121
231
|
self.check();
|
|
122
232
|
});
|
|
233
|
+
self.emit('connect', db);
|
|
234
|
+
self.emit('acquire', db);
|
|
123
235
|
}
|
|
124
236
|
cb(err, db);
|
|
125
237
|
});
|
|
@@ -132,6 +244,10 @@ class Pool {
|
|
|
132
244
|
destroy(callback) {
|
|
133
245
|
var self = this;
|
|
134
246
|
self._destroyed = true;
|
|
247
|
+
if (self._reaper) {
|
|
248
|
+
clearInterval(self._reaper);
|
|
249
|
+
self._reaper = null;
|
|
250
|
+
}
|
|
135
251
|
// [Fix 4] Drain pending callbacks so callers are not left hanging.
|
|
136
252
|
// This is critical when destroy() is called as a recovery measure after
|
|
137
253
|
// a timeout: without draining, every concurrent pool.get() that had not
|
|
@@ -170,6 +286,7 @@ class Pool {
|
|
|
170
286
|
self.pooldb.splice(_db_in_pool, 1);
|
|
171
287
|
db.connection._pooled = false;
|
|
172
288
|
db.detach(detachCallback);
|
|
289
|
+
self.emit('remove', db);
|
|
173
290
|
}
|
|
174
291
|
else {
|
|
175
292
|
// [Fix 5] Connection is currently in use (dbinuse > 0).
|
package/lib/types.d.ts
CHANGED
|
@@ -224,6 +224,19 @@ export interface Options {
|
|
|
224
224
|
* expected Firebird server response time under load.
|
|
225
225
|
*/
|
|
226
226
|
connectTimeout?: number;
|
|
227
|
+
/**
|
|
228
|
+
* Pool only: minimum number of physical connections the idle reaper
|
|
229
|
+
* keeps alive. Only meaningful together with `idleTimeoutMillis`.
|
|
230
|
+
* Default 0 (the pool may shrink to no connections).
|
|
231
|
+
*/
|
|
232
|
+
min?: number;
|
|
233
|
+
/**
|
|
234
|
+
* Pool only: close connections that have been idle in the pool for this
|
|
235
|
+
* many milliseconds, never shrinking below `min`. Dead idle connections
|
|
236
|
+
* (server restarts, dropped sockets) are evicted on the same sweep.
|
|
237
|
+
* Default 0 (idle connections are kept forever).
|
|
238
|
+
*/
|
|
239
|
+
idleTimeoutMillis?: number;
|
|
227
240
|
/**
|
|
228
241
|
* **Firebird 6.0+ only (Protocol 20+)**
|
|
229
242
|
*
|
|
@@ -265,9 +278,24 @@ export interface Options {
|
|
|
265
278
|
export interface SvcMgrOptions extends Options {
|
|
266
279
|
manager: true;
|
|
267
280
|
}
|
|
281
|
+
export type PoolEvent = 'connect' | 'acquire' | 'release' | 'remove' | 'error';
|
|
268
282
|
export interface ConnectionPool {
|
|
269
283
|
get(callback: DatabaseCallback): void;
|
|
270
284
|
destroy(callback?: SimpleCallback): void;
|
|
285
|
+
/** Physical connections owned by the pool (idle + in use). */
|
|
286
|
+
readonly totalCount: number;
|
|
287
|
+
/** Connections sitting idle in the pool. */
|
|
288
|
+
readonly idleCount: number;
|
|
289
|
+
/** Connections currently handed out to callers. */
|
|
290
|
+
readonly activeCount: number;
|
|
291
|
+
/** get() requests queued for a free slot. */
|
|
292
|
+
readonly waitingCount: number;
|
|
293
|
+
on(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
|
|
294
|
+
on(event: 'error', listener: (err: Error, db?: Database) => void): this;
|
|
295
|
+
once(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
|
|
296
|
+
once(event: 'error', listener: (err: Error, db?: Database) => void): this;
|
|
297
|
+
off(event: PoolEvent, listener: (...args: any[]) => void): this;
|
|
298
|
+
removeListener(event: PoolEvent, listener: (...args: any[]) => void): this;
|
|
271
299
|
getAsync(): Promise<Database>;
|
|
272
300
|
destroyAsync(): Promise<void>;
|
|
273
301
|
/** Acquire a connection, run `work`, always return the connection to the pool. */
|
package/package.json
CHANGED
package/src/pool.ts
CHANGED
|
@@ -4,32 +4,140 @@
|
|
|
4
4
|
*
|
|
5
5
|
***************************************/
|
|
6
6
|
|
|
7
|
+
import Events from 'events';
|
|
7
8
|
import { fromCallback } from './callback';
|
|
8
9
|
import type { Callback } from './callback';
|
|
9
10
|
|
|
10
11
|
type AttachFn = (options: any, callback: Callback) => void;
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Connection pool with pg.Pool-style observability.
|
|
15
|
+
*
|
|
16
|
+
* Events (all optional to listen to):
|
|
17
|
+
* 'connect' (db) — a new physical connection was established
|
|
18
|
+
* 'acquire' (db) — a connection was handed to a caller
|
|
19
|
+
* 'release' (db) — a connection was returned to the idle pool
|
|
20
|
+
* 'remove' (db) — a physical connection left the pool for good
|
|
21
|
+
* 'error' (err, db) — a background error (idle eviction, reaper detach);
|
|
22
|
+
* only emitted when a listener is attached, so
|
|
23
|
+
* existing applications never crash on it
|
|
24
|
+
*
|
|
25
|
+
* Metrics: totalCount, idleCount, activeCount, waitingCount.
|
|
26
|
+
*
|
|
27
|
+
* Options: max (factory argument), options.min (floor the reaper never
|
|
28
|
+
* shrinks below), options.idleTimeoutMillis (close idle connections after
|
|
29
|
+
* this many ms; 0/absent = never), options.connectTimeout.
|
|
30
|
+
*/
|
|
31
|
+
class Pool extends Events.EventEmitter {
|
|
13
32
|
attach: AttachFn;
|
|
14
33
|
internaldb: any[];
|
|
15
34
|
pooldb: any[];
|
|
16
35
|
dbinuse: number;
|
|
17
36
|
_creating: number;
|
|
18
37
|
max: number;
|
|
38
|
+
min: number;
|
|
39
|
+
idleTimeoutMillis: number;
|
|
19
40
|
pending: Callback[];
|
|
20
41
|
options: any;
|
|
21
42
|
_destroyed: boolean;
|
|
43
|
+
_reaper: NodeJS.Timeout | null;
|
|
22
44
|
|
|
23
45
|
constructor(attach: AttachFn, max: number, options: any) {
|
|
46
|
+
super();
|
|
24
47
|
this.attach = attach;
|
|
25
48
|
this.internaldb = []; // connections created by the pool (for destroy)
|
|
26
49
|
this.pooldb = []; // connections available in the pool (idle)
|
|
27
50
|
this.dbinuse = 0; // connections currently handed out to callers
|
|
28
51
|
this._creating = 0; // connections currently being created (attach in flight)
|
|
29
52
|
this.max = max || 4;
|
|
53
|
+
this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
|
|
54
|
+
this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
|
|
30
55
|
this.pending = []; // callbacks waiting for a free slot
|
|
31
56
|
this.options = options;
|
|
32
57
|
this._destroyed = false; // true after destroy() — prevents further use
|
|
58
|
+
this._reaper = null;
|
|
59
|
+
|
|
60
|
+
if (this.idleTimeoutMillis) {
|
|
61
|
+
var self = this;
|
|
62
|
+
// Sweep at half the idle timeout (bounded to 100ms..30s) so a
|
|
63
|
+
// connection lives at most ~1.5x idleTimeoutMillis. unref() keeps
|
|
64
|
+
// the timer from holding the process open.
|
|
65
|
+
var interval = Math.min(Math.max(this.idleTimeoutMillis / 2, 100), 30000);
|
|
66
|
+
this._reaper = setInterval(function() { self._reap(); }, interval);
|
|
67
|
+
if (this._reaper.unref) this._reaper.unref();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Physical connections owned by the pool (idle + in use). */
|
|
72
|
+
get totalCount(): number {
|
|
73
|
+
return this.internaldb.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Connections sitting idle in the pool. */
|
|
77
|
+
get idleCount(): number {
|
|
78
|
+
return this.pooldb.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Connections currently handed out to callers. */
|
|
82
|
+
get activeCount(): number {
|
|
83
|
+
return this.dbinuse;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** get() requests queued for a free slot. */
|
|
87
|
+
get waitingCount(): number {
|
|
88
|
+
return this.pending.length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** True when the connection can no longer be used. */
|
|
92
|
+
_isDead(db: any): boolean {
|
|
93
|
+
return !db.connection || db.connection._isClosed || db.connection._isDetach ||
|
|
94
|
+
!db.connection._socket || db.connection._socket.destroyed;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Drop a physical connection from the pool's books and emit 'remove'. */
|
|
98
|
+
_forget(db: any): void {
|
|
99
|
+
var idx = this.internaldb.indexOf(db);
|
|
100
|
+
if (idx !== -1) this.internaldb.splice(idx, 1);
|
|
101
|
+
idx = this.pooldb.indexOf(db);
|
|
102
|
+
if (idx !== -1) this.pooldb.splice(idx, 1);
|
|
103
|
+
this.emit('remove', db);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Emit 'error' only when someone listens — never crash the app. */
|
|
107
|
+
_emitError(err: any, db?: any): void {
|
|
108
|
+
if (this.listenerCount('error') > 0) this.emit('error', err, db);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Idle sweep: evict dead idle connections immediately and close healthy
|
|
113
|
+
* ones that have been idle longer than idleTimeoutMillis, keeping at
|
|
114
|
+
* least `min` physical connections. (issue #329)
|
|
115
|
+
*/
|
|
116
|
+
_reap(): void {
|
|
117
|
+
if (this._destroyed) return;
|
|
118
|
+
var self = this;
|
|
119
|
+
var now = Date.now();
|
|
120
|
+
|
|
121
|
+
// iterate over a copy — we splice from pooldb while walking
|
|
122
|
+
this.pooldb.slice().forEach(function(db) {
|
|
123
|
+
if (self._isDead(db)) {
|
|
124
|
+
self._forget(db);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (self.internaldb.length <= self.min) return;
|
|
128
|
+
var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
|
|
129
|
+
if (now - idleSince < self.idleTimeoutMillis) return;
|
|
130
|
+
|
|
131
|
+
self._forget(db);
|
|
132
|
+
db.connection._pooled = false;
|
|
133
|
+
try {
|
|
134
|
+
db.detach(function(err?: any) {
|
|
135
|
+
if (err) self._emitError(err, db);
|
|
136
|
+
});
|
|
137
|
+
} catch (e) {
|
|
138
|
+
self._emitError(e, db);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
33
141
|
}
|
|
34
142
|
|
|
35
143
|
get(callback: Callback): this {
|
|
@@ -59,15 +167,15 @@ class Pool {
|
|
|
59
167
|
if (self.pooldb.length) {
|
|
60
168
|
var db = self.pooldb.shift();
|
|
61
169
|
// Discard connections that have been closed or destroyed while idle
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
if (idx !== -1) self.internaldb.splice(idx, 1);
|
|
170
|
+
if (self._isDead(db)) {
|
|
171
|
+
self._forget(db);
|
|
65
172
|
self.pending.unshift(cb);
|
|
66
173
|
setImmediate(function () { self.check(); });
|
|
67
174
|
return self;
|
|
68
175
|
}
|
|
69
176
|
// Idle connection available — hand it out immediately.
|
|
70
177
|
self.dbinuse++;
|
|
178
|
+
self.emit('acquire', db);
|
|
71
179
|
cb(null, db);
|
|
72
180
|
} else {
|
|
73
181
|
// No idle connection — create a new one via attach().
|
|
@@ -133,14 +241,20 @@ class Pool {
|
|
|
133
241
|
if (self.pooldb.indexOf(db) !== -1 || self.internaldb.indexOf(db) === -1)
|
|
134
242
|
return;
|
|
135
243
|
// if not usable don't put it back in the pool
|
|
136
|
-
if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false)
|
|
244
|
+
if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false) {
|
|
137
245
|
self.internaldb.splice(self.internaldb.indexOf(db), 1);
|
|
138
|
-
|
|
246
|
+
self.emit('remove', db);
|
|
247
|
+
} else {
|
|
248
|
+
db.__poolIdleSince = Date.now();
|
|
139
249
|
self.pooldb.push(db);
|
|
250
|
+
self.emit('release', db);
|
|
251
|
+
}
|
|
140
252
|
|
|
141
253
|
self.dbinuse--;
|
|
142
254
|
self.check();
|
|
143
255
|
});
|
|
256
|
+
self.emit('connect', db);
|
|
257
|
+
self.emit('acquire', db);
|
|
144
258
|
}
|
|
145
259
|
|
|
146
260
|
cb(err, db);
|
|
@@ -157,6 +271,11 @@ class Pool {
|
|
|
157
271
|
var self = this;
|
|
158
272
|
self._destroyed = true;
|
|
159
273
|
|
|
274
|
+
if (self._reaper) {
|
|
275
|
+
clearInterval(self._reaper);
|
|
276
|
+
self._reaper = null;
|
|
277
|
+
}
|
|
278
|
+
|
|
160
279
|
// [Fix 4] Drain pending callbacks so callers are not left hanging.
|
|
161
280
|
// This is critical when destroy() is called as a recovery measure after
|
|
162
281
|
// a timeout: without draining, every concurrent pool.get() that had not
|
|
@@ -197,6 +316,7 @@ class Pool {
|
|
|
197
316
|
self.pooldb.splice(_db_in_pool, 1);
|
|
198
317
|
db.connection._pooled = false;
|
|
199
318
|
db.detach(detachCallback);
|
|
319
|
+
self.emit('remove', db);
|
|
200
320
|
} else {
|
|
201
321
|
// [Fix 5] Connection is currently in use (dbinuse > 0).
|
|
202
322
|
// The caller is responsible for releasing it via detach().
|
package/src/types.ts
CHANGED
|
@@ -281,6 +281,19 @@ export interface Options {
|
|
|
281
281
|
* expected Firebird server response time under load.
|
|
282
282
|
*/
|
|
283
283
|
connectTimeout?: number;
|
|
284
|
+
/**
|
|
285
|
+
* Pool only: minimum number of physical connections the idle reaper
|
|
286
|
+
* keeps alive. Only meaningful together with `idleTimeoutMillis`.
|
|
287
|
+
* Default 0 (the pool may shrink to no connections).
|
|
288
|
+
*/
|
|
289
|
+
min?: number;
|
|
290
|
+
/**
|
|
291
|
+
* Pool only: close connections that have been idle in the pool for this
|
|
292
|
+
* many milliseconds, never shrinking below `min`. Dead idle connections
|
|
293
|
+
* (server restarts, dropped sockets) are evicted on the same sweep.
|
|
294
|
+
* Default 0 (idle connections are kept forever).
|
|
295
|
+
*/
|
|
296
|
+
idleTimeoutMillis?: number;
|
|
284
297
|
/**
|
|
285
298
|
* **Firebird 6.0+ only (Protocol 20+)**
|
|
286
299
|
*
|
|
@@ -324,10 +337,30 @@ export interface SvcMgrOptions extends Options {
|
|
|
324
337
|
manager: true; // Attach to ServiceManager
|
|
325
338
|
}
|
|
326
339
|
|
|
340
|
+
export type PoolEvent = 'connect' | 'acquire' | 'release' | 'remove' | 'error';
|
|
341
|
+
|
|
327
342
|
export interface ConnectionPool {
|
|
328
343
|
get(callback: DatabaseCallback): void;
|
|
329
344
|
destroy(callback?: SimpleCallback): void;
|
|
330
345
|
|
|
346
|
+
// Metrics (live counters, pg.Pool-style)
|
|
347
|
+
/** Physical connections owned by the pool (idle + in use). */
|
|
348
|
+
readonly totalCount: number;
|
|
349
|
+
/** Connections sitting idle in the pool. */
|
|
350
|
+
readonly idleCount: number;
|
|
351
|
+
/** Connections currently handed out to callers. */
|
|
352
|
+
readonly activeCount: number;
|
|
353
|
+
/** get() requests queued for a free slot. */
|
|
354
|
+
readonly waitingCount: number;
|
|
355
|
+
|
|
356
|
+
// Events
|
|
357
|
+
on(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
|
|
358
|
+
on(event: 'error', listener: (err: Error, db?: Database) => void): this;
|
|
359
|
+
once(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
|
|
360
|
+
once(event: 'error', listener: (err: Error, db?: Database) => void): this;
|
|
361
|
+
off(event: PoolEvent, listener: (...args: any[]) => void): this;
|
|
362
|
+
removeListener(event: PoolEvent, listener: (...args: any[]) => void): this;
|
|
363
|
+
|
|
331
364
|
// Promise / async-await API
|
|
332
365
|
getAsync(): Promise<Database>;
|
|
333
366
|
destroyAsync(): Promise<void>;
|