bullmq 6.3.0 → 6.3.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/cjs/classes/redis-queue-backend.js +8 -0
- package/dist/cjs/classes/worker.js +14 -6
- package/dist/cjs/postgres/postgres-connection.js +132 -5
- package/dist/cjs/postgres/postgres-queue-backend.js +34 -5
- package/dist/cjs/tsconfig-cjs.tsbuildinfo +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/classes/redis-queue-backend.d.ts +6 -0
- package/dist/esm/classes/redis-queue-backend.js +8 -0
- package/dist/esm/classes/worker.d.ts +1 -0
- package/dist/esm/classes/worker.js +14 -6
- package/dist/esm/interfaces/queue-backend.d.ts +12 -0
- package/dist/esm/postgres/pg-types.d.ts +11 -0
- package/dist/esm/postgres/postgres-connection.d.ts +50 -0
- package/dist/esm/postgres/postgres-connection.js +132 -5
- package/dist/esm/postgres/postgres-queue-backend.d.ts +15 -0
- package/dist/esm/postgres/postgres-queue-backend.js +34 -5
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +3 -3
|
@@ -88,6 +88,22 @@ export declare class PostgresConnection extends EventEmitter {
|
|
|
88
88
|
* (owned pool); `false` when it is a pooled client we must `release()`.
|
|
89
89
|
*/
|
|
90
90
|
private listenClientIsStandalone;
|
|
91
|
+
/**
|
|
92
|
+
* Whether TCP keepalive is known to be enabled on the dedicated `LISTEN`
|
|
93
|
+
* connection. Only then can a silently dropped connection be detected (as a
|
|
94
|
+
* socket `'error'`) and rebuilt, which is the premise of the backend's large
|
|
95
|
+
* {@link PostgresQueueBackend.maximumBlockTimeout}; when it is not, the
|
|
96
|
+
* backend falls back to a conservative block ceiling instead.
|
|
97
|
+
*/
|
|
98
|
+
private listenClientKeepAlive;
|
|
99
|
+
/**
|
|
100
|
+
* The last `application_name` applied to the `LISTEN` client (via
|
|
101
|
+
* {@link setListenClientName}). Re-applied whenever that client is rebuilt: a
|
|
102
|
+
* fresh connection starts unnamed, which would otherwise silently drop the
|
|
103
|
+
* worker / QueueEvents from `getWorkers()` / `getQueueEvents()` for the rest
|
|
104
|
+
* of its lifetime.
|
|
105
|
+
*/
|
|
106
|
+
private listenClientName;
|
|
91
107
|
constructor(connection: PostgresConnectionOptions);
|
|
92
108
|
/**
|
|
93
109
|
* Forwards an underlying pool / LISTEN-client error as this connection's
|
|
@@ -117,6 +133,40 @@ export declare class PostgresConnection extends EventEmitter {
|
|
|
117
133
|
* out of it (so such pools should be sized `>= 2`).
|
|
118
134
|
*/
|
|
119
135
|
getListenClient(): Promise<PgListenClient>;
|
|
136
|
+
/**
|
|
137
|
+
* Whether TCP keepalive is enabled on the dedicated `LISTEN` connection, so a
|
|
138
|
+
* silently dropped connection is detected (and the client rebuilt) instead of
|
|
139
|
+
* lingering unnoticed. Consulted by the backend when deciding how long a
|
|
140
|
+
* worker may block.
|
|
141
|
+
*/
|
|
142
|
+
get hasListenClientKeepAlive(): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Sets the `application_name` of the dedicated `LISTEN` connection — the
|
|
145
|
+
* PostgreSQL analogue of Redis `CLIENT SETNAME` — and remembers it so it is
|
|
146
|
+
* re-applied to any client rebuilt after a drop.
|
|
147
|
+
*/
|
|
148
|
+
setListenClientName(name: string): Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Re-applies the remembered {@link listenClientName} to a freshly established
|
|
151
|
+
* `LISTEN` client. Best-effort: discovery must never break the client.
|
|
152
|
+
*/
|
|
153
|
+
private applyListenClientName;
|
|
154
|
+
/**
|
|
155
|
+
* Handles a fatal error on the dedicated `LISTEN` client.
|
|
156
|
+
*
|
|
157
|
+
* A `LISTEN` subscription lives on one specific physical connection: once that
|
|
158
|
+
* connection drops, the memoized client is dead and every re-`LISTEN` issued
|
|
159
|
+
* on it is silently lost — so a blocked worker would stop receiving NOTIFYs
|
|
160
|
+
* until its next poll. Invalidate the memo (and tear the dead client down) so
|
|
161
|
+
* the next {@link getListenClient} establishes a fresh connection, and emit
|
|
162
|
+
* `'listenerinvalidated'` so subscribed backends re-`LISTEN` and wake any
|
|
163
|
+
* in-flight blocking wait (which is parked on the now-dead client).
|
|
164
|
+
*
|
|
165
|
+
* Guarded so a stale error from an already-replaced client — or one racing an
|
|
166
|
+
* in-progress {@link close} (which owns teardown) — does not disturb the
|
|
167
|
+
* current client; the error is still forwarded either way.
|
|
168
|
+
*/
|
|
169
|
+
private handleListenClientError;
|
|
120
170
|
/**
|
|
121
171
|
* Truthy once {@link PostgresConnection.close} has begun.
|
|
122
172
|
*/
|
|
@@ -24,6 +24,34 @@ function loadPgModule() {
|
|
|
24
24
|
'already-constructed `pg.Pool` instance as the connection instead of a ' +
|
|
25
25
|
'config object or connection string.');
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Idle time (ms) before the first TCP keepalive probe is sent on the dedicated
|
|
29
|
+
* `LISTEN` connection. Short enough that a silently dropped connection is
|
|
30
|
+
* detected in seconds instead of the OS default (typically two hours) — which,
|
|
31
|
+
* with a large `maximumBlockTimeout`, is what a blocked worker would otherwise
|
|
32
|
+
* have to wait for.
|
|
33
|
+
*/
|
|
34
|
+
const LISTEN_KEEPALIVE_INITIAL_DELAY_MS = 10000;
|
|
35
|
+
/**
|
|
36
|
+
* Best-effort: turns TCP keepalive on for an already-established client's
|
|
37
|
+
* socket. Needed for the pooled `LISTEN` client, since a user-supplied
|
|
38
|
+
* `pg.Pool` may have been created without `keepAlive` and a checked-out client
|
|
39
|
+
* inherits that configuration. Returns whether keepalive is now enabled.
|
|
40
|
+
*/
|
|
41
|
+
function enableSocketKeepAlive(client) {
|
|
42
|
+
var _a;
|
|
43
|
+
const stream = (_a = client.connection) === null || _a === void 0 ? void 0 : _a.stream;
|
|
44
|
+
if (!stream || typeof stream.setKeepAlive !== 'function') {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
stream.setKeepAlive(true, LISTEN_KEEPALIVE_INITIAL_DELAY_MS);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (_b) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
27
55
|
/**
|
|
28
56
|
* Owns the PostgreSQL connection resources for a single backend:
|
|
29
57
|
*
|
|
@@ -37,6 +65,7 @@ function loadPgModule() {
|
|
|
37
65
|
*/
|
|
38
66
|
export class PostgresConnection extends EventEmitter {
|
|
39
67
|
constructor(connection) {
|
|
68
|
+
var _a;
|
|
40
69
|
super();
|
|
41
70
|
/**
|
|
42
71
|
* `true` when {@link listenClient} is a standalone `pg.Client` we must `end()`
|
|
@@ -51,10 +80,13 @@ export class PostgresConnection extends EventEmitter {
|
|
|
51
80
|
this.migrateOnConnect = false;
|
|
52
81
|
this.pgModule = undefined;
|
|
53
82
|
this.listenClientConfig = undefined;
|
|
83
|
+
// A user-supplied pool decides its own `keepAlive`; when it is off we try
|
|
84
|
+
// to enable it on the checked-out LISTEN socket (see getListenClient).
|
|
85
|
+
this.listenClientKeepAlive = Boolean((_a = connection.options) === null || _a === void 0 ? void 0 : _a.keepAlive);
|
|
54
86
|
}
|
|
55
87
|
else {
|
|
56
88
|
const pg = loadPgModule();
|
|
57
|
-
const
|
|
89
|
+
const _b = typeof connection === 'string'
|
|
58
90
|
? {
|
|
59
91
|
schema: undefined,
|
|
60
92
|
skipVersionCheck: undefined,
|
|
@@ -62,7 +94,7 @@ export class PostgresConnection extends EventEmitter {
|
|
|
62
94
|
skipMigrations: undefined,
|
|
63
95
|
connectionString: connection,
|
|
64
96
|
}
|
|
65
|
-
: connection, { schema, skipVersionCheck, migrate, skipMigrations } =
|
|
97
|
+
: connection, { schema, skipVersionCheck, migrate, skipMigrations } = _b, poolConfig = __rest(_b, ["schema", "skipVersionCheck", "migrate", "skipMigrations"]);
|
|
66
98
|
if (migrate !== undefined && skipMigrations !== undefined) {
|
|
67
99
|
throw new Error('BullMQ: `migrate` and `skipMigrations` are mutually exclusive. Set only one.');
|
|
68
100
|
}
|
|
@@ -85,6 +117,8 @@ export class PostgresConnection extends EventEmitter {
|
|
|
85
117
|
// Keep the means to build a dedicated LISTEN connection on demand.
|
|
86
118
|
this.pgModule = pg;
|
|
87
119
|
this.listenClientConfig = resolvedConfig;
|
|
120
|
+
// We build the LISTEN client ourselves, always with keepAlive enabled.
|
|
121
|
+
this.listenClientKeepAlive = true;
|
|
88
122
|
}
|
|
89
123
|
// The pool emits 'error' for idle clients that drop; surface it but never
|
|
90
124
|
// let it crash the process — hence the guarded {@link emitError} (a bare
|
|
@@ -153,24 +187,117 @@ export class PostgresConnection extends EventEmitter {
|
|
|
153
187
|
if (!this.listenClientPromise) {
|
|
154
188
|
this.listenClientPromise = (async () => {
|
|
155
189
|
if (this.pgModule && this.listenClientConfig) {
|
|
156
|
-
const client = new this.pgModule.Client(this.listenClientConfig)
|
|
190
|
+
const client = new this.pgModule.Client(Object.assign(Object.assign({}, this.listenClientConfig), {
|
|
191
|
+
// A LISTEN subscription is bound to one physical connection. Enable
|
|
192
|
+
// TCP keepalive so a silently dropped connection surfaces as a
|
|
193
|
+
// socket `'error'` (and is then rebuilt below) instead of going
|
|
194
|
+
// unnoticed until the next poll — which, with a large
|
|
195
|
+
// `maximumBlockTimeout`, could be up to an hour away.
|
|
196
|
+
keepAlive: true, keepAliveInitialDelayMillis: LISTEN_KEEPALIVE_INITIAL_DELAY_MS }));
|
|
157
197
|
await client.connect();
|
|
158
|
-
client.on('error', err => this.
|
|
198
|
+
client.on('error', err => this.handleListenClientError(client, err));
|
|
199
|
+
this.listenClientKeepAlive = true;
|
|
159
200
|
this.listenClientIsStandalone = true;
|
|
160
201
|
this.listenClient = client;
|
|
202
|
+
await this.applyListenClientName(client);
|
|
161
203
|
return client;
|
|
162
204
|
}
|
|
163
205
|
else {
|
|
164
206
|
const client = await this.pool.connect();
|
|
165
|
-
client.on('error', err => this.
|
|
207
|
+
client.on('error', err => this.handleListenClientError(client, err));
|
|
208
|
+
// The pool is the user's, so its clients may have been created
|
|
209
|
+
// without `keepAlive`; enable it on this socket so a silent drop is
|
|
210
|
+
// still detected. If that is not possible the connection reports no
|
|
211
|
+
// keepalive and the backend caps its block timeout conservatively.
|
|
212
|
+
this.listenClientKeepAlive =
|
|
213
|
+
this.listenClientKeepAlive || enableSocketKeepAlive(client);
|
|
166
214
|
this.listenClientIsStandalone = false;
|
|
167
215
|
this.listenClient = client;
|
|
216
|
+
await this.applyListenClientName(client);
|
|
168
217
|
return client;
|
|
169
218
|
}
|
|
170
219
|
})();
|
|
171
220
|
}
|
|
172
221
|
return this.listenClientPromise;
|
|
173
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Whether TCP keepalive is enabled on the dedicated `LISTEN` connection, so a
|
|
225
|
+
* silently dropped connection is detected (and the client rebuilt) instead of
|
|
226
|
+
* lingering unnoticed. Consulted by the backend when deciding how long a
|
|
227
|
+
* worker may block.
|
|
228
|
+
*/
|
|
229
|
+
get hasListenClientKeepAlive() {
|
|
230
|
+
return this.listenClientKeepAlive;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Sets the `application_name` of the dedicated `LISTEN` connection — the
|
|
234
|
+
* PostgreSQL analogue of Redis `CLIENT SETNAME` — and remembers it so it is
|
|
235
|
+
* re-applied to any client rebuilt after a drop.
|
|
236
|
+
*/
|
|
237
|
+
async setListenClientName(name) {
|
|
238
|
+
this.listenClientName = name;
|
|
239
|
+
const client = await this.getListenClient();
|
|
240
|
+
await client.query(`SELECT set_config('application_name', $1, false)`, [
|
|
241
|
+
name,
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Re-applies the remembered {@link listenClientName} to a freshly established
|
|
246
|
+
* `LISTEN` client. Best-effort: discovery must never break the client.
|
|
247
|
+
*/
|
|
248
|
+
async applyListenClientName(client) {
|
|
249
|
+
if (!this.listenClientName) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
await client.query(`SELECT set_config('application_name', $1, false)`, [
|
|
254
|
+
this.listenClientName,
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
catch (_a) {
|
|
258
|
+
// Discovery is best-effort; leave the connection unnamed.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Handles a fatal error on the dedicated `LISTEN` client.
|
|
263
|
+
*
|
|
264
|
+
* A `LISTEN` subscription lives on one specific physical connection: once that
|
|
265
|
+
* connection drops, the memoized client is dead and every re-`LISTEN` issued
|
|
266
|
+
* on it is silently lost — so a blocked worker would stop receiving NOTIFYs
|
|
267
|
+
* until its next poll. Invalidate the memo (and tear the dead client down) so
|
|
268
|
+
* the next {@link getListenClient} establishes a fresh connection, and emit
|
|
269
|
+
* `'listenerinvalidated'` so subscribed backends re-`LISTEN` and wake any
|
|
270
|
+
* in-flight blocking wait (which is parked on the now-dead client).
|
|
271
|
+
*
|
|
272
|
+
* Guarded so a stale error from an already-replaced client — or one racing an
|
|
273
|
+
* in-progress {@link close} (which owns teardown) — does not disturb the
|
|
274
|
+
* current client; the error is still forwarded either way.
|
|
275
|
+
*/
|
|
276
|
+
handleListenClientError(client, err) {
|
|
277
|
+
if (this.listenClient === client && !this.closing) {
|
|
278
|
+
const wasStandalone = this.listenClientIsStandalone;
|
|
279
|
+
this.listenClient = undefined;
|
|
280
|
+
this.listenClientPromise = undefined;
|
|
281
|
+
// Drop orphaned notification listeners from in-flight waits; keep the
|
|
282
|
+
// 'error' listener so a follow-up error from the dying client is still
|
|
283
|
+
// swallowed via `emitError` rather than crashing the process.
|
|
284
|
+
client.removeAllListeners('notification');
|
|
285
|
+
try {
|
|
286
|
+
if (wasStandalone) {
|
|
287
|
+
void client.end().catch(() => undefined);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Destroy (rather than return) the broken pooled client.
|
|
291
|
+
client.release(true);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (_a) {
|
|
295
|
+
// Best-effort teardown of an already-broken client.
|
|
296
|
+
}
|
|
297
|
+
this.emit('listenerinvalidated');
|
|
298
|
+
}
|
|
299
|
+
this.emitError(err);
|
|
300
|
+
}
|
|
174
301
|
/**
|
|
175
302
|
* Truthy once {@link PostgresConnection.close} has begun.
|
|
176
303
|
*/
|
|
@@ -78,6 +78,21 @@ export declare class PostgresQueueBackend extends EventEmitter implements IQueue
|
|
|
78
78
|
* positive timeout is fine; we mirror the Redis backend's smallest unit.
|
|
79
79
|
*/
|
|
80
80
|
get minimumBlockTimeout(): number;
|
|
81
|
+
/**
|
|
82
|
+
* PostgreSQL `LISTEN`/`NOTIFY` keeps the connection open and re-arms the wait
|
|
83
|
+
* to the next due delayed job, so there is no cheap-reconnect reason to cap
|
|
84
|
+
* the block at 10s like Redis. A large ceiling lets an idle worker go quiet
|
|
85
|
+
* instead of re-polling every 10s (important for serverless Postgres that
|
|
86
|
+
* suspends when idle). 3600s stays well under the 32-bit `setTimeout` ms
|
|
87
|
+
* ceiling (~24.8 days) that a larger delay would overflow.
|
|
88
|
+
*
|
|
89
|
+
* This relies on a dropped LISTEN connection being *detected* (TCP keepalive)
|
|
90
|
+
* and rebuilt; when keepalive could not be enabled — a user-supplied
|
|
91
|
+
* `pg.Pool` configured without it, whose checked-out client we could not
|
|
92
|
+
* adjust — a silent drop would go unnoticed for the whole block, so the
|
|
93
|
+
* ceiling stays at the conservative Redis-like 10s instead.
|
|
94
|
+
*/
|
|
95
|
+
get maximumBlockTimeout(): number;
|
|
81
96
|
forQueue(queueName: string, _prefix?: string): IQueueBackend;
|
|
82
97
|
/**
|
|
83
98
|
* The queue's qualified name. With a schema-based namespace there is no
|
|
@@ -186,6 +186,19 @@ export class PostgresQueueBackend extends EventEmitter {
|
|
|
186
186
|
this.connection.on('error', err => this.emit('error', err));
|
|
187
187
|
this.connection.on('ready', () => this.emit('ready'));
|
|
188
188
|
this.connection.on('close', () => this.emit('close'));
|
|
189
|
+
// When the shared LISTEN client drops, the connection rebuilds it and
|
|
190
|
+
// emits `'listenerinvalidated'`. Re-issue our LISTENs on the fresh client
|
|
191
|
+
// and wake any in-flight wait (parked on the now-dead client) so the
|
|
192
|
+
// worker loop re-enters waitForJob/readEvents and re-subscribes. Only the
|
|
193
|
+
// connection-owning backend blocks (and thus LISTENs); non-owning
|
|
194
|
+
// `forQueue` siblings never do.
|
|
195
|
+
this.connection.on('listenerinvalidated', () => {
|
|
196
|
+
var _a, _b;
|
|
197
|
+
this.listening = false;
|
|
198
|
+
this.listeningEvents = false;
|
|
199
|
+
(_a = this.cancelWait) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
200
|
+
(_b = this.cancelEventWait) === null || _b === void 0 ? void 0 : _b.call(this);
|
|
201
|
+
});
|
|
189
202
|
}
|
|
190
203
|
}
|
|
191
204
|
// ============================================================
|
|
@@ -243,12 +256,11 @@ export class PostgresQueueBackend extends EventEmitter {
|
|
|
243
256
|
// PostgreSQL analogue of Redis `CLIENT SETNAME`. This is the long-lived
|
|
244
257
|
// connection a worker / QueueEvents holds, so it appears (under this name)
|
|
245
258
|
// in pg_stat_activity and is therefore discoverable by getWorkers /
|
|
246
|
-
// getQueueEvents via getClientList.
|
|
259
|
+
// getQueueEvents via getClientList. The connection remembers the name and
|
|
260
|
+
// re-applies it whenever it has to rebuild the LISTEN client (a fresh
|
|
261
|
+
// connection would otherwise start unnamed, i.e. undiscoverable).
|
|
247
262
|
await this.connection.waitUntilReady();
|
|
248
|
-
|
|
249
|
-
await client.query(`SELECT set_config('application_name', $1, false)`, [
|
|
250
|
-
name,
|
|
251
|
-
]);
|
|
263
|
+
await this.connection.setListenClientName(name);
|
|
252
264
|
}
|
|
253
265
|
/**
|
|
254
266
|
* PostgreSQL `LISTEN`/`NOTIFY` has no minimum block granularity, so any
|
|
@@ -257,6 +269,23 @@ export class PostgresQueueBackend extends EventEmitter {
|
|
|
257
269
|
get minimumBlockTimeout() {
|
|
258
270
|
return 0.001;
|
|
259
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* PostgreSQL `LISTEN`/`NOTIFY` keeps the connection open and re-arms the wait
|
|
274
|
+
* to the next due delayed job, so there is no cheap-reconnect reason to cap
|
|
275
|
+
* the block at 10s like Redis. A large ceiling lets an idle worker go quiet
|
|
276
|
+
* instead of re-polling every 10s (important for serverless Postgres that
|
|
277
|
+
* suspends when idle). 3600s stays well under the 32-bit `setTimeout` ms
|
|
278
|
+
* ceiling (~24.8 days) that a larger delay would overflow.
|
|
279
|
+
*
|
|
280
|
+
* This relies on a dropped LISTEN connection being *detected* (TCP keepalive)
|
|
281
|
+
* and rebuilt; when keepalive could not be enabled — a user-supplied
|
|
282
|
+
* `pg.Pool` configured without it, whose checked-out client we could not
|
|
283
|
+
* adjust — a silent drop would go unnoticed for the whole block, so the
|
|
284
|
+
* ceiling stays at the conservative Redis-like 10s instead.
|
|
285
|
+
*/
|
|
286
|
+
get maximumBlockTimeout() {
|
|
287
|
+
return this.connection.hasListenClientKeepAlive ? 3600 : 10;
|
|
288
|
+
}
|
|
260
289
|
forQueue(queueName, _prefix) {
|
|
261
290
|
// The namespace is the connection's schema, shared by all queues, so a
|
|
262
291
|
// sibling backend only needs a different queue name. BullMQ's per-queue
|