bullmq 6.3.1 → 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
|
@@ -242,6 +242,14 @@ class RedisQueueBackend extends events_1.EventEmitter {
|
|
|
242
242
|
? 0.001
|
|
243
243
|
: 0.002;
|
|
244
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Largest meaningful block timeout (seconds). Capped at 10s because a
|
|
247
|
+
* `BZPOPMIN` blocked longer than this risks issues on reconnection
|
|
248
|
+
* (see #1658).
|
|
249
|
+
*/
|
|
250
|
+
get maximumBlockTimeout() {
|
|
251
|
+
return 10;
|
|
252
|
+
}
|
|
245
253
|
/**
|
|
246
254
|
* Interrupts the in-flight blocking wait by disconnecting the dedicated
|
|
247
255
|
* blocking connection. No-op if there is none.
|
|
@@ -14,8 +14,9 @@ const errors_1 = require("./errors");
|
|
|
14
14
|
const enums_1 = require("../enums");
|
|
15
15
|
const job_scheduler_1 = require("./job-scheduler");
|
|
16
16
|
const lock_manager_1 = require("./lock-manager");
|
|
17
|
-
// 10 seconds is the maximum time a BZPOPMIN can block
|
|
18
|
-
|
|
17
|
+
// 10 seconds is the maximum time a BZPOPMIN can block, so it is the default
|
|
18
|
+
// ceiling used when a backend does not delegate its own `maximumBlockTimeout`.
|
|
19
|
+
const defaultMaximumBlockTimeout = 10;
|
|
19
20
|
/**
|
|
20
21
|
*
|
|
21
22
|
* This class represents a worker that is able to process jobs from the queue.
|
|
@@ -396,6 +397,10 @@ class Worker extends queue_base_1.QueueBase {
|
|
|
396
397
|
get minimumBlockTimeout() {
|
|
397
398
|
return this.backend.minimumBlockTimeout;
|
|
398
399
|
}
|
|
400
|
+
get maximumBlockTimeout() {
|
|
401
|
+
var _a;
|
|
402
|
+
return (_a = this.backend.maximumBlockTimeout) !== null && _a !== void 0 ? _a : defaultMaximumBlockTimeout;
|
|
403
|
+
}
|
|
399
404
|
isRateLimited() {
|
|
400
405
|
return this.limitUntil > Date.now();
|
|
401
406
|
}
|
|
@@ -466,10 +471,13 @@ class Worker extends queue_base_1.QueueBase {
|
|
|
466
471
|
return this.minimumBlockTimeout;
|
|
467
472
|
}
|
|
468
473
|
else {
|
|
469
|
-
// We restrict the maximum block timeout to
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
474
|
+
// We restrict the maximum block timeout to avoid blocking the
|
|
475
|
+
// connection for too long in the case of reconnections. The ceiling is
|
|
476
|
+
// backend-specific: Redis caps it at 10s (a `BZPOPMIN` blocked longer
|
|
477
|
+
// risks issues on reconnection, see #1658), whereas a backend that
|
|
478
|
+
// keeps the connection open and re-arms to the next due job can allow a
|
|
479
|
+
// much larger value so an idle worker stops re-polling.
|
|
480
|
+
return Math.min(blockDelay / 1000, this.maximumBlockTimeout);
|
|
473
481
|
}
|
|
474
482
|
}
|
|
475
483
|
else {
|
|
@@ -27,6 +27,34 @@ function loadPgModule() {
|
|
|
27
27
|
'already-constructed `pg.Pool` instance as the connection instead of a ' +
|
|
28
28
|
'config object or connection string.');
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Idle time (ms) before the first TCP keepalive probe is sent on the dedicated
|
|
32
|
+
* `LISTEN` connection. Short enough that a silently dropped connection is
|
|
33
|
+
* detected in seconds instead of the OS default (typically two hours) — which,
|
|
34
|
+
* with a large `maximumBlockTimeout`, is what a blocked worker would otherwise
|
|
35
|
+
* have to wait for.
|
|
36
|
+
*/
|
|
37
|
+
const LISTEN_KEEPALIVE_INITIAL_DELAY_MS = 10000;
|
|
38
|
+
/**
|
|
39
|
+
* Best-effort: turns TCP keepalive on for an already-established client's
|
|
40
|
+
* socket. Needed for the pooled `LISTEN` client, since a user-supplied
|
|
41
|
+
* `pg.Pool` may have been created without `keepAlive` and a checked-out client
|
|
42
|
+
* inherits that configuration. Returns whether keepalive is now enabled.
|
|
43
|
+
*/
|
|
44
|
+
function enableSocketKeepAlive(client) {
|
|
45
|
+
var _a;
|
|
46
|
+
const stream = (_a = client.connection) === null || _a === void 0 ? void 0 : _a.stream;
|
|
47
|
+
if (!stream || typeof stream.setKeepAlive !== 'function') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
stream.setKeepAlive(true, LISTEN_KEEPALIVE_INITIAL_DELAY_MS);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch (_b) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
30
58
|
/**
|
|
31
59
|
* Owns the PostgreSQL connection resources for a single backend:
|
|
32
60
|
*
|
|
@@ -40,6 +68,7 @@ function loadPgModule() {
|
|
|
40
68
|
*/
|
|
41
69
|
class PostgresConnection extends events_1.EventEmitter {
|
|
42
70
|
constructor(connection) {
|
|
71
|
+
var _a;
|
|
43
72
|
super();
|
|
44
73
|
/**
|
|
45
74
|
* `true` when {@link listenClient} is a standalone `pg.Client` we must `end()`
|
|
@@ -54,10 +83,13 @@ class PostgresConnection extends events_1.EventEmitter {
|
|
|
54
83
|
this.migrateOnConnect = false;
|
|
55
84
|
this.pgModule = undefined;
|
|
56
85
|
this.listenClientConfig = undefined;
|
|
86
|
+
// A user-supplied pool decides its own `keepAlive`; when it is off we try
|
|
87
|
+
// to enable it on the checked-out LISTEN socket (see getListenClient).
|
|
88
|
+
this.listenClientKeepAlive = Boolean((_a = connection.options) === null || _a === void 0 ? void 0 : _a.keepAlive);
|
|
57
89
|
}
|
|
58
90
|
else {
|
|
59
91
|
const pg = loadPgModule();
|
|
60
|
-
const
|
|
92
|
+
const _b = typeof connection === 'string'
|
|
61
93
|
? {
|
|
62
94
|
schema: undefined,
|
|
63
95
|
skipVersionCheck: undefined,
|
|
@@ -65,7 +97,7 @@ class PostgresConnection extends events_1.EventEmitter {
|
|
|
65
97
|
skipMigrations: undefined,
|
|
66
98
|
connectionString: connection,
|
|
67
99
|
}
|
|
68
|
-
: connection, { schema, skipVersionCheck, migrate, skipMigrations } =
|
|
100
|
+
: connection, { schema, skipVersionCheck, migrate, skipMigrations } = _b, poolConfig = tslib_1.__rest(_b, ["schema", "skipVersionCheck", "migrate", "skipMigrations"]);
|
|
69
101
|
if (migrate !== undefined && skipMigrations !== undefined) {
|
|
70
102
|
throw new Error('BullMQ: `migrate` and `skipMigrations` are mutually exclusive. Set only one.');
|
|
71
103
|
}
|
|
@@ -88,6 +120,8 @@ class PostgresConnection extends events_1.EventEmitter {
|
|
|
88
120
|
// Keep the means to build a dedicated LISTEN connection on demand.
|
|
89
121
|
this.pgModule = pg;
|
|
90
122
|
this.listenClientConfig = resolvedConfig;
|
|
123
|
+
// We build the LISTEN client ourselves, always with keepAlive enabled.
|
|
124
|
+
this.listenClientKeepAlive = true;
|
|
91
125
|
}
|
|
92
126
|
// The pool emits 'error' for idle clients that drop; surface it but never
|
|
93
127
|
// let it crash the process — hence the guarded {@link emitError} (a bare
|
|
@@ -156,24 +190,117 @@ class PostgresConnection extends events_1.EventEmitter {
|
|
|
156
190
|
if (!this.listenClientPromise) {
|
|
157
191
|
this.listenClientPromise = (async () => {
|
|
158
192
|
if (this.pgModule && this.listenClientConfig) {
|
|
159
|
-
const client = new this.pgModule.Client(this.listenClientConfig)
|
|
193
|
+
const client = new this.pgModule.Client(Object.assign(Object.assign({}, this.listenClientConfig), {
|
|
194
|
+
// A LISTEN subscription is bound to one physical connection. Enable
|
|
195
|
+
// TCP keepalive so a silently dropped connection surfaces as a
|
|
196
|
+
// socket `'error'` (and is then rebuilt below) instead of going
|
|
197
|
+
// unnoticed until the next poll — which, with a large
|
|
198
|
+
// `maximumBlockTimeout`, could be up to an hour away.
|
|
199
|
+
keepAlive: true, keepAliveInitialDelayMillis: LISTEN_KEEPALIVE_INITIAL_DELAY_MS }));
|
|
160
200
|
await client.connect();
|
|
161
|
-
client.on('error', err => this.
|
|
201
|
+
client.on('error', err => this.handleListenClientError(client, err));
|
|
202
|
+
this.listenClientKeepAlive = true;
|
|
162
203
|
this.listenClientIsStandalone = true;
|
|
163
204
|
this.listenClient = client;
|
|
205
|
+
await this.applyListenClientName(client);
|
|
164
206
|
return client;
|
|
165
207
|
}
|
|
166
208
|
else {
|
|
167
209
|
const client = await this.pool.connect();
|
|
168
|
-
client.on('error', err => this.
|
|
210
|
+
client.on('error', err => this.handleListenClientError(client, err));
|
|
211
|
+
// The pool is the user's, so its clients may have been created
|
|
212
|
+
// without `keepAlive`; enable it on this socket so a silent drop is
|
|
213
|
+
// still detected. If that is not possible the connection reports no
|
|
214
|
+
// keepalive and the backend caps its block timeout conservatively.
|
|
215
|
+
this.listenClientKeepAlive =
|
|
216
|
+
this.listenClientKeepAlive || enableSocketKeepAlive(client);
|
|
169
217
|
this.listenClientIsStandalone = false;
|
|
170
218
|
this.listenClient = client;
|
|
219
|
+
await this.applyListenClientName(client);
|
|
171
220
|
return client;
|
|
172
221
|
}
|
|
173
222
|
})();
|
|
174
223
|
}
|
|
175
224
|
return this.listenClientPromise;
|
|
176
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Whether TCP keepalive is enabled on the dedicated `LISTEN` connection, so a
|
|
228
|
+
* silently dropped connection is detected (and the client rebuilt) instead of
|
|
229
|
+
* lingering unnoticed. Consulted by the backend when deciding how long a
|
|
230
|
+
* worker may block.
|
|
231
|
+
*/
|
|
232
|
+
get hasListenClientKeepAlive() {
|
|
233
|
+
return this.listenClientKeepAlive;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Sets the `application_name` of the dedicated `LISTEN` connection — the
|
|
237
|
+
* PostgreSQL analogue of Redis `CLIENT SETNAME` — and remembers it so it is
|
|
238
|
+
* re-applied to any client rebuilt after a drop.
|
|
239
|
+
*/
|
|
240
|
+
async setListenClientName(name) {
|
|
241
|
+
this.listenClientName = name;
|
|
242
|
+
const client = await this.getListenClient();
|
|
243
|
+
await client.query(`SELECT set_config('application_name', $1, false)`, [
|
|
244
|
+
name,
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Re-applies the remembered {@link listenClientName} to a freshly established
|
|
249
|
+
* `LISTEN` client. Best-effort: discovery must never break the client.
|
|
250
|
+
*/
|
|
251
|
+
async applyListenClientName(client) {
|
|
252
|
+
if (!this.listenClientName) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await client.query(`SELECT set_config('application_name', $1, false)`, [
|
|
257
|
+
this.listenClientName,
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
catch (_a) {
|
|
261
|
+
// Discovery is best-effort; leave the connection unnamed.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Handles a fatal error on the dedicated `LISTEN` client.
|
|
266
|
+
*
|
|
267
|
+
* A `LISTEN` subscription lives on one specific physical connection: once that
|
|
268
|
+
* connection drops, the memoized client is dead and every re-`LISTEN` issued
|
|
269
|
+
* on it is silently lost — so a blocked worker would stop receiving NOTIFYs
|
|
270
|
+
* until its next poll. Invalidate the memo (and tear the dead client down) so
|
|
271
|
+
* the next {@link getListenClient} establishes a fresh connection, and emit
|
|
272
|
+
* `'listenerinvalidated'` so subscribed backends re-`LISTEN` and wake any
|
|
273
|
+
* in-flight blocking wait (which is parked on the now-dead client).
|
|
274
|
+
*
|
|
275
|
+
* Guarded so a stale error from an already-replaced client — or one racing an
|
|
276
|
+
* in-progress {@link close} (which owns teardown) — does not disturb the
|
|
277
|
+
* current client; the error is still forwarded either way.
|
|
278
|
+
*/
|
|
279
|
+
handleListenClientError(client, err) {
|
|
280
|
+
if (this.listenClient === client && !this.closing) {
|
|
281
|
+
const wasStandalone = this.listenClientIsStandalone;
|
|
282
|
+
this.listenClient = undefined;
|
|
283
|
+
this.listenClientPromise = undefined;
|
|
284
|
+
// Drop orphaned notification listeners from in-flight waits; keep the
|
|
285
|
+
// 'error' listener so a follow-up error from the dying client is still
|
|
286
|
+
// swallowed via `emitError` rather than crashing the process.
|
|
287
|
+
client.removeAllListeners('notification');
|
|
288
|
+
try {
|
|
289
|
+
if (wasStandalone) {
|
|
290
|
+
void client.end().catch(() => undefined);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Destroy (rather than return) the broken pooled client.
|
|
294
|
+
client.release(true);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (_a) {
|
|
298
|
+
// Best-effort teardown of an already-broken client.
|
|
299
|
+
}
|
|
300
|
+
this.emit('listenerinvalidated');
|
|
301
|
+
}
|
|
302
|
+
this.emitError(err);
|
|
303
|
+
}
|
|
177
304
|
/**
|
|
178
305
|
* Truthy once {@link PostgresConnection.close} has begun.
|
|
179
306
|
*/
|
|
@@ -189,6 +189,19 @@ class PostgresQueueBackend extends events_1.EventEmitter {
|
|
|
189
189
|
this.connection.on('error', err => this.emit('error', err));
|
|
190
190
|
this.connection.on('ready', () => this.emit('ready'));
|
|
191
191
|
this.connection.on('close', () => this.emit('close'));
|
|
192
|
+
// When the shared LISTEN client drops, the connection rebuilds it and
|
|
193
|
+
// emits `'listenerinvalidated'`. Re-issue our LISTENs on the fresh client
|
|
194
|
+
// and wake any in-flight wait (parked on the now-dead client) so the
|
|
195
|
+
// worker loop re-enters waitForJob/readEvents and re-subscribes. Only the
|
|
196
|
+
// connection-owning backend blocks (and thus LISTENs); non-owning
|
|
197
|
+
// `forQueue` siblings never do.
|
|
198
|
+
this.connection.on('listenerinvalidated', () => {
|
|
199
|
+
var _a, _b;
|
|
200
|
+
this.listening = false;
|
|
201
|
+
this.listeningEvents = false;
|
|
202
|
+
(_a = this.cancelWait) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
203
|
+
(_b = this.cancelEventWait) === null || _b === void 0 ? void 0 : _b.call(this);
|
|
204
|
+
});
|
|
192
205
|
}
|
|
193
206
|
}
|
|
194
207
|
// ============================================================
|
|
@@ -246,12 +259,11 @@ class PostgresQueueBackend extends events_1.EventEmitter {
|
|
|
246
259
|
// PostgreSQL analogue of Redis `CLIENT SETNAME`. This is the long-lived
|
|
247
260
|
// connection a worker / QueueEvents holds, so it appears (under this name)
|
|
248
261
|
// in pg_stat_activity and is therefore discoverable by getWorkers /
|
|
249
|
-
// getQueueEvents via getClientList.
|
|
262
|
+
// getQueueEvents via getClientList. The connection remembers the name and
|
|
263
|
+
// re-applies it whenever it has to rebuild the LISTEN client (a fresh
|
|
264
|
+
// connection would otherwise start unnamed, i.e. undiscoverable).
|
|
250
265
|
await this.connection.waitUntilReady();
|
|
251
|
-
|
|
252
|
-
await client.query(`SELECT set_config('application_name', $1, false)`, [
|
|
253
|
-
name,
|
|
254
|
-
]);
|
|
266
|
+
await this.connection.setListenClientName(name);
|
|
255
267
|
}
|
|
256
268
|
/**
|
|
257
269
|
* PostgreSQL `LISTEN`/`NOTIFY` has no minimum block granularity, so any
|
|
@@ -260,6 +272,23 @@ class PostgresQueueBackend extends events_1.EventEmitter {
|
|
|
260
272
|
get minimumBlockTimeout() {
|
|
261
273
|
return 0.001;
|
|
262
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* PostgreSQL `LISTEN`/`NOTIFY` keeps the connection open and re-arms the wait
|
|
277
|
+
* to the next due delayed job, so there is no cheap-reconnect reason to cap
|
|
278
|
+
* the block at 10s like Redis. A large ceiling lets an idle worker go quiet
|
|
279
|
+
* instead of re-polling every 10s (important for serverless Postgres that
|
|
280
|
+
* suspends when idle). 3600s stays well under the 32-bit `setTimeout` ms
|
|
281
|
+
* ceiling (~24.8 days) that a larger delay would overflow.
|
|
282
|
+
*
|
|
283
|
+
* This relies on a dropped LISTEN connection being *detected* (TCP keepalive)
|
|
284
|
+
* and rebuilt; when keepalive could not be enabled — a user-supplied
|
|
285
|
+
* `pg.Pool` configured without it, whose checked-out client we could not
|
|
286
|
+
* adjust — a silent drop would go unnoticed for the whole block, so the
|
|
287
|
+
* ceiling stays at the conservative Redis-like 10s instead.
|
|
288
|
+
*/
|
|
289
|
+
get maximumBlockTimeout() {
|
|
290
|
+
return this.connection.hasListenClientKeepAlive ? 3600 : 10;
|
|
291
|
+
}
|
|
263
292
|
forQueue(queueName, _prefix) {
|
|
264
293
|
// The namespace is the connection's schema, shared by all queues, so a
|
|
265
294
|
// sibling backend only needs a different queue name. BullMQ's per-queue
|