@pauldeng/node-red-contrib-bullmq 1.0.3 → 2.0.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/README.md +61 -38
- package/bull-queue.html +241 -73
- package/bull-queue.js +623 -120
- package/docs/ARCHITECTURE.md +62 -18
- package/docs/CHANGE_WORKFLOW.md +10 -11
- package/docs/COMMANDS.md +72 -11
- package/docs/CONNECTIONS.md +62 -5
- package/docs/MIGRATION.md +56 -22
- package/docs/NODE_GUIDE.md +51 -16
- package/docs/REFERENCE_MAP.md +27 -10
- package/docs/RELEASE.md +17 -4
- package/docs/RULES.md +33 -0
- package/docs/TELEMETRY.md +55 -0
- package/docs/TESTING.md +30 -16
- package/docs/TROUBLESHOOTING.md +44 -5
- package/examples/README.md +40 -12
- package/examples/bullmq_features.json +218 -16
- package/examples/example_flow.json +11 -11
- package/examples/postgres_backend.json +149 -0
- package/examples/repeatable_jobs.json +51 -19
- package/examples/scheduled_notifications.json +216 -0
- package/lib/acknowledgements.js +19 -1
- package/lib/commands.js +74 -35
- package/lib/connections.js +212 -26
- package/lib/scheduler.js +5 -74
- package/lib/serialization.js +1 -4
- package/package.json +24 -8
package/bull-queue.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
+
createPostgresBackend,
|
|
5
|
+
DelayedError,
|
|
4
6
|
FlowProducer,
|
|
7
|
+
MINIMUM_POSTGRES_VERSION,
|
|
5
8
|
Queue,
|
|
6
9
|
QueueEvents,
|
|
10
|
+
RECOMMENDED_POSTGRES_VERSION,
|
|
11
|
+
SchemaMigrationRequiredError,
|
|
12
|
+
SchemaVersionMismatchError,
|
|
7
13
|
UnrecoverableError,
|
|
14
|
+
UnsupportedPostgresVersionError,
|
|
15
|
+
WaitingError,
|
|
8
16
|
Worker,
|
|
9
17
|
} = require("bullmq");
|
|
10
18
|
const IORedis = require("ioredis");
|
|
@@ -16,12 +24,14 @@ const {
|
|
|
16
24
|
} = require("./lib/acknowledgements");
|
|
17
25
|
const { dispatchCommand } = require("./lib/commands");
|
|
18
26
|
const {
|
|
27
|
+
POSTGRES_CONNECTION_TIMEOUT_MS,
|
|
19
28
|
buildBullMQOptions,
|
|
20
29
|
buildRedisDescriptor,
|
|
21
30
|
createRedisConnection,
|
|
22
31
|
normalizeQueueConfig,
|
|
23
32
|
} = require("./lib/connections");
|
|
24
33
|
const { serializeFlowJob, serializeJob } = require("./lib/serialization");
|
|
34
|
+
const { version: PACKAGE_VERSION } = require("./package.json");
|
|
25
35
|
|
|
26
36
|
const DEFAULT_EVENTS = [
|
|
27
37
|
"active",
|
|
@@ -37,6 +47,7 @@ const DEFAULT_EVENTS = [
|
|
|
37
47
|
"progress",
|
|
38
48
|
"removed",
|
|
39
49
|
"resumed",
|
|
50
|
+
"retries-exhausted",
|
|
40
51
|
"stalled",
|
|
41
52
|
"waiting",
|
|
42
53
|
"waiting-children",
|
|
@@ -47,6 +58,34 @@ const DEFAULT_EVENTS = [
|
|
|
47
58
|
// an unreachable Redis server.
|
|
48
59
|
const CLOSE_GRACE_MS = 1000;
|
|
49
60
|
|
|
61
|
+
// A connection that is actually ready gets a longer budget: Worker.close()
|
|
62
|
+
// waits for in-flight jobs, and cutting that off abandons a running job to the
|
|
63
|
+
// stalled checker, which re-runs it and can eventually fail it for exceeding
|
|
64
|
+
// maxStalledCount. The ceiling stays well under Node-RED's own ~15s node close
|
|
65
|
+
// timeout, and resources close concurrently, so this bounds one resource, not
|
|
66
|
+
// the sum.
|
|
67
|
+
const GRACEFUL_CLOSE_MS = 10000;
|
|
68
|
+
|
|
69
|
+
// PostgreSQL's connection timeout is its only way to stop an outstanding
|
|
70
|
+
// connect. Unlike Redis, it has no raw client that we can force closed, so its
|
|
71
|
+
// close budget needs scheduling margin beyond that timeout. Derived from the
|
|
72
|
+
// timeout it has to outlast rather than written as a literal, so raising
|
|
73
|
+
// POSTGRES_CONNECTION_TIMEOUT_MS cannot silently leave the budget short. It
|
|
74
|
+
// has a ceiling as well as a floor: Node-RED gives each node ~15s to close, so
|
|
75
|
+
// POSTGRES_CONNECTION_TIMEOUT_MS must stay well under 14s or this budget stops
|
|
76
|
+
// bounding anything and Node-RED's own timeout cuts the close off instead.
|
|
77
|
+
const POSTGRES_CLOSE_MS = POSTGRES_CONNECTION_TIMEOUT_MS + CLOSE_GRACE_MS;
|
|
78
|
+
|
|
79
|
+
function closeBudgetFor(connection) {
|
|
80
|
+
// PostgreSQL owners have no companion connection in node.resources because
|
|
81
|
+
// BullMQ owns their pool. Redis owners retain the live ioredis status that
|
|
82
|
+
// distinguishes a healthy worker drain from an unreachable fast close.
|
|
83
|
+
if (!connection) {
|
|
84
|
+
return POSTGRES_CLOSE_MS;
|
|
85
|
+
}
|
|
86
|
+
return connection.status === "ready" ? GRACEFUL_CLOSE_MS : CLOSE_GRACE_MS;
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
async function settled(promise) {
|
|
51
90
|
try {
|
|
52
91
|
await promise;
|
|
@@ -56,13 +95,16 @@ async function settled(promise) {
|
|
|
56
95
|
return "settled";
|
|
57
96
|
}
|
|
58
97
|
|
|
59
|
-
async function timedOut(ms) {
|
|
60
|
-
await sleep(ms);
|
|
61
|
-
return "timeout";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
98
|
async function settleWithin(promise, ms) {
|
|
65
|
-
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
try {
|
|
101
|
+
return await Promise.race([
|
|
102
|
+
settled(promise),
|
|
103
|
+
sleep(ms, "timeout", { signal: controller.signal }),
|
|
104
|
+
]);
|
|
105
|
+
} finally {
|
|
106
|
+
controller.abort();
|
|
107
|
+
}
|
|
66
108
|
}
|
|
67
109
|
|
|
68
110
|
function disconnectClient(client) {
|
|
@@ -75,7 +117,7 @@ function disconnectClient(client) {
|
|
|
75
117
|
}
|
|
76
118
|
}
|
|
77
119
|
|
|
78
|
-
function forceDisconnect(resource) {
|
|
120
|
+
async function forceDisconnect(resource) {
|
|
79
121
|
if (!resource) {
|
|
80
122
|
return;
|
|
81
123
|
}
|
|
@@ -84,18 +126,44 @@ function forceDisconnect(resource) {
|
|
|
84
126
|
disconnectClient(resource);
|
|
85
127
|
return;
|
|
86
128
|
}
|
|
87
|
-
// BullMQ
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
129
|
+
// BullMQ 6.3.1's public disconnect() can await the same never-ready promise
|
|
130
|
+
// as close(), which would spend a second shutdown budget after the first one
|
|
131
|
+
// already expired. The exact BullMQ pin makes this one backend escape hatch
|
|
132
|
+
// deliberate and testable until upstream disconnect becomes bounded.
|
|
133
|
+
if (typeof resource.getBackend !== "function") {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const backend = resource.getBackend();
|
|
137
|
+
// RedisQueueBackend is the only installed backend exposing raw `_client`
|
|
138
|
+
// handles. PostgreSQL deliberately skips this branch.
|
|
139
|
+
if (backend && backend.connection && backend.connection._client) {
|
|
140
|
+
disconnectClient(backend.connection._client);
|
|
141
|
+
disconnectClient(
|
|
142
|
+
backend.blockingConnection && backend.blockingConnection._client,
|
|
143
|
+
);
|
|
92
144
|
}
|
|
93
|
-
|
|
94
|
-
|
|
145
|
+
// Measured on installed BullMQ 6.3.1: a Worker's close() never reaches this
|
|
146
|
+
// point on its own here, because its very first cleanup step awaits the
|
|
147
|
+
// same stuck connection above. That means the lock-renewal timer it starts
|
|
148
|
+
// on construction (independent of connection state) is never cancelled by
|
|
149
|
+
// Worker's own close() -- stop it here directly so it cannot outlive the
|
|
150
|
+
// resource and keep the process alive.
|
|
151
|
+
if (resource.lockManager && typeof resource.lockManager.close === "function") {
|
|
152
|
+
await resource.lockManager.close();
|
|
95
153
|
}
|
|
154
|
+
// Same reasoning for the stalled-job checker: Worker.close() only stops it
|
|
155
|
+
// after the cleanup step that hung above, and each check reschedules its own
|
|
156
|
+
// timer. This one is only live if the worker processed at least once before
|
|
157
|
+
// Redis went away -- the common production case, unlike a connection that was
|
|
158
|
+
// never reachable at all.
|
|
159
|
+
if (typeof resource.stalledCheckStopper === "function") {
|
|
160
|
+
resource.stalledCheckStopper();
|
|
161
|
+
}
|
|
162
|
+
// PostgreSQL has no raw force-disconnect branch. closeBudgetFor therefore
|
|
163
|
+
// lets its own connection timeout settle close() before Node-RED calls done.
|
|
96
164
|
}
|
|
97
165
|
|
|
98
|
-
async function closeResource(resource) {
|
|
166
|
+
async function closeResource(resource, connection) {
|
|
99
167
|
if (!resource) {
|
|
100
168
|
return;
|
|
101
169
|
}
|
|
@@ -106,26 +174,29 @@ async function closeResource(resource) {
|
|
|
106
174
|
// connections and force-disconnect everything else.
|
|
107
175
|
if (resource.status === "ready" && typeof resource.quit === "function") {
|
|
108
176
|
if ((await settleWithin(resource.quit(), CLOSE_GRACE_MS)) === "timeout") {
|
|
109
|
-
forceDisconnect(resource);
|
|
177
|
+
await forceDisconnect(resource);
|
|
110
178
|
}
|
|
111
179
|
} else {
|
|
112
|
-
forceDisconnect(resource);
|
|
180
|
+
await forceDisconnect(resource);
|
|
113
181
|
}
|
|
114
182
|
return;
|
|
115
183
|
}
|
|
116
184
|
|
|
117
185
|
// BullMQ resource: QueueEvents.close() blocks forever on a connection that
|
|
118
|
-
// never became ready, so cap the graceful close and force-disconnect
|
|
119
|
-
//
|
|
120
|
-
if (
|
|
121
|
-
|
|
186
|
+
// never became ready, so cap the graceful close and force-disconnect when
|
|
187
|
+
// it does not settle in time (see forceDisconnect for the fallback chain).
|
|
188
|
+
if (
|
|
189
|
+
(await settleWithin(resource.close(), closeBudgetFor(connection))) ===
|
|
190
|
+
"timeout"
|
|
191
|
+
) {
|
|
192
|
+
await forceDisconnect(resource);
|
|
122
193
|
}
|
|
123
194
|
}
|
|
124
195
|
|
|
125
196
|
async function closeResourcePair(owner, connection) {
|
|
126
197
|
let firstError;
|
|
127
198
|
try {
|
|
128
|
-
await closeResource(owner);
|
|
199
|
+
await closeResource(owner, connection);
|
|
129
200
|
} catch (err) {
|
|
130
201
|
firstError = err;
|
|
131
202
|
}
|
|
@@ -151,6 +222,62 @@ function nodeDone(node, done, err, msg) {
|
|
|
151
222
|
}
|
|
152
223
|
}
|
|
153
224
|
|
|
225
|
+
// FlowProducer.addBulk(flows) accepts no options argument, so the queuesOptions
|
|
226
|
+
// route withFlowJobDefaults() uses for add() is unavailable here. Stamp the
|
|
227
|
+
// retention onto each job's own opts instead; anything the caller set wins.
|
|
228
|
+
function withBulkFlowJobDefaults(flow, defaultJobOptions) {
|
|
229
|
+
if (!defaultJobOptions || !flow || typeof flow !== "object") {
|
|
230
|
+
return flow;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const children = Array.isArray(flow.children)
|
|
234
|
+
? flow.children.map((child) =>
|
|
235
|
+
withBulkFlowJobDefaults(child, defaultJobOptions),
|
|
236
|
+
)
|
|
237
|
+
: flow.children;
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
...flow,
|
|
241
|
+
opts: { ...defaultJobOptions, ...flow.opts },
|
|
242
|
+
...(children === undefined ? {} : { children }),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function withFlowJobDefaults(flow, flowOptions, defaultJobOptions) {
|
|
247
|
+
if (!defaultJobOptions) {
|
|
248
|
+
return flowOptions;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const options =
|
|
252
|
+
flowOptions && typeof flowOptions === "object" ? flowOptions : {};
|
|
253
|
+
// Already a fresh copy, so later queues are assigned into it rather than
|
|
254
|
+
// re-spreading the whole map per queue name.
|
|
255
|
+
const queuesOptions = { ...options.queuesOptions };
|
|
256
|
+
const pending = [flow];
|
|
257
|
+
|
|
258
|
+
while (pending.length > 0) {
|
|
259
|
+
const job = pending.pop();
|
|
260
|
+
if (!job || typeof job !== "object") {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (typeof job.queueName === "string" && job.queueName) {
|
|
264
|
+
const queueOptions = queuesOptions[job.queueName] || {};
|
|
265
|
+
queuesOptions[job.queueName] = {
|
|
266
|
+
...queueOptions,
|
|
267
|
+
defaultJobOptions: {
|
|
268
|
+
...defaultJobOptions,
|
|
269
|
+
...queueOptions.defaultJobOptions,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (Array.isArray(job.children)) {
|
|
274
|
+
pending.push(...job.children);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { ...options, queuesOptions };
|
|
279
|
+
}
|
|
280
|
+
|
|
154
281
|
function isPresent(value) {
|
|
155
282
|
return value !== undefined && value !== null && value !== "";
|
|
156
283
|
}
|
|
@@ -190,14 +317,60 @@ function setDisconnected(node) {
|
|
|
190
317
|
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
|
191
318
|
}
|
|
192
319
|
|
|
193
|
-
function attachErrorListener(resource, node) {
|
|
320
|
+
function attachErrorListener(resource, node, startupFailureOwner) {
|
|
194
321
|
if (!resource || typeof resource.on !== "function") {
|
|
195
|
-
return;
|
|
322
|
+
return () => {};
|
|
196
323
|
}
|
|
324
|
+
let ready = false;
|
|
197
325
|
resource.on("error", (err) => {
|
|
198
326
|
setDisconnected(node);
|
|
199
|
-
|
|
327
|
+
if (startupFailureOwner && !ready) {
|
|
328
|
+
startupFailureOwner.reportBackendFailure(err);
|
|
329
|
+
} else {
|
|
330
|
+
node.error(err);
|
|
331
|
+
}
|
|
200
332
|
});
|
|
333
|
+
return function markReady() {
|
|
334
|
+
ready = true;
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Turns a rejected backend readiness promise -- or a synchronous resource
|
|
339
|
+
// construction failure, since BullMQ's postgres factory validates and loads
|
|
340
|
+
// its optional `pg` dependency before any I/O -- into one of a handful of
|
|
341
|
+
// actionable categories, so a Node-RED user is told what to DO rather than
|
|
342
|
+
// just that the backend is unavailable. The category string is also the
|
|
343
|
+
// dedup key each config node latches on; see node.reportBackendFailure.
|
|
344
|
+
function describeBackendFailure(err) {
|
|
345
|
+
const message = err && err.message ? err.message : String(err);
|
|
346
|
+
if (err instanceof SchemaMigrationRequiredError) {
|
|
347
|
+
return {
|
|
348
|
+
category: "schema-migration-required",
|
|
349
|
+
message: `${message} Set the queue configuration node's "migrate" property to true so this node initialises the schema, or run BullMQ's PostgreSQL migrations against the database yourself before deploying.`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (err instanceof SchemaVersionMismatchError) {
|
|
353
|
+
return {
|
|
354
|
+
category: "schema-version-mismatch",
|
|
355
|
+
message: `${message} Upgrade this Node-RED package to a release that uses the required BullMQ major; PostgreSQL schema downgrades are not supported.`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (err instanceof UnsupportedPostgresVersionError) {
|
|
359
|
+
return {
|
|
360
|
+
category: "unsupported-postgres-version",
|
|
361
|
+
message: `${message} BullMQ's PostgreSQL backend requires server version ${MINIMUM_POSTGRES_VERSION} or newer (${RECOMMENDED_POSTGRES_VERSION}+ recommended).`,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// BullMQ lazily requires the optional `pg` package and, when it cannot be
|
|
365
|
+
// resolved, throws its own actionable error rather than a raw
|
|
366
|
+
// MODULE_NOT_FOUND -- reuse that message instead of wrapping it again.
|
|
367
|
+
if (message.includes("npm install pg")) {
|
|
368
|
+
return { category: "pg-missing", message };
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
category: "generic",
|
|
372
|
+
message: `BullMQ backend is unavailable: ${message}`,
|
|
373
|
+
};
|
|
201
374
|
}
|
|
202
375
|
|
|
203
376
|
function createJobMessage(job, queueName, extraBull = {}) {
|
|
@@ -223,24 +396,60 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
223
396
|
RED.nodes.createNode(this, n);
|
|
224
397
|
const node = this;
|
|
225
398
|
|
|
226
|
-
node.users = {};
|
|
227
399
|
node.resources = new Map();
|
|
228
400
|
node.config = normalizeQueueConfig(n, node.credentials || {});
|
|
229
401
|
node.queue = null;
|
|
230
402
|
node.producerConnection = null;
|
|
231
|
-
|
|
232
|
-
node.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
403
|
+
node.telemetry = undefined;
|
|
404
|
+
node.telemetryUnavailable = false;
|
|
405
|
+
|
|
406
|
+
// Categories already reported for this config node. BullMQ builds one
|
|
407
|
+
// backend per resource (Queue/Worker/QueueEvents/FlowProducer each get
|
|
408
|
+
// their own pool), so the same misconfiguration can be observed from
|
|
409
|
+
// more than one of them -- this latch is what keeps one deploy at one
|
|
410
|
+
// report per distinct failure instead of one per resource that hit it.
|
|
411
|
+
node.backendFailures = new Set();
|
|
412
|
+
node.reportBackendFailure = function reportBackendFailure(err) {
|
|
413
|
+
const { category, message } = describeBackendFailure(err);
|
|
414
|
+
if (node.backendFailures.has(category)) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
node.backendFailures.add(category);
|
|
418
|
+
node.error(message);
|
|
239
419
|
};
|
|
240
420
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
421
|
+
// Lazily constructs (and caches) the one BullMQOtel instance shared by
|
|
422
|
+
// this config node's Queue/Worker/FlowProducer. Deferred until first
|
|
423
|
+
// resource creation rather than built in the constructor above: Node-RED
|
|
424
|
+
// builds config nodes at deploy time, which can precede the host's
|
|
425
|
+
// OpenTelemetry bootstrap, and enableMetrics needs a MeterProvider
|
|
426
|
+
// registered before BullMQOtel is constructed.
|
|
427
|
+
node.getTelemetry = function getTelemetry() {
|
|
428
|
+
if (!node.config.telemetry) {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
if (node.telemetry || node.telemetryUnavailable) {
|
|
432
|
+
return node.telemetry;
|
|
433
|
+
}
|
|
434
|
+
let BullMQOtel;
|
|
435
|
+
try {
|
|
436
|
+
({ BullMQOtel } = require("bullmq-otel"));
|
|
437
|
+
} catch (err) {
|
|
438
|
+
node.telemetryUnavailable = true;
|
|
439
|
+
node.error(
|
|
440
|
+
"BullMQ telemetry is enabled but bullmq-otel is not installed; run npm install bullmq-otel",
|
|
441
|
+
);
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
const serviceName =
|
|
445
|
+
node.config.telemetryServiceName || node.config.queueName;
|
|
446
|
+
node.telemetry = new BullMQOtel({
|
|
447
|
+
tracerName: serviceName,
|
|
448
|
+
meterName: serviceName,
|
|
449
|
+
version: PACKAGE_VERSION,
|
|
450
|
+
enableMetrics: node.config.telemetryMetrics,
|
|
451
|
+
});
|
|
452
|
+
return node.telemetry;
|
|
244
453
|
};
|
|
245
454
|
|
|
246
455
|
// owner is the node whose status should reflect connection errors. It
|
|
@@ -262,61 +471,182 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
262
471
|
await closeResourcePair(owner, connection);
|
|
263
472
|
};
|
|
264
473
|
|
|
474
|
+
// The four BullMQ owners differ only in which constructor they call and
|
|
475
|
+
// where that constructor takes the backend factory: Queue and QueueEvents
|
|
476
|
+
// take it 3rd, Worker 4th, FlowProducer 2nd. Everything around that is the
|
|
477
|
+
// same work, so `construct` receives the built options plus the factory to
|
|
478
|
+
// pass on -- undefined on Redis, where every one of those constructors
|
|
479
|
+
// defaults the parameter to BullMQ's own Redis factory.
|
|
480
|
+
//
|
|
481
|
+
// On PostgreSQL, BullMQ's factory validates the schema name and
|
|
482
|
+
// synchronously loads the optional `pg` module (createPostgresBackend ->
|
|
483
|
+
// new PostgresConnection) before any resource exists, so a bad schema name
|
|
484
|
+
// or a missing `pg` install throws out of `new Queue/Worker/QueueEvents/
|
|
485
|
+
// FlowProducer(...)` itself rather than through waitUntilReady(). Catch it
|
|
486
|
+
// here instead of crashing the owning node's constructor.
|
|
487
|
+
function createResource(role, owner, telemetry, construct, extraOptions) {
|
|
488
|
+
// BullMQ owns every PostgreSQL connection (a pool plus a dedicated
|
|
489
|
+
// LISTEN client per backend); this package builds none of its own on
|
|
490
|
+
// that path, so createConnection is skipped entirely.
|
|
491
|
+
const isPostgres = node.config.backend === "postgres";
|
|
492
|
+
const connection = isPostgres
|
|
493
|
+
? undefined
|
|
494
|
+
: node.createConnection(role, owner);
|
|
495
|
+
const options = {
|
|
496
|
+
...buildBullMQOptions(
|
|
497
|
+
node.config,
|
|
498
|
+
isPostgres ? node.config.postgres : connection,
|
|
499
|
+
telemetry,
|
|
500
|
+
role,
|
|
501
|
+
),
|
|
502
|
+
...extraOptions,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
let resource;
|
|
506
|
+
if (isPostgres) {
|
|
507
|
+
try {
|
|
508
|
+
resource = construct(options, createPostgresBackend);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
publishBackendStatus("disconnected");
|
|
511
|
+
node.reportBackendFailure(err);
|
|
512
|
+
return { resource: undefined, connection };
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
resource = construct(options);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// On postgres there is no connection of our own -- the owner is tracked
|
|
519
|
+
// with no value so close/redeploy still walks it, but has nothing raw to
|
|
520
|
+
// close.
|
|
521
|
+
node.resources.set(resource, connection);
|
|
522
|
+
return { resource, connection };
|
|
523
|
+
}
|
|
524
|
+
|
|
265
525
|
node.getQueue = function getQueue() {
|
|
266
526
|
if (!node.queue) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
node.
|
|
271
|
-
|
|
527
|
+
const { resource, connection } = createResource(
|
|
528
|
+
"producer",
|
|
529
|
+
node,
|
|
530
|
+
node.getTelemetry(),
|
|
531
|
+
(options, factory) =>
|
|
532
|
+
new Queue(node.config.queueName, options, factory),
|
|
533
|
+
// Queue-level auto-removal. Without it BullMQ keeps every completed
|
|
534
|
+
// and failed job forever, which is the unbounded growth its
|
|
535
|
+
// production guide warns about. Per-job msg.jobopts still wins.
|
|
536
|
+
node.config.defaultJobOptions
|
|
537
|
+
? { defaultJobOptions: node.config.defaultJobOptions }
|
|
538
|
+
: undefined,
|
|
272
539
|
);
|
|
273
|
-
|
|
540
|
+
if (!resource) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
node.queue = resource;
|
|
544
|
+
node.producerConnection = connection || null;
|
|
545
|
+
node.watchBackend(node.queue.getBackend());
|
|
274
546
|
attachErrorListener(node.queue, node);
|
|
275
547
|
}
|
|
276
548
|
return node.queue;
|
|
277
549
|
};
|
|
278
550
|
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
551
|
+
// Live reachability of the shared backend, owned here rather than read by
|
|
552
|
+
// each bullmq cmd node. waitUntilReady() cannot answer "is it reachable
|
|
553
|
+
// now": RedisQueueBackend awaits connection.client, which returns the
|
|
554
|
+
// promise built once in the constructor, and PostgresConnection memoizes
|
|
555
|
+
// readyPromise the same way. So a node deployed after an outage began would
|
|
556
|
+
// see that resolved promise and paint a false "connected". The backend's
|
|
557
|
+
// ready/error/close events are live, so they own the state after the first
|
|
558
|
+
// observation and every reader sees the current value.
|
|
559
|
+
node.backendStatus = "connecting";
|
|
560
|
+
node.backendReaders = new Set();
|
|
561
|
+
|
|
562
|
+
function publishBackendStatus(status) {
|
|
563
|
+
node.backendStatus = status;
|
|
564
|
+
for (const read of node.backendReaders) {
|
|
565
|
+
read(status);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
node.watchBackend = function watchBackend(backend) {
|
|
570
|
+
backend.on("ready", () => publishBackendStatus("connected"));
|
|
571
|
+
backend.on("error", () => publishBackendStatus("disconnected"));
|
|
572
|
+
backend.on("close", () => publishBackendStatus("disconnected"));
|
|
573
|
+
// Seed the first observation once, for the whole config node. An async
|
|
574
|
+
// IIFE rather than a promise chain (forbidden in this file) so it never
|
|
575
|
+
// blocks deploy, and it must not overwrite a live event that already
|
|
576
|
+
// told us more than this memoized promise can.
|
|
577
|
+
(async () => {
|
|
578
|
+
try {
|
|
579
|
+
await backend.waitUntilReady();
|
|
580
|
+
if (node.backendStatus === "connecting") {
|
|
581
|
+
publishBackendStatus("connected");
|
|
582
|
+
}
|
|
583
|
+
} catch (err) {
|
|
584
|
+
// Do NOT assume an event already covered this. PostgresConnection's
|
|
585
|
+
// bootstrap() rejects on connect, auth, migration, or schema failure
|
|
586
|
+
// and emits nothing (its emitError only forwards idle-pool and LISTEN
|
|
587
|
+
// errors, and only when a listener is already attached), and Queue's
|
|
588
|
+
// constructor swallows the same rejection. Without this the node
|
|
589
|
+
// would sit on "connecting" forever with nothing reported. The
|
|
590
|
+
// sibling bullmq events and bullmq flow nodes handle their own
|
|
591
|
+
// waitUntilReady() rejection through node.reportBackendFailure too,
|
|
592
|
+
// so the same failure seen from more than one resource still lands
|
|
593
|
+
// as one report.
|
|
594
|
+
if (node.backendStatus === "connecting") {
|
|
595
|
+
publishBackendStatus("disconnected");
|
|
596
|
+
node.reportBackendFailure(err);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
})();
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// Readers get the current status immediately, which is what makes a
|
|
603
|
+
// late-deployed node correct: the shared state is live, unlike the
|
|
604
|
+
// already-fired "ready" event it would otherwise have missed.
|
|
605
|
+
node.readBackendStatus = function readBackendStatus(read) {
|
|
606
|
+
node.backendReaders.add(read);
|
|
607
|
+
read(node.backendStatus);
|
|
608
|
+
return function stopReading() {
|
|
609
|
+
node.backendReaders.delete(read);
|
|
610
|
+
};
|
|
284
611
|
};
|
|
285
612
|
|
|
286
613
|
// Runtime nodes pass themselves as owner and attach their own resource
|
|
287
614
|
// error listener, so worker/events/flow errors surface on the visible
|
|
288
615
|
// runtime node rather than the hidden config node.
|
|
289
616
|
node.createWorker = function createWorker(processor, options, owner = node) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
617
|
+
return createResource(
|
|
618
|
+
"worker",
|
|
619
|
+
owner,
|
|
620
|
+
node.getTelemetry(),
|
|
621
|
+
(workerOptions, factory) =>
|
|
622
|
+
new Worker(node.config.queueName, processor, workerOptions, factory),
|
|
623
|
+
options,
|
|
624
|
+
).resource;
|
|
297
625
|
};
|
|
298
626
|
|
|
299
627
|
node.createQueueEvents = function createQueueEvents(owner = node) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
628
|
+
// QueueEvents deliberately gets no telemetry instance; see docs/TELEMETRY.md.
|
|
629
|
+
return createResource(
|
|
630
|
+
"events",
|
|
631
|
+
owner,
|
|
632
|
+
undefined,
|
|
633
|
+
(options, factory) =>
|
|
634
|
+
new QueueEvents(node.config.queueName, options, factory),
|
|
635
|
+
).resource;
|
|
307
636
|
};
|
|
308
637
|
|
|
309
638
|
node.createFlowProducer = function createFlowProducer(owner = node) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
639
|
+
return createResource(
|
|
640
|
+
"producer",
|
|
641
|
+
owner,
|
|
642
|
+
node.getTelemetry(),
|
|
643
|
+
(options, factory) => new FlowProducer(options, factory),
|
|
644
|
+
).resource;
|
|
316
645
|
};
|
|
317
646
|
|
|
318
647
|
node.on("close", async function onClose(removed, done) {
|
|
319
648
|
try {
|
|
649
|
+
node.backendFailures.clear();
|
|
320
650
|
const resources = Array.from(node.resources.entries()).reverse();
|
|
321
651
|
node.resources.clear();
|
|
322
652
|
await Promise.all(
|
|
@@ -332,7 +662,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
332
662
|
});
|
|
333
663
|
}
|
|
334
664
|
|
|
335
|
-
RED.nodes.registerType("
|
|
665
|
+
RED.nodes.registerType("bullmq-queue-server", BullQueueServerSetup, {
|
|
336
666
|
credentials: {
|
|
337
667
|
password: { type: "password" },
|
|
338
668
|
sentinelPassword: { type: "password" },
|
|
@@ -350,32 +680,44 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
350
680
|
|
|
351
681
|
if (!node.bullConn) {
|
|
352
682
|
node.status({ fill: "red", shape: "ring", text: "missing queue" });
|
|
353
|
-
node.error("Missing
|
|
683
|
+
node.error("Missing bullmq-queue-server config node");
|
|
354
684
|
return;
|
|
355
685
|
}
|
|
356
686
|
|
|
357
|
-
node.
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
687
|
+
// Mirror the config node's live view of the shared backend. Reading the
|
|
688
|
+
// shared state rather than subscribing to the backend directly keeps the
|
|
689
|
+
// listener count on the backend constant no matter how many bullmq cmd
|
|
690
|
+
// nodes a flow has, and gives a node deployed mid-outage the current
|
|
691
|
+
// status instead of a stale one.
|
|
692
|
+
const applyBackendStatus = (status) => {
|
|
693
|
+
if (status === "connected") {
|
|
694
|
+
setConnected(node);
|
|
695
|
+
} else if (status === "disconnected") {
|
|
696
|
+
setDisconnected(node);
|
|
697
|
+
} else {
|
|
698
|
+
setConnecting(node);
|
|
699
|
+
}
|
|
366
700
|
};
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
701
|
+
// Create the shared queue up front, as this node has always done, so the
|
|
702
|
+
// status reflects real reachability instead of a static "configured" the
|
|
703
|
+
// moment the flow deploys. This is the call that builds the queue, its
|
|
704
|
+
// connection, and (once PostgreSQL is wired) its pool; reading the status
|
|
705
|
+
// afterwards only subscribes.
|
|
706
|
+
node.bullConn.getQueue();
|
|
707
|
+
const stopReadingBackend =
|
|
708
|
+
node.bullConn.readBackendStatus(applyBackendStatus);
|
|
376
709
|
node.on("input", async function onInput(msg, send, done) {
|
|
377
710
|
try {
|
|
378
|
-
const
|
|
711
|
+
const queue = node.bullConn.getQueue();
|
|
712
|
+
if (!queue) {
|
|
713
|
+
// Construction failed synchronously (missing pg, bad schema name).
|
|
714
|
+
// The config node already reported why; say something useful here
|
|
715
|
+
// rather than letting queue.add() throw a bare TypeError per message.
|
|
716
|
+
throw new Error(
|
|
717
|
+
"BullMQ queue is unavailable; see the queue configuration node's error",
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const result = await dispatchCommand(queue, msg);
|
|
379
721
|
msg.payload = result;
|
|
380
722
|
nodeSend(node, send, msg);
|
|
381
723
|
nodeDone(node, done);
|
|
@@ -385,10 +727,8 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
385
727
|
});
|
|
386
728
|
|
|
387
729
|
node.on("close", function onClose(removed, done) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
node.bullConn.deregister(node, done);
|
|
730
|
+
stopReadingBackend();
|
|
731
|
+
done();
|
|
392
732
|
});
|
|
393
733
|
}
|
|
394
734
|
|
|
@@ -401,14 +741,17 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
401
741
|
|
|
402
742
|
if (!node.bullQueue) {
|
|
403
743
|
node.status({ fill: "red", shape: "ring", text: "missing queue" });
|
|
404
|
-
node.error("Missing
|
|
744
|
+
node.error("Missing bullmq-queue-server config node");
|
|
405
745
|
return;
|
|
406
746
|
}
|
|
407
747
|
|
|
408
|
-
node.bullQueue.register(node);
|
|
409
|
-
|
|
410
748
|
const workerOptions = {
|
|
411
749
|
concurrency: parsePositiveInteger(n.concurrency, 1, "Concurrency"),
|
|
750
|
+
maxStartedAttempts: parsePositiveInteger(
|
|
751
|
+
n.maxStartedAttempts,
|
|
752
|
+
100,
|
|
753
|
+
"Max Started Attempts"
|
|
754
|
+
),
|
|
412
755
|
};
|
|
413
756
|
const hasLimiterMax = isPresent(n.limiterMax);
|
|
414
757
|
const hasLimiterDuration = isPresent(n.limiterDuration);
|
|
@@ -425,8 +768,21 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
425
768
|
),
|
|
426
769
|
};
|
|
427
770
|
}
|
|
771
|
+
if (node.bullQueue.config.backend === "postgres") {
|
|
772
|
+
// A permanent readiness error (for example an unmigrated schema) makes
|
|
773
|
+
// BullMQ's autorun loop retry without delay. Start only after the one
|
|
774
|
+
// readiness promise succeeds so a configuration error cannot spin the
|
|
775
|
+
// Node-RED runtime.
|
|
776
|
+
workerOptions.autorun = false;
|
|
777
|
+
}
|
|
428
778
|
|
|
429
|
-
|
|
779
|
+
// Arity 3 tells BullMQ to create and track a per-job AbortController
|
|
780
|
+
// (worker.js: processorAcceptsSignal = processor.length >= 3), which is
|
|
781
|
+
// what makes cancelJob/cancelAllJobs able to reach this job at all.
|
|
782
|
+
// The token is the job's lock: bullmq job needs it for moveToWait and
|
|
783
|
+
// moveToDelayed, and it stays in the acknowledgement registry -- never in
|
|
784
|
+
// a message.
|
|
785
|
+
const processor = async (job, token, signal) => {
|
|
430
786
|
if (node.completionMode === "manual") {
|
|
431
787
|
const timeoutMs = parseAckTimeoutMs(n.ackTimeout);
|
|
432
788
|
const acknowledgement = acknowledgements.create(
|
|
@@ -435,6 +791,9 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
435
791
|
queue: node.bullQueue.getQueue(),
|
|
436
792
|
queueName: node.bullQueue.config.queueName,
|
|
437
793
|
runNodeId: node.id,
|
|
794
|
+
worker: node.worker,
|
|
795
|
+
signal,
|
|
796
|
+
token,
|
|
438
797
|
},
|
|
439
798
|
timeoutMs
|
|
440
799
|
);
|
|
@@ -453,8 +812,52 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
453
812
|
};
|
|
454
813
|
|
|
455
814
|
node.worker = node.bullQueue.createWorker(processor, workerOptions, node);
|
|
456
|
-
|
|
457
|
-
|
|
815
|
+
// createWorker reports and returns undefined on a synchronous backend
|
|
816
|
+
// construction failure (e.g. postgres selected with pg not installed);
|
|
817
|
+
// nothing was created, so there is nothing to close on this node's own
|
|
818
|
+
// close, and no listener setup below would have anything to attach to.
|
|
819
|
+
if (!node.worker) {
|
|
820
|
+
setDisconnected(node);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const workerStartupFailureOwner =
|
|
824
|
+
node.bullQueue.config.backend === "postgres" ? node.bullQueue : undefined;
|
|
825
|
+
const markWorkerReady = attachErrorListener(
|
|
826
|
+
node.worker,
|
|
827
|
+
node,
|
|
828
|
+
workerStartupFailureOwner,
|
|
829
|
+
);
|
|
830
|
+
if (workerStartupFailureOwner) {
|
|
831
|
+
let workerReady = false;
|
|
832
|
+
async function startPostgresWorker() {
|
|
833
|
+
try {
|
|
834
|
+
await node.worker.waitUntilReady();
|
|
835
|
+
if (node.worker.closing) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
workerReady = true;
|
|
839
|
+
markWorkerReady();
|
|
840
|
+
setConnected(node);
|
|
841
|
+
await node.worker.run();
|
|
842
|
+
} catch (err) {
|
|
843
|
+
if (node.worker.closing) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
setDisconnected(node);
|
|
847
|
+
if (workerReady) {
|
|
848
|
+
node.error(err);
|
|
849
|
+
} else {
|
|
850
|
+
workerStartupFailureOwner.reportBackendFailure(err);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
void startPostgresWorker();
|
|
855
|
+
} else {
|
|
856
|
+
node.worker.on("ready", () => {
|
|
857
|
+
markWorkerReady();
|
|
858
|
+
setConnected(node);
|
|
859
|
+
});
|
|
860
|
+
}
|
|
458
861
|
node.worker.on("closed", () => setDisconnected(node));
|
|
459
862
|
setConnecting(node);
|
|
460
863
|
|
|
@@ -465,7 +868,6 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
465
868
|
);
|
|
466
869
|
try {
|
|
467
870
|
await node.bullQueue.releaseResource(node.worker);
|
|
468
|
-
node.bullQueue.deregister(node, () => {});
|
|
469
871
|
done();
|
|
470
872
|
} catch (err) {
|
|
471
873
|
done(err);
|
|
@@ -543,8 +945,63 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
543
945
|
context.fail(Worker.RateLimitError());
|
|
544
946
|
nodeDone(node, done);
|
|
545
947
|
return;
|
|
948
|
+
// Step/retry transitions. Each hands the job's lock token back to
|
|
949
|
+
// BullMQ and then settles the acknowledgement with the error class
|
|
950
|
+
// the worker special-cases, so the job is NOT moved to failed
|
|
951
|
+
// (worker.js checks DelayedError and WaitingError).
|
|
952
|
+
case "moveToWait": {
|
|
953
|
+
await context.job.moveToWait(context.token);
|
|
954
|
+
context.fail(new WaitingError());
|
|
955
|
+
msg.payload = true;
|
|
956
|
+
nodeSend(node, send, msg);
|
|
957
|
+
nodeDone(node, done);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
case "moveToDelayed": {
|
|
961
|
+
const delay = Number(msg.delay);
|
|
962
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
963
|
+
throw new Error(
|
|
964
|
+
"moveToDelayed requires msg.delay in milliseconds"
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
await context.job.moveToDelayed(Date.now() + delay, context.token);
|
|
968
|
+
context.fail(new DelayedError());
|
|
969
|
+
msg.payload = true;
|
|
970
|
+
nodeSend(node, send, msg);
|
|
971
|
+
nodeDone(node, done);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
case "updateData":
|
|
975
|
+
await context.job.updateData(
|
|
976
|
+
Object.hasOwn(msg, "jobData") ? msg.jobData : msg.payload
|
|
977
|
+
);
|
|
978
|
+
msg.payload = serializeJob(context.job);
|
|
979
|
+
nodeSend(node, send, msg);
|
|
980
|
+
nodeDone(node, done);
|
|
981
|
+
return;
|
|
982
|
+
case "cancelJob": {
|
|
983
|
+
const reason = msg.reason || "BullMQ job cancelled";
|
|
984
|
+
const cancelled = context.worker.cancelJob(context.job.id, reason);
|
|
985
|
+
if (!cancelled) {
|
|
986
|
+
throw new Error(
|
|
987
|
+
`BullMQ found no cancellable processor for job ${context.job.id}`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
msg.payload = cancelled;
|
|
991
|
+
nodeSend(node, send, msg);
|
|
992
|
+
nodeDone(node, done);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
case "cancelAllJobs": {
|
|
996
|
+
const reason = msg.reason || "BullMQ job cancelled";
|
|
997
|
+
context.worker.cancelAllJobs(reason);
|
|
998
|
+
msg.payload = true;
|
|
999
|
+
nodeSend(node, send, msg);
|
|
1000
|
+
nodeDone(node, done);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
546
1003
|
default:
|
|
547
|
-
throw new Error(`Unsupported
|
|
1004
|
+
throw new Error(`Unsupported bullmq job action: ${action}`);
|
|
548
1005
|
}
|
|
549
1006
|
} catch (err) {
|
|
550
1007
|
nodeDone(node, done, err, msg);
|
|
@@ -560,13 +1017,23 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
560
1017
|
|
|
561
1018
|
if (!node.bullConn) {
|
|
562
1019
|
node.status({ fill: "red", shape: "ring", text: "missing queue" });
|
|
563
|
-
node.error("Missing
|
|
1020
|
+
node.error("Missing bullmq-queue-server config node");
|
|
564
1021
|
return;
|
|
565
1022
|
}
|
|
566
1023
|
|
|
567
|
-
node.bullConn.register(node);
|
|
568
1024
|
node.queueEvents = node.bullConn.createQueueEvents(node);
|
|
569
|
-
|
|
1025
|
+
if (!node.queueEvents) {
|
|
1026
|
+
// Construction failed synchronously and already reported why.
|
|
1027
|
+
setDisconnected(node);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const eventsStartupFailureOwner =
|
|
1031
|
+
node.bullConn.config.backend === "postgres" ? node.bullConn : undefined;
|
|
1032
|
+
const markQueueEventsReady = attachErrorListener(
|
|
1033
|
+
node.queueEvents,
|
|
1034
|
+
node,
|
|
1035
|
+
eventsStartupFailureOwner,
|
|
1036
|
+
);
|
|
570
1037
|
const events = parseEventFilter(n.events);
|
|
571
1038
|
for (const event of events) {
|
|
572
1039
|
node.queueEvents.on(event, (payload, eventId) => {
|
|
@@ -585,10 +1052,15 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
585
1052
|
try {
|
|
586
1053
|
setConnecting(node);
|
|
587
1054
|
await node.queueEvents.waitUntilReady();
|
|
1055
|
+
markQueueEventsReady();
|
|
588
1056
|
setConnected(node);
|
|
589
1057
|
} catch (err) {
|
|
590
1058
|
setDisconnected(node);
|
|
591
|
-
|
|
1059
|
+
if (eventsStartupFailureOwner) {
|
|
1060
|
+
eventsStartupFailureOwner.reportBackendFailure(err);
|
|
1061
|
+
} else {
|
|
1062
|
+
node.error(err);
|
|
1063
|
+
}
|
|
592
1064
|
}
|
|
593
1065
|
}
|
|
594
1066
|
updateReadyStatus();
|
|
@@ -596,7 +1068,6 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
596
1068
|
node.on("close", async function onClose(removed, done) {
|
|
597
1069
|
try {
|
|
598
1070
|
await node.bullConn.releaseResource(node.queueEvents);
|
|
599
|
-
node.bullConn.deregister(node, () => {});
|
|
600
1071
|
done();
|
|
601
1072
|
} catch (err) {
|
|
602
1073
|
done(err);
|
|
@@ -612,21 +1083,36 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
612
1083
|
|
|
613
1084
|
if (!node.bullConn) {
|
|
614
1085
|
node.status({ fill: "red", shape: "ring", text: "missing queue" });
|
|
615
|
-
node.error("Missing
|
|
1086
|
+
node.error("Missing bullmq-queue-server config node");
|
|
616
1087
|
return;
|
|
617
1088
|
}
|
|
618
1089
|
|
|
619
|
-
node.bullConn.register(node);
|
|
620
1090
|
node.flowProducer = node.bullConn.createFlowProducer(node);
|
|
621
|
-
|
|
1091
|
+
if (!node.flowProducer) {
|
|
1092
|
+
// Construction failed synchronously and already reported why.
|
|
1093
|
+
setDisconnected(node);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const flowStartupFailureOwner =
|
|
1097
|
+
node.bullConn.config.backend === "postgres" ? node.bullConn : undefined;
|
|
1098
|
+
const markFlowProducerReady = attachErrorListener(
|
|
1099
|
+
node.flowProducer,
|
|
1100
|
+
node,
|
|
1101
|
+
flowStartupFailureOwner,
|
|
1102
|
+
);
|
|
622
1103
|
async function updateReadyStatus() {
|
|
623
1104
|
try {
|
|
624
1105
|
setConnecting(node);
|
|
625
1106
|
await node.flowProducer.waitUntilReady();
|
|
1107
|
+
markFlowProducerReady();
|
|
626
1108
|
setConnected(node);
|
|
627
1109
|
} catch (err) {
|
|
628
1110
|
setDisconnected(node);
|
|
629
|
-
|
|
1111
|
+
if (flowStartupFailureOwner) {
|
|
1112
|
+
flowStartupFailureOwner.reportBackendFailure(err);
|
|
1113
|
+
} else {
|
|
1114
|
+
node.error(err);
|
|
1115
|
+
}
|
|
630
1116
|
}
|
|
631
1117
|
}
|
|
632
1118
|
updateReadyStatus();
|
|
@@ -634,11 +1120,29 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
634
1120
|
node.on("input", async function onInput(msg, send, done) {
|
|
635
1121
|
try {
|
|
636
1122
|
if (!msg.payload || typeof msg.payload !== "object") {
|
|
637
|
-
throw new Error(
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
"bullmq flow requires msg.payload to contain a flow tree, or an array of flow trees"
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
const retention = node.bullConn.config.defaultJobOptions;
|
|
1128
|
+
if (Array.isArray(msg.payload)) {
|
|
1129
|
+
// addBulk creates every tree or none of them, which is the whole
|
|
1130
|
+
// reason to use it instead of one add() per tree.
|
|
1131
|
+
if (msg.payload.length === 0) {
|
|
1132
|
+
throw new Error("bullmq flow requires at least one flow tree");
|
|
1133
|
+
}
|
|
1134
|
+
const trees = await node.flowProducer.addBulk(
|
|
1135
|
+
msg.payload.map((flow) => withBulkFlowJobDefaults(flow, retention))
|
|
1136
|
+
);
|
|
1137
|
+
msg.payload = trees.map(serializeFlowJob);
|
|
1138
|
+
} else {
|
|
1139
|
+
msg.payload = serializeFlowJob(
|
|
1140
|
+
await node.flowProducer.add(
|
|
1141
|
+
msg.payload,
|
|
1142
|
+
withFlowJobDefaults(msg.payload, msg.flowopts, retention)
|
|
1143
|
+
)
|
|
1144
|
+
);
|
|
638
1145
|
}
|
|
639
|
-
msg.payload = serializeFlowJob(
|
|
640
|
-
await node.flowProducer.add(msg.payload, msg.flowopts)
|
|
641
|
-
);
|
|
642
1146
|
nodeSend(node, send, msg);
|
|
643
1147
|
nodeDone(node, done);
|
|
644
1148
|
} catch (err) {
|
|
@@ -649,7 +1153,6 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
649
1153
|
node.on("close", async function onClose(removed, done) {
|
|
650
1154
|
try {
|
|
651
1155
|
await node.bullConn.releaseResource(node.flowProducer);
|
|
652
|
-
node.bullConn.deregister(node, () => {});
|
|
653
1156
|
done();
|
|
654
1157
|
} catch (err) {
|
|
655
1158
|
done(err);
|
|
@@ -657,9 +1160,9 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
657
1160
|
});
|
|
658
1161
|
}
|
|
659
1162
|
|
|
660
|
-
RED.nodes.registerType("
|
|
661
|
-
RED.nodes.registerType("
|
|
662
|
-
RED.nodes.registerType("
|
|
663
|
-
RED.nodes.registerType("
|
|
664
|
-
RED.nodes.registerType("
|
|
1163
|
+
RED.nodes.registerType("bullmq cmd", BullQueueCmdNode);
|
|
1164
|
+
RED.nodes.registerType("bullmq run", BullQueueRunNode);
|
|
1165
|
+
RED.nodes.registerType("bullmq job", BullJobNode);
|
|
1166
|
+
RED.nodes.registerType("bullmq events", BullEventsNode);
|
|
1167
|
+
RED.nodes.registerType("bullmq flow", BullFlowNode);
|
|
665
1168
|
};
|