@steve31415/baselib 3.49.0 → 3.50.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/dist/db.d.ts +19 -4
- package/dist/db.js +52 -15
- package/package.json +1 -1
package/dist/db.d.ts
CHANGED
|
@@ -115,8 +115,16 @@ export declare const IDLE_PROBE_TIMEOUT_MS = 3000;
|
|
|
115
115
|
* - `connect`: a new connection failed to establish (handshake dropped,
|
|
116
116
|
* credential rejected, `connectionTimeoutMillis` hit mid-handshake).
|
|
117
117
|
* No statement ran, so the operation is safe to retry.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
118
|
+
* Also a checkout that timed out while the pool was still opening a
|
|
119
|
+
* connection (`pool.connecting` > 0, message "a new connection did not
|
|
120
|
+
* finish within the checkout timeout"): pg-pool queues a checkout that
|
|
121
|
+
* finds an idle client, and if another checkout takes that client
|
|
122
|
+
* first it hands the queued one a new connection, so a stalled
|
|
123
|
+
* handshake surfaces with pg-pool's starvation error (coder
|
|
124
|
+
* 2026-11-02: total 4 of 6, 3 idle, a 15 s Cloud SQL handshake).
|
|
125
|
+
* - `wait`: the checkout timed out waiting for a free client with no
|
|
126
|
+
* connection being opened. Pool starvation, not loss: the pool's
|
|
127
|
+
* `max` or a client held too long.
|
|
120
128
|
* - `idle`: a client idle in the pool died; pg-pool discards it and the
|
|
121
129
|
* next checkout opens a fresh one. Harmless by construction. With
|
|
122
130
|
* `meta.detectedAt: 'checkout'` the death went unnoticed until a
|
|
@@ -127,7 +135,8 @@ export declare const IDLE_PROBE_TIMEOUT_MS = 3000;
|
|
|
127
135
|
* any transaction or advisory lock it held is gone.
|
|
128
136
|
* Each event carries `elapsedMs` (time in that phase: the connect
|
|
129
137
|
* attempt, the wait, the idle spell, the checkout), the pg `code`, the
|
|
130
|
-
* pool's counts
|
|
138
|
+
* pool's counts (`total`, `idle`, `waiting`, `connecting`, `max`) and,
|
|
139
|
+
* given a Cloud SQL credential reader,
|
|
131
140
|
* `credentialRemainingMs`; the logger adds request activity, as on every
|
|
132
141
|
* server event (`activity.ts`: was Cloud Run CPU allocated?). Query:
|
|
133
142
|
* `['apps'] | where kind == 'db-connection-loss' | summarize count() by app, tostring(meta.phase)`.
|
|
@@ -169,7 +178,13 @@ export declare function createPool(opts?: PoolOptions): Promise<pg.Pool>;
|
|
|
169
178
|
* drops the database `with (force)` can kill a backend mid-goodbye, and the
|
|
170
179
|
* FATAL it sends back surfaces as an unhandled pool 'error' (it took down a
|
|
171
180
|
* todo2 deploy with every test green, 2026-08-20). pg-pool emits 'remove' for
|
|
172
|
-
* each client once its socket has closed; wait for all of them.
|
|
181
|
+
* each client once its socket has closed; wait for all of them.
|
|
182
|
+
*
|
|
183
|
+
* Only a client that finished connecting gets a 'remove': one still being
|
|
184
|
+
* opened that then fails leaves the pool silently, so counting it (as
|
|
185
|
+
* `totalCount` does) waited forever for an event that never came (found
|
|
186
|
+
* 2026-11-02 with a stalled handshake). Count the connected clients, and
|
|
187
|
+
* any that finish connecting while the pool ends. */
|
|
173
188
|
export declare function endPool(pool: pg.Pool): Promise<void>;
|
|
174
189
|
export declare function migrate(pool: pg.Pool, migrationsDir: string, logger?: Logger): Promise<{
|
|
175
190
|
applied: string[];
|
package/dist/db.js
CHANGED
|
@@ -112,7 +112,20 @@ const CONNECTION_LOSS_KIND = 'db-connection-loss';
|
|
|
112
112
|
const ACQUIRE_FAILED_MESSAGE = {
|
|
113
113
|
connect: 'database pool: new connection failed',
|
|
114
114
|
wait: 'database pool: checkout timed out waiting for a free client',
|
|
115
|
+
stalled: 'database pool: a new connection did not finish within the checkout timeout',
|
|
115
116
|
};
|
|
117
|
+
/** pg-pool's client list, read for diagnostics only (pg-pool 3.x
|
|
118
|
+
* `_clients`, pg 8.x `_connecting` / `_connected`). Undefined when a
|
|
119
|
+
* version moves them; callers then fall back to the public counts. */
|
|
120
|
+
function poolClients(pool) {
|
|
121
|
+
const clients = pool._clients;
|
|
122
|
+
return Array.isArray(clients) ? clients : undefined;
|
|
123
|
+
}
|
|
124
|
+
/** Clients the pool is still opening: counted in `totalCount`, neither idle
|
|
125
|
+
* nor lent out. */
|
|
126
|
+
function connectingCount(pool) {
|
|
127
|
+
return poolClients(pool)?.filter((c) => c._connecting === true).length;
|
|
128
|
+
}
|
|
116
129
|
const instrumented = new WeakSet();
|
|
117
130
|
// Clients under an idle probe: their death is reported once, by the probe,
|
|
118
131
|
// as an idle loss rather than a checked-out one.
|
|
@@ -127,7 +140,7 @@ function connectionLossWarner(pool, logger, credentialExpiry) {
|
|
|
127
140
|
...extra,
|
|
128
141
|
...(since === undefined ? {} : { elapsedMs: Date.now() - since }),
|
|
129
142
|
code: err.code,
|
|
130
|
-
pool: { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount },
|
|
143
|
+
pool: { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, connecting: connectingCount(pool), max: pool.options.max },
|
|
131
144
|
...(credentialExpiry
|
|
132
145
|
? expiry === undefined
|
|
133
146
|
? { credentialUnknown: true }
|
|
@@ -148,8 +161,16 @@ function connectionLossWarner(pool, logger, credentialExpiry) {
|
|
|
148
161
|
* - `connect`: a new connection failed to establish (handshake dropped,
|
|
149
162
|
* credential rejected, `connectionTimeoutMillis` hit mid-handshake).
|
|
150
163
|
* No statement ran, so the operation is safe to retry.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
164
|
+
* Also a checkout that timed out while the pool was still opening a
|
|
165
|
+
* connection (`pool.connecting` > 0, message "a new connection did not
|
|
166
|
+
* finish within the checkout timeout"): pg-pool queues a checkout that
|
|
167
|
+
* finds an idle client, and if another checkout takes that client
|
|
168
|
+
* first it hands the queued one a new connection, so a stalled
|
|
169
|
+
* handshake surfaces with pg-pool's starvation error (coder
|
|
170
|
+
* 2026-11-02: total 4 of 6, 3 idle, a 15 s Cloud SQL handshake).
|
|
171
|
+
* - `wait`: the checkout timed out waiting for a free client with no
|
|
172
|
+
* connection being opened. Pool starvation, not loss: the pool's
|
|
173
|
+
* `max` or a client held too long.
|
|
153
174
|
* - `idle`: a client idle in the pool died; pg-pool discards it and the
|
|
154
175
|
* next checkout opens a fresh one. Harmless by construction. With
|
|
155
176
|
* `meta.detectedAt: 'checkout'` the death went unnoticed until a
|
|
@@ -160,7 +181,8 @@ function connectionLossWarner(pool, logger, credentialExpiry) {
|
|
|
160
181
|
* any transaction or advisory lock it held is gone.
|
|
161
182
|
* Each event carries `elapsedMs` (time in that phase: the connect
|
|
162
183
|
* attempt, the wait, the idle spell, the checkout), the pg `code`, the
|
|
163
|
-
* pool's counts
|
|
184
|
+
* pool's counts (`total`, `idle`, `waiting`, `connecting`, `max`) and,
|
|
185
|
+
* given a Cloud SQL credential reader,
|
|
164
186
|
* `credentialRemainingMs`; the logger adds request activity, as on every
|
|
165
187
|
* server event (`activity.ts`: was Cloud Run CPU allocated?). Query:
|
|
166
188
|
* `['apps'] | where kind == 'db-connection-loss' | summarize count() by app, tostring(meta.phase)`.
|
|
@@ -224,8 +246,11 @@ export function logPoolErrors(pool, logger, options = {}) {
|
|
|
224
246
|
// A checkout after end() is the caller's shutdown bug, not a lost connection.
|
|
225
247
|
if (message.startsWith('Cannot use a pool after calling end'))
|
|
226
248
|
return;
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
if (message !== 'timeout exceeded when trying to connect')
|
|
250
|
+
return warn('connect', ACQUIRE_FAILED_MESSAGE.connect, err, started);
|
|
251
|
+
// The pool's own timeout: starvation only if nothing was being opened.
|
|
252
|
+
const opening = (connectingCount(pool) ?? 0) > 0;
|
|
253
|
+
warn(opening ? 'connect' : 'wait', opening ? ACQUIRE_FAILED_MESSAGE.stalled : ACQUIRE_FAILED_MESSAGE.wait, err, started);
|
|
229
254
|
};
|
|
230
255
|
pool.connect = ((cb) => {
|
|
231
256
|
const started = Date.now();
|
|
@@ -353,18 +378,30 @@ export async function createPool(opts = {}) {
|
|
|
353
378
|
* drops the database `with (force)` can kill a backend mid-goodbye, and the
|
|
354
379
|
* FATAL it sends back surfaces as an unhandled pool 'error' (it took down a
|
|
355
380
|
* todo2 deploy with every test green, 2026-08-20). pg-pool emits 'remove' for
|
|
356
|
-
* each client once its socket has closed; wait for all of them.
|
|
381
|
+
* each client once its socket has closed; wait for all of them.
|
|
382
|
+
*
|
|
383
|
+
* Only a client that finished connecting gets a 'remove': one still being
|
|
384
|
+
* opened that then fails leaves the pool silently, so counting it (as
|
|
385
|
+
* `totalCount` does) waited forever for an event that never came (found
|
|
386
|
+
* 2026-11-02 with a stalled handshake). Count the connected clients, and
|
|
387
|
+
* any that finish connecting while the pool ends. */
|
|
357
388
|
export async function endPool(pool) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
389
|
+
const clients = poolClients(pool);
|
|
390
|
+
let open = clients ? clients.filter((c) => c._connected === true).length : pool.totalCount;
|
|
391
|
+
let resolveAll;
|
|
392
|
+
const allClosed = new Promise((resolve) => (resolveAll = resolve));
|
|
393
|
+
const settle = () => {
|
|
394
|
+
if (open <= 0)
|
|
395
|
+
resolveAll();
|
|
396
|
+
};
|
|
397
|
+
pool.on('connect', () => open++);
|
|
398
|
+
pool.on('remove', () => {
|
|
399
|
+
open--;
|
|
400
|
+
settle();
|
|
366
401
|
});
|
|
367
402
|
await pool.end();
|
|
403
|
+
// No client can start connecting once end() has resolved.
|
|
404
|
+
settle();
|
|
368
405
|
await allClosed;
|
|
369
406
|
}
|
|
370
407
|
// Plain-SQL migrations: numbered files (001-*.sql, 002-*.sql, …) applied in
|
package/package.json
CHANGED