node-firebird 2.7.0 → 2.8.1
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/srp.js +17 -4
- package/lib/types.d.ts +28 -0
- package/package.json +1 -1
- package/src/pool.ts +126 -6
- package/src/srp.ts +18 -5
- 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/srp.js
CHANGED
|
@@ -117,12 +117,16 @@ function clientProof(user, password, salt, A, B, a, hashAlgo = 'sha1') {
|
|
|
117
117
|
dump('n2', n2);
|
|
118
118
|
n1 = modPow(n1, n2, PRIME.N);
|
|
119
119
|
n2 = toBigInt(getHash('sha1', user));
|
|
120
|
-
|
|
120
|
+
// K is hashed as the raw fixed-length digest, exactly like the server's
|
|
121
|
+
// digest.process(sessionKey) over a 20-byte UCharBuffer. Converting it
|
|
122
|
+
// through bigint dropped a leading zero byte (~0.4% of connections) and
|
|
123
|
+
// broke the proof (issue #421).
|
|
124
|
+
var M = toBigInt(getHash(hashAlgo, toBuffer(n1), toBuffer(n2), salt, toBuffer(A), toBuffer(B), K));
|
|
121
125
|
dump('n1-2', n1);
|
|
122
126
|
dump('n2-2', n2);
|
|
123
127
|
dump('proof:M', M);
|
|
124
128
|
return {
|
|
125
|
-
clientSessionKey: K,
|
|
129
|
+
clientSessionKey: toBigInt(K),
|
|
126
130
|
authData: M,
|
|
127
131
|
};
|
|
128
132
|
}
|
|
@@ -162,12 +166,19 @@ function pad(n) {
|
|
|
162
166
|
/**
|
|
163
167
|
* Scramble keys.
|
|
164
168
|
*
|
|
169
|
+
* The server hashes the minimal (stripped) magnitude bytes of A and B
|
|
170
|
+
* (RemotePassword::computeScramble → processStrippedInt in Firebird's
|
|
171
|
+
* srp.cpp, identical in 3.0 through master) — NOT the 128-byte padded
|
|
172
|
+
* form, which the engine only uses for k = H(N, pad(g)). Padding here
|
|
173
|
+
* made u diverge whenever A or B had a leading zero byte (~0.8% of
|
|
174
|
+
* connections), failing the proof (issue #421).
|
|
175
|
+
*
|
|
165
176
|
* @param A BigInt Client public key.
|
|
166
177
|
* @param B BigInt Server public key.
|
|
167
178
|
* @returns {BigInt}
|
|
168
179
|
*/
|
|
169
180
|
function getScramble(A, B, hashAlgo = 'sha1') {
|
|
170
|
-
return BigInt('0x' + getHash(hashAlgo,
|
|
181
|
+
return BigInt('0x' + getHash(hashAlgo, toBuffer(A), toBuffer(B)));
|
|
171
182
|
}
|
|
172
183
|
/**
|
|
173
184
|
* Client session secret.
|
|
@@ -183,6 +194,8 @@ function getScramble(A, B, hashAlgo = 'sha1') {
|
|
|
183
194
|
* @param A BigInt Client public key.
|
|
184
195
|
* @param B BigInt Server public key.
|
|
185
196
|
* @param a BigInt Client private key.
|
|
197
|
+
* @returns Buffer The raw session-key digest (fixed length, may start
|
|
198
|
+
* with a zero byte — significant for the proof).
|
|
186
199
|
*/
|
|
187
200
|
function clientSession(user, password, salt, A, B, a, hashAlgo = 'sha1') {
|
|
188
201
|
var u = getScramble(A, B, 'sha1');
|
|
@@ -200,7 +213,7 @@ function clientSession(user, password, salt, A, B, a, hashAlgo = 'sha1') {
|
|
|
200
213
|
var ux = (u * x) % PRIME.N;
|
|
201
214
|
var aux = (a + ux) % PRIME.N;
|
|
202
215
|
var sessionSecret = modPow(diff, aux, PRIME.N);
|
|
203
|
-
var K =
|
|
216
|
+
var K = Buffer.from(getHash('sha1', toBuffer(sessionSecret)), 'hex');
|
|
204
217
|
dump('B', B);
|
|
205
218
|
dump('u', u);
|
|
206
219
|
dump('x', x);
|
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/srp.ts
CHANGED
|
@@ -133,14 +133,18 @@ export function clientProof(user: string, password: string, salt: Buffer, A: big
|
|
|
133
133
|
|
|
134
134
|
n1 = modPow(n1, n2, PRIME.N);
|
|
135
135
|
n2 = toBigInt(getHash('sha1', user));
|
|
136
|
-
|
|
136
|
+
// K is hashed as the raw fixed-length digest, exactly like the server's
|
|
137
|
+
// digest.process(sessionKey) over a 20-byte UCharBuffer. Converting it
|
|
138
|
+
// through bigint dropped a leading zero byte (~0.4% of connections) and
|
|
139
|
+
// broke the proof (issue #421).
|
|
140
|
+
var M = toBigInt(getHash(hashAlgo, toBuffer(n1), toBuffer(n2), salt, toBuffer(A), toBuffer(B), K));
|
|
137
141
|
|
|
138
142
|
dump('n1-2', n1);
|
|
139
143
|
dump('n2-2', n2);
|
|
140
144
|
dump('proof:M', M);
|
|
141
145
|
|
|
142
146
|
return {
|
|
143
|
-
clientSessionKey: K,
|
|
147
|
+
clientSessionKey: toBigInt(K),
|
|
144
148
|
authData: M,
|
|
145
149
|
};
|
|
146
150
|
}
|
|
@@ -186,12 +190,19 @@ function pad(n: bigint): Buffer {
|
|
|
186
190
|
/**
|
|
187
191
|
* Scramble keys.
|
|
188
192
|
*
|
|
193
|
+
* The server hashes the minimal (stripped) magnitude bytes of A and B
|
|
194
|
+
* (RemotePassword::computeScramble → processStrippedInt in Firebird's
|
|
195
|
+
* srp.cpp, identical in 3.0 through master) — NOT the 128-byte padded
|
|
196
|
+
* form, which the engine only uses for k = H(N, pad(g)). Padding here
|
|
197
|
+
* made u diverge whenever A or B had a leading zero byte (~0.8% of
|
|
198
|
+
* connections), failing the proof (issue #421).
|
|
199
|
+
*
|
|
189
200
|
* @param A BigInt Client public key.
|
|
190
201
|
* @param B BigInt Server public key.
|
|
191
202
|
* @returns {BigInt}
|
|
192
203
|
*/
|
|
193
204
|
function getScramble(A: bigint, B: bigint, hashAlgo: string = 'sha1'): bigint {
|
|
194
|
-
return BigInt('0x' + getHash(hashAlgo,
|
|
205
|
+
return BigInt('0x' + getHash(hashAlgo, toBuffer(A), toBuffer(B)));
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
/**
|
|
@@ -208,8 +219,10 @@ function getScramble(A: bigint, B: bigint, hashAlgo: string = 'sha1'): bigint {
|
|
|
208
219
|
* @param A BigInt Client public key.
|
|
209
220
|
* @param B BigInt Server public key.
|
|
210
221
|
* @param a BigInt Client private key.
|
|
222
|
+
* @returns Buffer The raw session-key digest (fixed length, may start
|
|
223
|
+
* with a zero byte — significant for the proof).
|
|
211
224
|
*/
|
|
212
|
-
function clientSession(user: string, password: string, salt: Buffer, A: bigint, B: bigint, a: bigint, hashAlgo: string = 'sha1'):
|
|
225
|
+
function clientSession(user: string, password: string, salt: Buffer, A: bigint, B: bigint, a: bigint, hashAlgo: string = 'sha1'): Buffer {
|
|
213
226
|
var u = getScramble(A, B, 'sha1');
|
|
214
227
|
var x = getUserHash(user, salt, password, 'sha1');
|
|
215
228
|
var gx = modPow(PRIME.g, x, PRIME.N);
|
|
@@ -227,7 +240,7 @@ function clientSession(user: string, password: string, salt: Buffer, A: bigint,
|
|
|
227
240
|
var ux = (u * x) % PRIME.N;
|
|
228
241
|
var aux = (a + ux) % PRIME.N;
|
|
229
242
|
var sessionSecret = modPow(diff, aux, PRIME.N);
|
|
230
|
-
var K =
|
|
243
|
+
var K = Buffer.from(getHash('sha1', toBuffer(sessionSecret)), 'hex');
|
|
231
244
|
|
|
232
245
|
dump('B', B);
|
|
233
246
|
dump('u', u);
|
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>;
|