@pauldeng/node-red-contrib-bullmq 1.0.1 → 1.0.3
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 +13 -12
- package/bull-queue.html +65 -24
- package/bull-queue.js +78 -38
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/COMMANDS.md +3 -1
- package/docs/CONNECTIONS.md +16 -6
- package/docs/MIGRATION.md +1 -1
- package/docs/NODE_GUIDE.md +5 -2
- package/docs/REFERENCE_MAP.md +2 -0
- package/docs/RELEASE.md +2 -2
- package/docs/TESTING.md +2 -2
- package/docs/TROUBLESHOOTING.md +1 -1
- package/lib/acknowledgements.js +5 -4
- package/lib/commands.js +9 -4
- package/lib/connections.js +65 -23
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
Node-RED nodes for BullMQ-backed Redis job queues.
|
|
10
10
|
|
|
11
|
-
This package targets BullMQ 5.
|
|
11
|
+
This package targets BullMQ 5.80.9 and Node-RED 4.1 or 5.x. It preserves the legacy `bull-queue-server`, `bull cmd`, and `bull run` node types where BullMQ has compatible behavior, and adds `bull job`, `bull events`, and `bull flow`.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
+
To install - either use the manage palette option in the editor, or change to your Node-RED user directory.
|
|
16
|
+
|
|
15
17
|
```sh
|
|
18
|
+
cd ~/.node-red
|
|
16
19
|
npm install @pauldeng/node-red-contrib-bullmq
|
|
17
20
|
```
|
|
18
21
|
|
|
@@ -23,7 +26,7 @@ Repository: <https://github.com/pauldeng/node-red-contrib-bullmq>
|
|
|
23
26
|
- Node-RED 4.1.x or 5.x
|
|
24
27
|
- Node.js 18+ with Node-RED 4.1.x, or Node.js 22.9+ with Node-RED 5.x
|
|
25
28
|
- Redis with `maxmemory-policy=noeviction`
|
|
26
|
-
- BullMQ 5.
|
|
29
|
+
- BullMQ 5.80.9
|
|
27
30
|
|
|
28
31
|
Bull v4 Redis data is not automatically migrated. Drain, retire, or otherwise handle old Bull queues before upgrading the runtime dependency.
|
|
29
32
|
|
|
@@ -45,7 +48,7 @@ Supported deployment modes:
|
|
|
45
48
|
- AWS MemoryDB, configured as Redis Cluster with TLS
|
|
46
49
|
- Redis Sentinel
|
|
47
50
|
|
|
48
|
-
Authentication can use Redis ACL username/password. TLS supports CA, client certificate, client key, server name, and certificate verification. Cluster and MemoryDB
|
|
51
|
+
Authentication can use Redis ACL username/password. TLS supports CA, client certificate, client key, server name, and certificate verification. Cluster and MemoryDB prefixes must contain a hash tag, such as `{bull}`, to keep queue keys in one Redis Cluster slot for atomic operations.
|
|
49
52
|
|
|
50
53
|
## Legacy Repeat Cron Compatibility
|
|
51
54
|
|
|
@@ -63,11 +66,11 @@ msg.jobopts = {
|
|
|
63
66
|
return msg;
|
|
64
67
|
```
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
When adding a legacy repeat job, the scheduler id is `msg.schedulerId` when present, otherwise `msg.jobopts.jobId`. `repeat.cron` is translated to `repeat.pattern`; conflicting `cron` and `pattern` values are rejected. Lookup and removal commands require the exact scheduler id in `msg.schedulerId`, `msg.jobid`, or `msg.jobId`.
|
|
67
70
|
|
|
68
71
|
## Commands
|
|
69
72
|
|
|
70
|
-
`bull cmd` reads `msg.cmd`. The default command is `add`.
|
|
73
|
+
`bull cmd` reads `msg.cmd`. The legacy `msg.command` alias is also accepted, but new flows should use `msg.cmd`. The default command is `add`.
|
|
71
74
|
|
|
72
75
|
Core supported command families include:
|
|
73
76
|
|
|
@@ -95,14 +98,11 @@ See [docs/COMMANDS.md](docs/COMMANDS.md).
|
|
|
95
98
|
|
|
96
99
|
## Examples
|
|
97
100
|
|
|
98
|
-
Import
|
|
101
|
+
Import any of these flows into Node-RED:
|
|
99
102
|
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- manual acknowledgement
|
|
104
|
-
- QueueEvents
|
|
105
|
-
- parent/child flow producer
|
|
103
|
+
- [examples/example_flow.json](examples/example_flow.json): an end-to-end flow with add/run, a legacy scheduler compatibility case, delayed and prioritized jobs, manual acknowledgement, QueueEvents, and a parent/child flow.
|
|
104
|
+
- [examples/bullmq_features.json](examples/bullmq_features.json): focused examples of common BullMQ features.
|
|
105
|
+
- [examples/repeatable_jobs.json](examples/repeatable_jobs.json): legacy repeat-command and Job Scheduler compatibility examples.
|
|
106
106
|
|
|
107
107
|
The examples do not contain secrets.
|
|
108
108
|
|
|
@@ -122,3 +122,4 @@ Use [docs/TESTING.md](docs/TESTING.md) for Docker, Playwright, and MemoryDB test
|
|
|
122
122
|
- [Connection Guide](docs/CONNECTIONS.md)
|
|
123
123
|
- [Migration Guide](docs/MIGRATION.md)
|
|
124
124
|
- [Troubleshooting](docs/TROUBLESHOOTING.md)
|
|
125
|
+
- [Changelog](CHANGELOG.md)
|
package/bull-queue.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script type="text/html" data-template-name="bull-queue-server">
|
|
2
2
|
<div class="form-row">
|
|
3
3
|
<label for="node-config-input-name"><i class="fa fa-tasks"></i> Queue</label>
|
|
4
|
-
<input type="text" id="node-config-input-name" placeholder="
|
|
4
|
+
<input type="text" id="node-config-input-name" placeholder="email-jobs">
|
|
5
5
|
</div>
|
|
6
6
|
<div class="form-row">
|
|
7
7
|
<label for="node-config-input-deployment"><i class="fa fa-server"></i> Deployment</label>
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<label for="node-config-input-sentinelMasterName"><i class="fa fa-tag"></i> Master</label>
|
|
32
32
|
<input type="text" id="node-config-input-sentinelMasterName" placeholder="mymaster">
|
|
33
33
|
</div>
|
|
34
|
-
<div class="form-row">
|
|
34
|
+
<div class="form-row bull-db-row">
|
|
35
35
|
<label for="node-config-input-db"><i class="fa fa-database"></i> Database</label>
|
|
36
36
|
<input type="number" id="node-config-input-db" placeholder="0">
|
|
37
37
|
</div>
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
$(".bull-single-row").toggle(deployment === "single");
|
|
96
96
|
$(".bull-cluster-row").toggle(deployment === "cluster");
|
|
97
97
|
$(".bull-sentinel-row").toggle(deployment === "sentinel");
|
|
98
|
+
$(".bull-db-row").toggle(deployment !== "cluster");
|
|
98
99
|
$(".bull-tls-row").toggle($("#node-config-input-tls").is(":checked"));
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -161,7 +162,7 @@
|
|
|
161
162
|
<li><b>CA</b>: PEM certificate authority text for validating private Redis certificates. Stored as a Node-RED credential.</li>
|
|
162
163
|
<li><b>Client Cert</b>: PEM client certificate for mutual TLS deployments. Stored as a Node-RED credential.</li>
|
|
163
164
|
<li><b>Client Key</b>: PEM client private key for mutual TLS deployments. Stored as a Node-RED credential.</li>
|
|
164
|
-
<li><b>Prefix</b>: BullMQ key prefix.
|
|
165
|
+
<li><b>Prefix</b>: BullMQ key prefix. Redis Cluster and MemoryDB prefixes must contain a hash tag such as <code>{bull}</code> so BullMQ keys stay in one hash slot for atomic operations.</li>
|
|
165
166
|
</ul>
|
|
166
167
|
|
|
167
168
|
<h3>Example</h3>
|
|
@@ -269,7 +270,7 @@ return msg;</pre>
|
|
|
269
270
|
<option value="manual">Manual acknowledgement</option>
|
|
270
271
|
</select>
|
|
271
272
|
</div>
|
|
272
|
-
<div class="form-row">
|
|
273
|
+
<div class="form-row bull-ack-timeout-row">
|
|
273
274
|
<label for="node-input-ackTimeout"><i class="fa fa-clock-o"></i> Ack Timeout</label>
|
|
274
275
|
<input type="number" id="node-input-ackTimeout" placeholder="300000">
|
|
275
276
|
</div>
|
|
@@ -315,26 +316,62 @@ return msg;</pre>
|
|
|
315
316
|
</script>
|
|
316
317
|
|
|
317
318
|
<script type="text/javascript">
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
queue: { type: "bull-queue-server", required: true },
|
|
324
|
-
completionMode: { value: "immediate" },
|
|
325
|
-
ackTimeout: { value: 300000, validate: RED.validators.number() },
|
|
326
|
-
concurrency: { value: 1, validate: RED.validators.number() },
|
|
327
|
-
limiterMax: { value: "" },
|
|
328
|
-
limiterDuration: { value: "" }
|
|
329
|
-
},
|
|
330
|
-
inputs: 0,
|
|
331
|
-
outputs: 1,
|
|
332
|
-
align: "left",
|
|
333
|
-
icon: "bull_icon.png",
|
|
334
|
-
label: function() {
|
|
335
|
-
return this.name || "bull run";
|
|
319
|
+
(function() {
|
|
320
|
+
function updateBullRunRows() {
|
|
321
|
+
$(".bull-ack-timeout-row").toggle(
|
|
322
|
+
$("#node-input-completionMode").val() === "manual"
|
|
323
|
+
);
|
|
336
324
|
}
|
|
337
|
-
|
|
325
|
+
|
|
326
|
+
function positiveInteger(value) {
|
|
327
|
+
var parsed = Number(value);
|
|
328
|
+
return value !== "" && Number.isInteger(parsed) && parsed > 0;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function optionalPositivePair(value, otherProperty) {
|
|
332
|
+
var input = $("#node-input-" + otherProperty);
|
|
333
|
+
var other = input.length ? input.val() : this[otherProperty];
|
|
334
|
+
return (
|
|
335
|
+
((value === "" || value == null) && (other === "" || other == null)) ||
|
|
336
|
+
(positiveInteger(value) && positiveInteger(other))
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
RED.nodes.registerType("bull run", {
|
|
341
|
+
color: "#ffffff",
|
|
342
|
+
category: "function",
|
|
343
|
+
defaults: {
|
|
344
|
+
name: { value: "" },
|
|
345
|
+
queue: { type: "bull-queue-server", required: true },
|
|
346
|
+
completionMode: { value: "immediate" },
|
|
347
|
+
ackTimeout: { value: 300000, validate: RED.validators.number() },
|
|
348
|
+
concurrency: { value: 1, validate: positiveInteger },
|
|
349
|
+
limiterMax: {
|
|
350
|
+
value: "",
|
|
351
|
+
validate: function(value) {
|
|
352
|
+
return optionalPositivePair.call(this, value, "limiterDuration");
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
limiterDuration: {
|
|
356
|
+
value: "",
|
|
357
|
+
validate: function(value) {
|
|
358
|
+
return optionalPositivePair.call(this, value, "limiterMax");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
inputs: 0,
|
|
363
|
+
outputs: 1,
|
|
364
|
+
align: "left",
|
|
365
|
+
icon: "bull_icon.png",
|
|
366
|
+
label: function() {
|
|
367
|
+
return this.name || "bull run";
|
|
368
|
+
},
|
|
369
|
+
oneditprepare: function() {
|
|
370
|
+
$("#node-input-completionMode").on("change", updateBullRunRows);
|
|
371
|
+
updateBullRunRows();
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
})();
|
|
338
375
|
</script>
|
|
339
376
|
|
|
340
377
|
<script type="text/html" data-template-name="bull job">
|
|
@@ -386,7 +423,11 @@ return msg;</pre>
|
|
|
386
423
|
<pre>msg.cmd = "progress";
|
|
387
424
|
msg.progress = 50;
|
|
388
425
|
return msg;</pre>
|
|
389
|
-
<p>
|
|
426
|
+
<p>To use another <code>bull job</code> node with configured <b>Action</b> <code>complete</code>, pass the progress output through a Function node that removes the per-message override:</p>
|
|
427
|
+
<pre>delete msg.cmd;
|
|
428
|
+
msg.payload = { ok: true };
|
|
429
|
+
return msg;</pre>
|
|
430
|
+
<p>Alternatively, override the downstream action explicitly:</p>
|
|
390
431
|
<pre>msg.cmd = "complete";
|
|
391
432
|
msg.payload = { ok: true };
|
|
392
433
|
return msg;</pre>
|
package/bull-queue.js
CHANGED
|
@@ -47,22 +47,22 @@ const DEFAULT_EVENTS = [
|
|
|
47
47
|
// an unreachable Redis server.
|
|
48
48
|
const CLOSE_GRACE_MS = 1000;
|
|
49
49
|
|
|
50
|
-
async function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} catch (err) {
|
|
56
|
-
// a failed graceful close still counts as settled
|
|
57
|
-
}
|
|
58
|
-
settled = true;
|
|
59
|
-
})();
|
|
60
|
-
|
|
61
|
-
const deadline = Date.now() + ms;
|
|
62
|
-
while (!settled && Date.now() < deadline) {
|
|
63
|
-
await sleep(25);
|
|
50
|
+
async function settled(promise) {
|
|
51
|
+
try {
|
|
52
|
+
await promise;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// a failed graceful close still counts as settled
|
|
64
55
|
}
|
|
65
|
-
return
|
|
56
|
+
return "settled";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function timedOut(ms) {
|
|
60
|
+
await sleep(ms);
|
|
61
|
+
return "timeout";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function settleWithin(promise, ms) {
|
|
65
|
+
return await Promise.race([settled(promise), timedOut(ms)]);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function disconnectClient(client) {
|
|
@@ -86,7 +86,7 @@ function forceDisconnect(resource) {
|
|
|
86
86
|
}
|
|
87
87
|
// BullMQ resource. Its public disconnect() awaits a connection promise that
|
|
88
88
|
// never settles while Redis is unreachable, so reach for the underlying
|
|
89
|
-
// ioredis clients directly (BullMQ is pinned to exactly 5.
|
|
89
|
+
// ioredis clients directly (BullMQ is pinned to exactly 5.80.9).
|
|
90
90
|
if (resource.connection) {
|
|
91
91
|
disconnectClient(resource.connection._client);
|
|
92
92
|
}
|
|
@@ -122,6 +122,23 @@ async function closeResource(resource) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
async function closeResourcePair(owner, connection) {
|
|
126
|
+
let firstError;
|
|
127
|
+
try {
|
|
128
|
+
await closeResource(owner);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
firstError = err;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await closeResource(connection);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
firstError ||= err;
|
|
136
|
+
}
|
|
137
|
+
if (firstError) {
|
|
138
|
+
throw firstError;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
function nodeSend(node, send, msg) {
|
|
126
143
|
(send || node.send).call(node, msg);
|
|
127
144
|
}
|
|
@@ -134,13 +151,17 @@ function nodeDone(node, done, err, msg) {
|
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
153
|
|
|
137
|
-
function
|
|
138
|
-
|
|
154
|
+
function isPresent(value) {
|
|
155
|
+
return value !== undefined && value !== null && value !== "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parsePositiveInteger(value, defaultValue, field = "Value") {
|
|
159
|
+
if (!isPresent(value)) {
|
|
139
160
|
return defaultValue;
|
|
140
161
|
}
|
|
141
162
|
const parsed = Number(value);
|
|
142
163
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
143
|
-
throw new Error(
|
|
164
|
+
throw new Error(`${field} must be a positive integer`);
|
|
144
165
|
}
|
|
145
166
|
return parsed;
|
|
146
167
|
}
|
|
@@ -203,7 +224,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
203
224
|
const node = this;
|
|
204
225
|
|
|
205
226
|
node.users = {};
|
|
206
|
-
node.resources = new
|
|
227
|
+
node.resources = new Map();
|
|
207
228
|
node.config = normalizeQueueConfig(n, node.credentials || {});
|
|
208
229
|
node.queue = null;
|
|
209
230
|
node.producerConnection = null;
|
|
@@ -229,17 +250,27 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
229
250
|
const descriptor = buildRedisDescriptor(node.config, role);
|
|
230
251
|
const connection = createRedisConnection(descriptor, IORedis);
|
|
231
252
|
attachErrorListener(connection, owner);
|
|
232
|
-
node.resources.add(connection);
|
|
233
253
|
return connection;
|
|
234
254
|
};
|
|
235
255
|
|
|
256
|
+
node.releaseResource = async function releaseResource(owner) {
|
|
257
|
+
if (!node.resources.has(owner)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const connection = node.resources.get(owner);
|
|
261
|
+
node.resources.delete(owner);
|
|
262
|
+
await closeResourcePair(owner, connection);
|
|
263
|
+
};
|
|
264
|
+
|
|
236
265
|
node.getQueue = function getQueue() {
|
|
237
266
|
if (!node.queue) {
|
|
238
267
|
node.producerConnection = node.createConnection("producer");
|
|
268
|
+
node.producerConnection.setMaxListeners(0);
|
|
239
269
|
node.queue = new Queue(
|
|
240
270
|
node.config.queueName,
|
|
241
271
|
buildBullMQOptions(node.config, node.producerConnection)
|
|
242
272
|
);
|
|
273
|
+
node.resources.set(node.queue, node.producerConnection);
|
|
243
274
|
attachErrorListener(node.queue, node);
|
|
244
275
|
}
|
|
245
276
|
return node.queue;
|
|
@@ -261,7 +292,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
261
292
|
...buildBullMQOptions(node.config, connection),
|
|
262
293
|
...options,
|
|
263
294
|
});
|
|
264
|
-
node.resources.
|
|
295
|
+
node.resources.set(worker, connection);
|
|
265
296
|
return worker;
|
|
266
297
|
};
|
|
267
298
|
|
|
@@ -271,7 +302,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
271
302
|
node.config.queueName,
|
|
272
303
|
buildBullMQOptions(node.config, connection)
|
|
273
304
|
);
|
|
274
|
-
node.resources.
|
|
305
|
+
node.resources.set(queueEvents, connection);
|
|
275
306
|
return queueEvents;
|
|
276
307
|
};
|
|
277
308
|
|
|
@@ -280,19 +311,19 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
280
311
|
const flowProducer = new FlowProducer(
|
|
281
312
|
buildBullMQOptions(node.config, connection)
|
|
282
313
|
);
|
|
283
|
-
node.resources.
|
|
314
|
+
node.resources.set(flowProducer, connection);
|
|
284
315
|
return flowProducer;
|
|
285
316
|
};
|
|
286
317
|
|
|
287
318
|
node.on("close", async function onClose(removed, done) {
|
|
288
319
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
const resources = Array.from(node.resources.entries()).reverse();
|
|
321
|
+
node.resources.clear();
|
|
322
|
+
await Promise.all(
|
|
323
|
+
resources.map(([owner, connection]) =>
|
|
324
|
+
closeResourcePair(owner, connection)
|
|
325
|
+
)
|
|
326
|
+
);
|
|
296
327
|
node.status({});
|
|
297
328
|
done();
|
|
298
329
|
} catch (err) {
|
|
@@ -377,12 +408,21 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
377
408
|
node.bullQueue.register(node);
|
|
378
409
|
|
|
379
410
|
const workerOptions = {
|
|
380
|
-
concurrency: parsePositiveInteger(n.concurrency, 1),
|
|
411
|
+
concurrency: parsePositiveInteger(n.concurrency, 1, "Concurrency"),
|
|
381
412
|
};
|
|
382
|
-
|
|
413
|
+
const hasLimiterMax = isPresent(n.limiterMax);
|
|
414
|
+
const hasLimiterDuration = isPresent(n.limiterDuration);
|
|
415
|
+
if (hasLimiterMax !== hasLimiterDuration) {
|
|
416
|
+
throw new Error("Limiter Max and Limiter Duration must be set together");
|
|
417
|
+
}
|
|
418
|
+
if (hasLimiterMax) {
|
|
383
419
|
workerOptions.limiter = {
|
|
384
|
-
max: parsePositiveInteger(n.limiterMax),
|
|
385
|
-
duration: parsePositiveInteger(
|
|
420
|
+
max: parsePositiveInteger(n.limiterMax, undefined, "Limiter Max"),
|
|
421
|
+
duration: parsePositiveInteger(
|
|
422
|
+
n.limiterDuration,
|
|
423
|
+
undefined,
|
|
424
|
+
"Limiter Duration"
|
|
425
|
+
),
|
|
386
426
|
};
|
|
387
427
|
}
|
|
388
428
|
|
|
@@ -424,7 +464,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
424
464
|
new Error("BullMQ run node closed before acknowledgement")
|
|
425
465
|
);
|
|
426
466
|
try {
|
|
427
|
-
await
|
|
467
|
+
await node.bullQueue.releaseResource(node.worker);
|
|
428
468
|
node.bullQueue.deregister(node, () => {});
|
|
429
469
|
done();
|
|
430
470
|
} catch (err) {
|
|
@@ -555,7 +595,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
555
595
|
|
|
556
596
|
node.on("close", async function onClose(removed, done) {
|
|
557
597
|
try {
|
|
558
|
-
await
|
|
598
|
+
await node.bullConn.releaseResource(node.queueEvents);
|
|
559
599
|
node.bullConn.deregister(node, () => {});
|
|
560
600
|
done();
|
|
561
601
|
} catch (err) {
|
|
@@ -608,7 +648,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
608
648
|
|
|
609
649
|
node.on("close", async function onClose(removed, done) {
|
|
610
650
|
try {
|
|
611
|
-
await
|
|
651
|
+
await node.bullConn.releaseResource(node.flowProducer);
|
|
612
652
|
node.bullConn.deregister(node, () => {});
|
|
613
653
|
done();
|
|
614
654
|
} catch (err) {
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -24,7 +24,9 @@ The runtime does Node-RED lifecycle work only: creating nodes, wiring input hand
|
|
|
24
24
|
- QueueEvents uses a dedicated connection;
|
|
25
25
|
- Cluster and MemoryDB use `{bull}` by default as the BullMQ prefix.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Each BullMQ owner (`Queue`, `Worker`, `QueueEvents`, or `FlowProducer`) is tracked with its owned ioredis connection. A runtime node releases its pair on redeploy; config-node shutdown closes independent pairs concurrently, always attempting the BullMQ owner before its raw connection.
|
|
28
|
+
|
|
29
|
+
Connection and resource errors are reported on the consuming runtime node's status (`bull run`, `bull events`, `bull flow`). The config node owns the shared queue and its producer connection. Each `bull cmd` mirrors that shared producer connection on its visible status, while Queue errors report on the config node.
|
|
28
30
|
|
|
29
31
|
Secrets are read from Node-RED credentials first, with legacy plain fields accepted only for backward compatibility.
|
|
30
32
|
|
package/docs/COMMANDS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Command Reference
|
|
2
2
|
|
|
3
|
-
`bull cmd` reads `msg.cmd`.
|
|
3
|
+
`bull cmd` reads `msg.cmd`. The `msg.command` field is a legacy alias; use `msg.cmd` in new flows. The node writes the command result to `msg.payload`.
|
|
4
4
|
|
|
5
5
|
## Jobs
|
|
6
6
|
|
|
@@ -82,6 +82,8 @@ Legacy lookup/removal is exact-id based.
|
|
|
82
82
|
- `getRateLimitTtl`
|
|
83
83
|
- `removeRateLimitKey`
|
|
84
84
|
|
|
85
|
+
`setGlobalRateLimit` requires `msg.max` and `msg.duration`; the documented `{ max, duration }` object in `msg.payload` is also accepted.
|
|
86
|
+
|
|
85
87
|
## Logs And Metrics
|
|
86
88
|
|
|
87
89
|
- `addJobLog`
|
package/docs/CONNECTIONS.md
CHANGED
|
@@ -8,15 +8,15 @@ Producer commands use bounded retries so Node-RED input handlers fail instead of
|
|
|
8
8
|
|
|
9
9
|
## Redis Cluster
|
|
10
10
|
|
|
11
|
-
Use deployment `cluster` and provide startup nodes as comma or newline
|
|
11
|
+
Use deployment `cluster` and provide startup nodes as a comma- or newline-separated endpoint list.
|
|
12
12
|
|
|
13
|
-
Cluster auth and TLS are applied through ioredis `redisOptions`. The runtime sets a DNS lookup passthrough for
|
|
13
|
+
Cluster auth and TLS are applied through ioredis `redisOptions`. The runtime always sets a DNS lookup passthrough for cluster discovery, including non-TLS deployments.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The BullMQ prefix must contain a Redis hash tag, normally `{bull}`. Untagged Cluster prefixes are rejected to prevent `CROSSSLOT` failures.
|
|
16
16
|
|
|
17
17
|
## AWS MemoryDB
|
|
18
18
|
|
|
19
|
-
Use deployment `cluster`.
|
|
19
|
+
Use deployment `cluster`. The legacy `memorydb` deployment value remains a compatibility alias for `cluster` in imported flows.
|
|
20
20
|
|
|
21
21
|
Typical settings:
|
|
22
22
|
|
|
@@ -41,14 +41,24 @@ Use deployment `sentinel` and configure:
|
|
|
41
41
|
|
|
42
42
|
Sentinel authentication is separate from Redis data-node authentication.
|
|
43
43
|
|
|
44
|
+
## Cluster And Sentinel Endpoint Lists
|
|
45
|
+
|
|
46
|
+
Cluster startup nodes and Sentinel discovery lists accept host names, `host:port`, bare IPv6 with the default port, bracketed IPv6 such as `[2001:db8::1]:6379`, and `redis://` or `rediss://` URLs. Explicit URL schemes must agree with the applicable TLS setting: Cluster endpoints use `tls`, and Sentinel discovery endpoints use `sentinelTls`.
|
|
47
|
+
|
|
48
|
+
URL credentials are rejected. Store Redis and Sentinel usernames/passwords in the config node credential fields instead of embedding them in an endpoint.
|
|
49
|
+
|
|
44
50
|
## TLS
|
|
45
51
|
|
|
46
|
-
TLS options:
|
|
52
|
+
TLS behavior and options:
|
|
47
53
|
|
|
48
|
-
-
|
|
54
|
+
- rejects unauthorized certificates by default;
|
|
49
55
|
- optional CA;
|
|
50
56
|
- optional client certificate;
|
|
51
57
|
- optional client private key;
|
|
52
58
|
- optional server name.
|
|
53
59
|
|
|
54
60
|
Disable verification only when the Redis deployment cannot be configured with a trusted CA and the risk is understood.
|
|
61
|
+
|
|
62
|
+
## Secrets And Imported Flows
|
|
63
|
+
|
|
64
|
+
Passwords, CA data, client certificates, and private keys are stored in Node-RED credentials. Plaintext config fields remain a migration-only fallback for older imported flows; edit and redeploy those config nodes so secrets move into the credential store.
|
package/docs/MIGRATION.md
CHANGED
package/docs/NODE_GUIDE.md
CHANGED
|
@@ -10,7 +10,7 @@ Deployment modes:
|
|
|
10
10
|
- `cluster`: Redis Cluster and AWS MemoryDB.
|
|
11
11
|
- `sentinel`: Redis Sentinel.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Cluster and MemoryDB prefixes must contain a Redis hash tag; use `{bull}` unless you have a tested custom hash tag.
|
|
14
14
|
|
|
15
15
|
## `bull cmd`
|
|
16
16
|
|
|
@@ -19,6 +19,7 @@ Input node for producer and administration commands.
|
|
|
19
19
|
Input:
|
|
20
20
|
|
|
21
21
|
- `msg.cmd`: command name. Defaults to `add`.
|
|
22
|
+
- `msg.command`: legacy alias for `msg.cmd`; use `msg.cmd` in new flows.
|
|
22
23
|
- `msg.payload`: compatibility payload.
|
|
23
24
|
- `msg.jobData`: full BullMQ job data when supplied.
|
|
24
25
|
- `msg.jobName`: BullMQ job name. Defaults to `default`.
|
|
@@ -44,6 +45,8 @@ Completion modes:
|
|
|
44
45
|
- `immediate`: complete after sending the message.
|
|
45
46
|
- `manual`: wait for downstream `bull job` acknowledgement. Fails the job after the ack timeout; set the timeout to `0` to wait indefinitely.
|
|
46
47
|
|
|
48
|
+
Concurrency must be a positive integer. The optional limiter maximum and duration must either both be blank or both be positive integers.
|
|
49
|
+
|
|
47
50
|
## `bull job`
|
|
48
51
|
|
|
49
52
|
Acts on manual-mode active jobs. Actions can be configured or supplied in `msg.cmd`.
|
|
@@ -65,7 +68,7 @@ Non-terminal actions:
|
|
|
65
68
|
|
|
66
69
|
## `bull events`
|
|
67
70
|
|
|
68
|
-
QueueEvents source node.
|
|
71
|
+
QueueEvents source node. An empty event filter subscribes to: `active`, `added`, `cleaned`, `completed`, `deduplicated`, `delayed`, `drained`, `duplicated`, `failed`, `paused`, `progress`, `removed`, `resumed`, `stalled`, `waiting`, and `waiting-children`.
|
|
69
72
|
|
|
70
73
|
Output:
|
|
71
74
|
|
package/docs/REFERENCE_MAP.md
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
- `test/scheduler.test.js`: repeat scheduler compatibility.
|
|
22
22
|
- `test/commands.test.js`: command dispatch behavior.
|
|
23
23
|
- `test/acknowledgements.test.js`: manual-acknowledgement registry lifecycle and leak prevention.
|
|
24
|
+
- `test/shutdown.test.js`: BullMQ/ioredis resource ownership, partial redeploy cleanup, and concurrent shutdown.
|
|
25
|
+
- `test/async-style.test.js`: bounded-close async implementation constraints.
|
|
24
26
|
- `test/node-red-registration.test.js`: Node-RED node type registration.
|
|
25
27
|
- `test/editor-contract.test.js`: static editor surface.
|
|
26
28
|
- `test/docs-contract.test.js`: required docs and examples.
|
package/docs/RELEASE.md
CHANGED
|
@@ -13,7 +13,7 @@ used here only for the one-time first publish.)
|
|
|
13
13
|
|
|
14
14
|
1. Confirm `package.json` has the intended `version` and the name `@pauldeng/node-red-contrib-bullmq`.
|
|
15
15
|
2. Confirm the GitHub repository is `https://github.com/pauldeng/node-red-contrib-bullmq`.
|
|
16
|
-
3. Confirm BullMQ is pinned to exactly `5.
|
|
16
|
+
3. Confirm BullMQ is pinned to exactly `5.80.9`.
|
|
17
17
|
4. Confirm no examples, docs, fixtures, or logs contain Redis, Sentinel, or MemoryDB secrets.
|
|
18
18
|
5. Update `CHANGELOG.md` for the new version.
|
|
19
19
|
|
|
@@ -56,7 +56,7 @@ Do these once, before the first release.
|
|
|
56
56
|
- [ ] **Branch ruleset** on `master` (Settings -> Rules -> Rulesets): require a pull request with at least one review, require the CI status checks to pass, block force pushes, restrict deletions. Optionally require linear history and signed commits.
|
|
57
57
|
- [ ] **Code security and analysis** (Settings): enable Dependabot alerts, Dependabot security updates, secret scanning, push protection, and private vulnerability reporting.
|
|
58
58
|
- [ ] **Workflow permissions** (Settings -> Actions -> General): set the default `GITHUB_TOKEN` to read-only.
|
|
59
|
-
- [ ] **CodeQL**:
|
|
59
|
+
- [ ] **CodeQL**: enable Code scanning "default setup" in the Security tab.
|
|
60
60
|
- [ ] **`release` environment** (Settings -> Environments -> New environment -> `release`): add yourself as a required reviewer so `publish.yml` waits for manual approval.
|
|
61
61
|
|
|
62
62
|
## 4. First publish (one time, manual)
|
package/docs/TESTING.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
npm test
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
This runs built-in `node:test` suites for package metadata, connection normalization, scheduler compatibility, command dispatch, editor surface, registration, Docker fixture contracts, docs, and examples.
|
|
9
|
+
This runs built-in `node:test` suites for package metadata, connection normalization, scheduler compatibility, command dispatch, editor surface, registration, resource shutdown, async style, Docker fixture contracts, docs, and examples. The lifecycle checks live in `test/shutdown.test.js`; bounded-close implementation constraints live in `test/async-style.test.js`.
|
|
10
10
|
|
|
11
11
|
## Node-RED Runtime Tests
|
|
12
12
|
|
|
@@ -66,7 +66,7 @@ Current executable fixtures:
|
|
|
66
66
|
- `sentinel-auth`: Redis master, two replicas, and three Sentinels with data-node ACL auth
|
|
67
67
|
- `sentinel-tls`: Redis master, two replicas, and three TLS-enabled Sentinels
|
|
68
68
|
|
|
69
|
-
The shared deployment test proves Node-RED load, connection, add/run delivery,
|
|
69
|
+
The shared deployment test proves Node-RED load, connection, add/run delivery, legacy scheduler compatibility through `basecasts` creation/removal, and absolute scheduler minute/second metadata. TLS fixtures use local self-signed test certificates and disable certificate verification for those Docker-only deployments. MemoryDB remains the certificate-verified TLS deployment path.
|
|
70
70
|
|
|
71
71
|
## AWS MemoryDB
|
|
72
72
|
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -21,7 +21,7 @@ Keep TLS verification enabled when possible. Provide the CA certificate or serve
|
|
|
21
21
|
|
|
22
22
|
## Repeat Job Is Not Found
|
|
23
23
|
|
|
24
|
-
Legacy repeat lookup uses exact scheduler ids.
|
|
24
|
+
Legacy repeat lookup uses exact scheduler ids. Pass the id returned during creation in `msg.schedulerId`, `msg.jobid`, or `msg.jobId`. A job option id can help derive the scheduler id during creation, but it is not a separate lookup field.
|
|
25
25
|
|
|
26
26
|
## Bull v4 Queue Data Missing After Upgrade
|
|
27
27
|
|
package/lib/acknowledgements.js
CHANGED
|
@@ -5,7 +5,7 @@ const { EventEmitter, once } = require("node:events");
|
|
|
5
5
|
class AcknowledgementEntry {
|
|
6
6
|
constructor(context, timeoutMs) {
|
|
7
7
|
this.events = new EventEmitter();
|
|
8
|
-
this.
|
|
8
|
+
this.settlement = undefined;
|
|
9
9
|
this.job = context.job;
|
|
10
10
|
this.queue = context.queue;
|
|
11
11
|
this.queueName = context.queueName;
|
|
@@ -24,7 +24,8 @@ class AcknowledgementEntry {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async wait() {
|
|
27
|
-
const
|
|
27
|
+
const settlement =
|
|
28
|
+
this.settlement || (await once(this.events, "settled"))[0];
|
|
28
29
|
if (settlement.type === "reject") {
|
|
29
30
|
throw settlement.error;
|
|
30
31
|
}
|
|
@@ -40,10 +41,10 @@ class AcknowledgementEntry {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
settle(settlement) {
|
|
43
|
-
if (this.
|
|
44
|
+
if (this.settlement) {
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
|
-
this.
|
|
47
|
+
this.settlement = settlement;
|
|
47
48
|
clearTimeout(this.timeout);
|
|
48
49
|
this.events.emit("settled", settlement);
|
|
49
50
|
}
|
package/lib/commands.js
CHANGED
|
@@ -56,7 +56,7 @@ async function dispatchStopAndRemoveAllJobs(queue) {
|
|
|
56
56
|
|
|
57
57
|
const cleaned = {};
|
|
58
58
|
for (const state of CLEAN_STATES) {
|
|
59
|
-
cleaned[state] = await queue.clean(0,
|
|
59
|
+
cleaned[state] = await queue.clean(0, 0, state);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
return { removedSchedulers, cleaned };
|
|
@@ -102,7 +102,6 @@ async function dispatchCommand(queue, msg = {}) {
|
|
|
102
102
|
await queue.getDelayed(
|
|
103
103
|
valueOrDefault(msg.start, 0),
|
|
104
104
|
valueOrDefault(msg.end, -1),
|
|
105
|
-
valueOrDefault(msg.asc, false),
|
|
106
105
|
)
|
|
107
106
|
).map(serializeJob);
|
|
108
107
|
case "changeDelay": {
|
|
@@ -195,9 +194,15 @@ async function dispatchCommand(queue, msg = {}) {
|
|
|
195
194
|
return queue.getGlobalConcurrency();
|
|
196
195
|
case "removeGlobalConcurrency":
|
|
197
196
|
return queue.removeGlobalConcurrency();
|
|
198
|
-
case "setGlobalRateLimit":
|
|
199
|
-
|
|
197
|
+
case "setGlobalRateLimit": {
|
|
198
|
+
const max = msg.max ?? msg.payload?.max;
|
|
199
|
+
const duration = msg.duration ?? msg.payload?.duration;
|
|
200
|
+
if (max == null || duration == null) {
|
|
201
|
+
throw new Error("msg.max and msg.duration are required");
|
|
202
|
+
}
|
|
203
|
+
await queue.setGlobalRateLimit(max, duration);
|
|
200
204
|
return true;
|
|
205
|
+
}
|
|
201
206
|
case "getGlobalRateLimit":
|
|
202
207
|
return queue.getGlobalRateLimit();
|
|
203
208
|
case "removeGlobalRateLimit":
|
package/lib/connections.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { isIP } = require("node:net");
|
|
4
|
+
|
|
3
5
|
const DEFAULT_REDIS_HOST = "localhost";
|
|
4
6
|
const DEFAULT_REDIS_PORT = 6379;
|
|
5
7
|
const CLUSTER_PREFIX = "{bull}";
|
|
@@ -32,7 +34,12 @@ function toPort(value, defaultValue = DEFAULT_REDIS_PORT) {
|
|
|
32
34
|
return port;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
function parseEndpoint(
|
|
37
|
+
function parseEndpoint(
|
|
38
|
+
endpoint,
|
|
39
|
+
defaultPort = DEFAULT_REDIS_PORT,
|
|
40
|
+
tls,
|
|
41
|
+
tlsName = "TLS",
|
|
42
|
+
) {
|
|
36
43
|
if (typeof endpoint === "object" && endpoint !== null) {
|
|
37
44
|
const host = endpoint.host || endpoint.address;
|
|
38
45
|
if (!isPresent(host)) {
|
|
@@ -48,36 +55,54 @@ function parseEndpoint(endpoint, defaultPort = DEFAULT_REDIS_PORT) {
|
|
|
48
55
|
if (!text) {
|
|
49
56
|
throw new Error("Redis endpoint cannot be empty");
|
|
50
57
|
}
|
|
58
|
+
if (isIP(text)) {
|
|
59
|
+
return { host: text, port: defaultPort };
|
|
60
|
+
}
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const hasScheme = /^[a-z][a-z\d+.-]*:\/\//i.test(text);
|
|
63
|
+
let url;
|
|
64
|
+
try {
|
|
65
|
+
url = new URL(hasScheme ? text : `redis://${text}`);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(`Invalid Redis endpoint: ${text}`);
|
|
68
|
+
}
|
|
69
|
+
if (url.protocol !== "redis:" && url.protocol !== "rediss:") {
|
|
70
|
+
throw new Error(`Unsupported Redis endpoint protocol: ${url.protocol}`);
|
|
71
|
+
}
|
|
72
|
+
if (url.username || url.password) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Redis endpoint URLs cannot include credentials; use the config node credential fields",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (hasScheme && tls !== undefined) {
|
|
78
|
+
const secure = url.protocol === "rediss:";
|
|
79
|
+
if (secure !== tls) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`${url.protocol}// endpoint requires ${tlsName} to be ${secure ? "enabled" : "disabled"}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
64
84
|
}
|
|
65
85
|
|
|
66
|
-
|
|
86
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
87
|
+
if (!host) {
|
|
67
88
|
throw new Error(`Redis endpoint requires a host: ${text}`);
|
|
68
89
|
}
|
|
69
|
-
|
|
70
|
-
return { host: host.trim(), port };
|
|
90
|
+
return { host, port: toPort(url.port, defaultPort) };
|
|
71
91
|
}
|
|
72
92
|
|
|
73
|
-
function parseEndpointList(
|
|
93
|
+
function parseEndpointList(
|
|
94
|
+
value,
|
|
95
|
+
defaultPort = DEFAULT_REDIS_PORT,
|
|
96
|
+
tls,
|
|
97
|
+
tlsName = "TLS",
|
|
98
|
+
) {
|
|
74
99
|
if (Array.isArray(value)) {
|
|
75
100
|
return value
|
|
76
101
|
.flatMap((item) =>
|
|
77
102
|
typeof item === "string" ? item.split(/[\n,]+/) : [item],
|
|
78
103
|
)
|
|
79
104
|
.filter((item) => isPresent(item))
|
|
80
|
-
.map((item) => parseEndpoint(item, defaultPort));
|
|
105
|
+
.map((item) => parseEndpoint(item, defaultPort, tls, tlsName));
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
if (!isPresent(value)) {
|
|
@@ -88,7 +113,7 @@ function parseEndpointList(value, defaultPort = DEFAULT_REDIS_PORT) {
|
|
|
88
113
|
.split(/[\n,]+/)
|
|
89
114
|
.map((item) => item.trim())
|
|
90
115
|
.filter(Boolean)
|
|
91
|
-
.map((item) => parseEndpoint(item, defaultPort));
|
|
116
|
+
.map((item) => parseEndpoint(item, defaultPort, tls, tlsName));
|
|
92
117
|
}
|
|
93
118
|
|
|
94
119
|
function readSecret(config, credentials, name) {
|
|
@@ -115,6 +140,8 @@ function normalizeQueueConfig(config = {}, credentials = {}) {
|
|
|
115
140
|
if (!queueName) {
|
|
116
141
|
throw new Error("BullMQ queue name is required");
|
|
117
142
|
}
|
|
143
|
+
const tls = toBoolean(config.tls, false);
|
|
144
|
+
const sentinelTls = toBoolean(config.sentinelTls, false);
|
|
118
145
|
|
|
119
146
|
const normalized = {
|
|
120
147
|
queueName,
|
|
@@ -124,21 +151,31 @@ function normalizeQueueConfig(config = {}, credentials = {}) {
|
|
|
124
151
|
db: isPresent(config.db) ? Number(config.db) : undefined,
|
|
125
152
|
username: config.username || undefined,
|
|
126
153
|
password: readSecret(config, credentials, "password") || undefined,
|
|
127
|
-
tls
|
|
154
|
+
tls,
|
|
128
155
|
tlsRejectUnauthorized: toBoolean(config.tlsRejectUnauthorized, true),
|
|
129
156
|
tlsCa: readSecret(config, credentials, "tlsCa") || undefined,
|
|
130
157
|
tlsCert: readSecret(config, credentials, "tlsCert") || undefined,
|
|
131
158
|
tlsKey: readSecret(config, credentials, "tlsKey") || undefined,
|
|
132
159
|
tlsServerName: config.tlsServerName || undefined,
|
|
133
160
|
prefix: config.prefix || undefined,
|
|
134
|
-
clusterNodes: parseEndpointList(
|
|
161
|
+
clusterNodes: parseEndpointList(
|
|
162
|
+
config.clusterNodes || config.startupNodes,
|
|
163
|
+
DEFAULT_REDIS_PORT,
|
|
164
|
+
tls,
|
|
165
|
+
"TLS",
|
|
166
|
+
),
|
|
135
167
|
sentinelMasterName:
|
|
136
168
|
config.sentinelMasterName || config.masterName || config.nameOfMaster,
|
|
137
|
-
sentinels: parseEndpointList(
|
|
169
|
+
sentinels: parseEndpointList(
|
|
170
|
+
config.sentinels,
|
|
171
|
+
26379,
|
|
172
|
+
sentinelTls,
|
|
173
|
+
"Sentinel TLS",
|
|
174
|
+
),
|
|
138
175
|
sentinelUsername: config.sentinelUsername || undefined,
|
|
139
176
|
sentinelPassword:
|
|
140
177
|
readSecret(config, credentials, "sentinelPassword") || undefined,
|
|
141
|
-
sentinelTls
|
|
178
|
+
sentinelTls,
|
|
142
179
|
};
|
|
143
180
|
|
|
144
181
|
if (deployment === "cluster") {
|
|
@@ -148,6 +185,11 @@ function normalizeQueueConfig(config = {}, credentials = {}) {
|
|
|
148
185
|
];
|
|
149
186
|
}
|
|
150
187
|
normalized.prefix = normalized.prefix || CLUSTER_PREFIX;
|
|
188
|
+
if (!/\{[^}]+\}/.test(normalized.prefix)) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"Cluster BullMQ prefix must contain a Redis hash tag such as {bull}",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
151
193
|
}
|
|
152
194
|
|
|
153
195
|
if (deployment === "sentinel") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pauldeng/node-red-contrib-bullmq",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "BullMQ-backed Redis job queue nodes for Node-RED",
|
|
5
5
|
"main": "bull-queue.js",
|
|
6
6
|
"files": [
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"validate": "npx --yes node-red-dev validate"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"bullmq": "5.
|
|
29
|
-
"ioredis": "5.
|
|
28
|
+
"bullmq": "5.80.9",
|
|
29
|
+
"ioredis": "5.11.1"
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"keywords": [
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"email": "dengpeng.cn@gmail.com"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@playwright/test": "1.
|
|
66
|
-
"node-red": "5.0.
|
|
65
|
+
"@playwright/test": "1.61.1",
|
|
66
|
+
"node-red": "5.0.1",
|
|
67
67
|
"node-red-node-test-helper": "^0.3.6",
|
|
68
|
-
"prettier": "3.
|
|
68
|
+
"prettier": "3.9.4"
|
|
69
69
|
}
|
|
70
70
|
}
|