@rotorsoft/act-pg 1.13.1 → 1.13.2
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/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +64 -5
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +136 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +136 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -188,6 +188,34 @@ export declare class PostgresStore implements Store {
|
|
|
188
188
|
* connection would re-fire the stale handler.
|
|
189
189
|
*/
|
|
190
190
|
private _listen_handler;
|
|
191
|
+
/**
|
|
192
|
+
* Error listener attached to the active LISTEN client. node-postgres
|
|
193
|
+
* removes its idle-error guard on checkout, so a checked-out client
|
|
194
|
+
* that emits `error` (backend restart, failover, network drop) with no
|
|
195
|
+
* listener is an uncaught exception — a process crash (#1189). Tracked
|
|
196
|
+
* alongside `_listen_handler` so teardown detaches it in lockstep.
|
|
197
|
+
*/
|
|
198
|
+
private _listen_error_handler;
|
|
199
|
+
/**
|
|
200
|
+
* The caller's notification handler for the active subscription, kept
|
|
201
|
+
* so the self-healing reconnect path (#1189) can re-establish LISTEN
|
|
202
|
+
* on a fresh client after the dedicated one emits `error`. Cleared by
|
|
203
|
+
* `_teardown_listen`, which is what makes disposal cancel any pending
|
|
204
|
+
* reconnect.
|
|
205
|
+
*/
|
|
206
|
+
private _notify_handler;
|
|
207
|
+
/**
|
|
208
|
+
* Pending reconnect timer, if a LISTEN client error scheduled one.
|
|
209
|
+
* Tracked so `_teardown_listen` (and therefore `dispose()`) can cancel
|
|
210
|
+
* it — a reconnect must never fire after teardown.
|
|
211
|
+
*/
|
|
212
|
+
private _reconnect_timer;
|
|
213
|
+
/**
|
|
214
|
+
* Consecutive reconnect attempts since the last healthy LISTEN, used to
|
|
215
|
+
* grow the capped exponential backoff. Reset to 0 once a re-LISTEN
|
|
216
|
+
* succeeds.
|
|
217
|
+
*/
|
|
218
|
+
private _reconnect_attempts;
|
|
191
219
|
/**
|
|
192
220
|
* Cross-process commit subscription. **Present only when
|
|
193
221
|
* `config.notify === true`** — the orchestrator's auto-wire path
|
|
@@ -228,11 +256,17 @@ export declare class PostgresStore implements Store {
|
|
|
228
256
|
*/
|
|
229
257
|
dispose(): Promise<void>;
|
|
230
258
|
/**
|
|
231
|
-
* Tear down the active LISTEN subscription if any:
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
259
|
+
* Tear down the active LISTEN subscription if any: cancel any pending
|
|
260
|
+
* reconnect, forget the caller's handler (so no reconnect can fire
|
|
261
|
+
* after teardown), detach the notification + error listeners, run
|
|
262
|
+
* UNLISTEN, and destroy the dedicated client (do not return it to the
|
|
263
|
+
* pool — its listeners are removed but destroying belt-and-braces
|
|
264
|
+
* guards against any future change in pg-pool semantics that could
|
|
265
|
+
* re-issue a half-clean client).
|
|
266
|
+
*
|
|
267
|
+
* Clearing `_notify_handler` and the reconnect timer here is what makes
|
|
268
|
+
* `dispose()` safe during a pending reconnect (#1189): a scheduled
|
|
269
|
+
* `_reconnect` bails the moment it finds no handler.
|
|
236
270
|
*/
|
|
237
271
|
private _teardown_listen;
|
|
238
272
|
/**
|
|
@@ -437,10 +471,35 @@ export declare class PostgresStore implements Store {
|
|
|
437
471
|
* client; pool disposal also tears the subscription down as a safety
|
|
438
472
|
* net.
|
|
439
473
|
*
|
|
474
|
+
* The subscription is **self-healing** (#1189): the dedicated client
|
|
475
|
+
* has an `error` listener that, on a connection blip (backend restart,
|
|
476
|
+
* failover, network drop), tears the dead client down and re-LISTENs
|
|
477
|
+
* on a fresh one with capped exponential backoff — degrading to the
|
|
478
|
+
* poll path in between. A pending reconnect is cancelled by disposal,
|
|
479
|
+
* so no reconnect fires after teardown.
|
|
480
|
+
*
|
|
440
481
|
* @param handler Called for each cross-process commit notification.
|
|
441
482
|
* @returns Disposer that releases the LISTEN client.
|
|
442
483
|
*/
|
|
443
484
|
private _subscribe_notifications;
|
|
485
|
+
/**
|
|
486
|
+
* Check out a dedicated client, attach the notification + error
|
|
487
|
+
* listeners, and run `LISTEN`. Shared by the initial subscription and
|
|
488
|
+
* every reconnect (#1189). On any failure before `LISTEN` succeeds the
|
|
489
|
+
* client is detached and destroyed so nothing leaks — the caller
|
|
490
|
+
* decides whether to propagate (initial subscribe) or reschedule
|
|
491
|
+
* (reconnect).
|
|
492
|
+
*/
|
|
493
|
+
private _open_listen;
|
|
494
|
+
/**
|
|
495
|
+
* Self-heal the LISTEN subscription after the dedicated client emitted
|
|
496
|
+
* `error` (#1189). Detaches and destroys the dead client, then
|
|
497
|
+
* reconnects on a fresh one with capped exponential backoff. Bails
|
|
498
|
+
* immediately if the subscription was disposed while a reconnect was
|
|
499
|
+
* pending (`_notify_handler` cleared by `_teardown_listen`), so no
|
|
500
|
+
* reconnect ever fires after teardown.
|
|
501
|
+
*/
|
|
502
|
+
private _reconnect;
|
|
444
503
|
/**
|
|
445
504
|
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
446
505
|
* Windowed targets (`before` set) prune the prefix below the closest safe
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,SAAS,EACT,KAAK,EAEL,OAAO,EACP,cAAc,EACd,KAAK,EACL,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,MAAM,EACN,OAAO,EACP,KAAK,EACL,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAQxB,OAAO,EAEL,KAAK,UAAU,EAGhB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;AAUpB,KAAK,MAAM,GAAG,QAAQ,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,cAAc,CAAC,EAAE,UAAU,CAAC;CAC7B,CAAC,GACA,EAAE,CAAC,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -60,6 +60,8 @@ var SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
|
60
60
|
var PG_UNIQUE_VIOLATION = "23505";
|
|
61
61
|
var NOTIFY_CHANNEL_PREFIX = "act_commit";
|
|
62
62
|
var NOTIFY_MAX_PAYLOAD_BYTES = 8e3;
|
|
63
|
+
var NOTIFY_RECONNECT_BASE_MS = 250;
|
|
64
|
+
var NOTIFY_RECONNECT_MAX_MS = 3e4;
|
|
63
65
|
function notify_channel(schema, table) {
|
|
64
66
|
return `${NOTIFY_CHANNEL_PREFIX}_${schema}_${table}`;
|
|
65
67
|
}
|
|
@@ -132,6 +134,34 @@ var PostgresStore = class {
|
|
|
132
134
|
* connection would re-fire the stale handler.
|
|
133
135
|
*/
|
|
134
136
|
_listen_handler;
|
|
137
|
+
/**
|
|
138
|
+
* Error listener attached to the active LISTEN client. node-postgres
|
|
139
|
+
* removes its idle-error guard on checkout, so a checked-out client
|
|
140
|
+
* that emits `error` (backend restart, failover, network drop) with no
|
|
141
|
+
* listener is an uncaught exception — a process crash (#1189). Tracked
|
|
142
|
+
* alongside `_listen_handler` so teardown detaches it in lockstep.
|
|
143
|
+
*/
|
|
144
|
+
_listen_error_handler;
|
|
145
|
+
/**
|
|
146
|
+
* The caller's notification handler for the active subscription, kept
|
|
147
|
+
* so the self-healing reconnect path (#1189) can re-establish LISTEN
|
|
148
|
+
* on a fresh client after the dedicated one emits `error`. Cleared by
|
|
149
|
+
* `_teardown_listen`, which is what makes disposal cancel any pending
|
|
150
|
+
* reconnect.
|
|
151
|
+
*/
|
|
152
|
+
_notify_handler;
|
|
153
|
+
/**
|
|
154
|
+
* Pending reconnect timer, if a LISTEN client error scheduled one.
|
|
155
|
+
* Tracked so `_teardown_listen` (and therefore `dispose()`) can cancel
|
|
156
|
+
* it — a reconnect must never fire after teardown.
|
|
157
|
+
*/
|
|
158
|
+
_reconnect_timer;
|
|
159
|
+
/**
|
|
160
|
+
* Consecutive reconnect attempts since the last healthy LISTEN, used to
|
|
161
|
+
* grow the capped exponential backoff. Reset to 0 once a re-LISTEN
|
|
162
|
+
* succeeds.
|
|
163
|
+
*/
|
|
164
|
+
_reconnect_attempts = 0;
|
|
135
165
|
/**
|
|
136
166
|
* Cross-process commit subscription. **Present only when
|
|
137
167
|
* `config.notify === true`** — the orchestrator's auto-wire path
|
|
@@ -199,16 +229,30 @@ var PostgresStore = class {
|
|
|
199
229
|
await this._pool.end();
|
|
200
230
|
}
|
|
201
231
|
/**
|
|
202
|
-
* Tear down the active LISTEN subscription if any:
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
232
|
+
* Tear down the active LISTEN subscription if any: cancel any pending
|
|
233
|
+
* reconnect, forget the caller's handler (so no reconnect can fire
|
|
234
|
+
* after teardown), detach the notification + error listeners, run
|
|
235
|
+
* UNLISTEN, and destroy the dedicated client (do not return it to the
|
|
236
|
+
* pool — its listeners are removed but destroying belt-and-braces
|
|
237
|
+
* guards against any future change in pg-pool semantics that could
|
|
238
|
+
* re-issue a half-clean client).
|
|
239
|
+
*
|
|
240
|
+
* Clearing `_notify_handler` and the reconnect timer here is what makes
|
|
241
|
+
* `dispose()` safe during a pending reconnect (#1189): a scheduled
|
|
242
|
+
* `_reconnect` bails the moment it finds no handler.
|
|
207
243
|
*/
|
|
208
244
|
async _teardown_listen() {
|
|
245
|
+
if (this._reconnect_timer) {
|
|
246
|
+
clearTimeout(this._reconnect_timer);
|
|
247
|
+
this._reconnect_timer = void 0;
|
|
248
|
+
}
|
|
249
|
+
this._notify_handler = void 0;
|
|
250
|
+
this._reconnect_attempts = 0;
|
|
209
251
|
if (!this._listen_client) return;
|
|
210
252
|
this._listen_client.removeListener("notification", this._listen_handler);
|
|
253
|
+
this._listen_client.removeListener("error", this._listen_error_handler);
|
|
211
254
|
this._listen_handler = void 0;
|
|
255
|
+
this._listen_error_handler = void 0;
|
|
212
256
|
try {
|
|
213
257
|
await this._listen_client.query(`UNLISTEN ${this._channel}`);
|
|
214
258
|
} catch {
|
|
@@ -272,7 +316,7 @@ var PostgresStore = class {
|
|
|
272
316
|
stream varchar(100) COLLATE pg_catalog."default" PRIMARY KEY,
|
|
273
317
|
source varchar(100) COLLATE pg_catalog."default",
|
|
274
318
|
at int NOT NULL DEFAULT -1,
|
|
275
|
-
retry
|
|
319
|
+
retry int NOT NULL DEFAULT -1,
|
|
276
320
|
blocked boolean NOT NULL DEFAULT false,
|
|
277
321
|
error text,
|
|
278
322
|
leased_by text,
|
|
@@ -294,6 +338,21 @@ var PostgresStore = class {
|
|
|
294
338
|
`ALTER TABLE ${this._fqs}
|
|
295
339
|
ADD COLUMN IF NOT EXISTS deferred_at timestamptz;`
|
|
296
340
|
);
|
|
341
|
+
await client.query(
|
|
342
|
+
`DO $$
|
|
343
|
+
BEGIN
|
|
344
|
+
IF EXISTS (
|
|
345
|
+
SELECT 1 FROM information_schema.columns
|
|
346
|
+
WHERE table_schema = '${this.config.schema}'
|
|
347
|
+
AND table_name = '${this.config.table}_streams'
|
|
348
|
+
AND column_name = 'retry'
|
|
349
|
+
AND data_type = 'smallint'
|
|
350
|
+
) THEN
|
|
351
|
+
EXECUTE 'ALTER TABLE ${this._fqs} ALTER COLUMN retry TYPE integer';
|
|
352
|
+
END IF;
|
|
353
|
+
END
|
|
354
|
+
$$;`
|
|
355
|
+
);
|
|
297
356
|
await client.query(
|
|
298
357
|
`DROP INDEX IF EXISTS "${this.config.schema}"."${this.config.table}_streams_fetch_ix"`
|
|
299
358
|
);
|
|
@@ -1180,11 +1239,39 @@ var PostgresStore = class {
|
|
|
1180
1239
|
* client; pool disposal also tears the subscription down as a safety
|
|
1181
1240
|
* net.
|
|
1182
1241
|
*
|
|
1242
|
+
* The subscription is **self-healing** (#1189): the dedicated client
|
|
1243
|
+
* has an `error` listener that, on a connection blip (backend restart,
|
|
1244
|
+
* failover, network drop), tears the dead client down and re-LISTENs
|
|
1245
|
+
* on a fresh one with capped exponential backoff — degrading to the
|
|
1246
|
+
* poll path in between. A pending reconnect is cancelled by disposal,
|
|
1247
|
+
* so no reconnect fires after teardown.
|
|
1248
|
+
*
|
|
1183
1249
|
* @param handler Called for each cross-process commit notification.
|
|
1184
1250
|
* @returns Disposer that releases the LISTEN client.
|
|
1185
1251
|
*/
|
|
1186
1252
|
async _subscribe_notifications(handler) {
|
|
1187
1253
|
await this._teardown_listen();
|
|
1254
|
+
this._notify_handler = handler;
|
|
1255
|
+
try {
|
|
1256
|
+
await this._open_listen(handler);
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
this._notify_handler = void 0;
|
|
1259
|
+
throw err;
|
|
1260
|
+
}
|
|
1261
|
+
return async () => {
|
|
1262
|
+
if (this._notify_handler !== handler) return;
|
|
1263
|
+
await this._teardown_listen();
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Check out a dedicated client, attach the notification + error
|
|
1268
|
+
* listeners, and run `LISTEN`. Shared by the initial subscription and
|
|
1269
|
+
* every reconnect (#1189). On any failure before `LISTEN` succeeds the
|
|
1270
|
+
* client is detached and destroyed so nothing leaks — the caller
|
|
1271
|
+
* decides whether to propagate (initial subscribe) or reschedule
|
|
1272
|
+
* (reconnect).
|
|
1273
|
+
*/
|
|
1274
|
+
async _open_listen(handler) {
|
|
1188
1275
|
const client = await this._client("notify");
|
|
1189
1276
|
const on_notification = (msg) => {
|
|
1190
1277
|
if (msg.channel !== this._channel) return;
|
|
@@ -1223,20 +1310,59 @@ var PostgresStore = class {
|
|
|
1223
1310
|
logger.error(err, "act_commit: handler threw, listener preserved");
|
|
1224
1311
|
}
|
|
1225
1312
|
};
|
|
1313
|
+
const on_error = (err) => {
|
|
1314
|
+
logger.error(err, "act_commit: LISTEN client errored, reconnecting");
|
|
1315
|
+
this._reconnect();
|
|
1316
|
+
};
|
|
1226
1317
|
client.on("notification", on_notification);
|
|
1318
|
+
client.on("error", on_error);
|
|
1227
1319
|
try {
|
|
1228
1320
|
await client.query(`LISTEN ${this._channel}`);
|
|
1229
1321
|
} catch (err) {
|
|
1230
1322
|
client.removeListener("notification", on_notification);
|
|
1323
|
+
client.removeListener("error", on_error);
|
|
1231
1324
|
client.release(true);
|
|
1232
1325
|
throw err;
|
|
1233
1326
|
}
|
|
1234
1327
|
this._listen_client = client;
|
|
1235
1328
|
this._listen_handler = on_notification;
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1329
|
+
this._listen_error_handler = on_error;
|
|
1330
|
+
this._reconnect_attempts = 0;
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Self-heal the LISTEN subscription after the dedicated client emitted
|
|
1334
|
+
* `error` (#1189). Detaches and destroys the dead client, then
|
|
1335
|
+
* reconnects on a fresh one with capped exponential backoff. Bails
|
|
1336
|
+
* immediately if the subscription was disposed while a reconnect was
|
|
1337
|
+
* pending (`_notify_handler` cleared by `_teardown_listen`), so no
|
|
1338
|
+
* reconnect ever fires after teardown.
|
|
1339
|
+
*/
|
|
1340
|
+
_reconnect() {
|
|
1341
|
+
const handler = this._notify_handler;
|
|
1342
|
+
if (!handler) return;
|
|
1343
|
+
if (this._listen_client) {
|
|
1344
|
+
this._listen_client.removeListener("notification", this._listen_handler);
|
|
1345
|
+
this._listen_client.removeListener("error", this._listen_error_handler);
|
|
1346
|
+
this._listen_handler = void 0;
|
|
1347
|
+
this._listen_error_handler = void 0;
|
|
1348
|
+
this._listen_client.release(true);
|
|
1349
|
+
this._listen_client = void 0;
|
|
1350
|
+
}
|
|
1351
|
+
const delay = Math.min(
|
|
1352
|
+
NOTIFY_RECONNECT_MAX_MS,
|
|
1353
|
+
NOTIFY_RECONNECT_BASE_MS * 2 ** this._reconnect_attempts
|
|
1354
|
+
);
|
|
1355
|
+
this._reconnect_attempts++;
|
|
1356
|
+
this._reconnect_timer = setTimeout(() => {
|
|
1357
|
+
this._reconnect_timer = void 0;
|
|
1358
|
+
const current = this._notify_handler;
|
|
1359
|
+
if (!current) return;
|
|
1360
|
+
this._open_listen(current).catch((err) => {
|
|
1361
|
+
logger.error(err, "act_commit: LISTEN reconnect failed, retrying");
|
|
1362
|
+
this._reconnect();
|
|
1363
|
+
});
|
|
1364
|
+
}, delay);
|
|
1365
|
+
this._reconnect_timer.unref?.();
|
|
1240
1366
|
}
|
|
1241
1367
|
/**
|
|
1242
1368
|
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|