@pauldeng/node-red-contrib-bullmq 1.0.0 → 1.0.1
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 +3 -3
- package/bull-queue.js +128 -26
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Node-RED nodes for BullMQ-backed Redis job queues.
|
|
10
10
|
|
|
11
|
-
This package targets BullMQ 5.78.0
|
|
11
|
+
This package targets BullMQ 5.78.0 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
|
|
|
@@ -20,8 +20,8 @@ Repository: <https://github.com/pauldeng/node-red-contrib-bullmq>
|
|
|
20
20
|
|
|
21
21
|
## Requirements
|
|
22
22
|
|
|
23
|
-
- Node.
|
|
24
|
-
- Node-RED 4.1.x
|
|
23
|
+
- Node-RED 4.1.x or 5.x
|
|
24
|
+
- Node.js 18+ with Node-RED 4.1.x, or Node.js 22.9+ with Node-RED 5.x
|
|
25
25
|
- Redis with `maxmemory-policy=noeviction`
|
|
26
26
|
- BullMQ 5.78.0
|
|
27
27
|
|
package/bull-queue.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
Worker,
|
|
9
9
|
} = require("bullmq");
|
|
10
10
|
const IORedis = require("ioredis");
|
|
11
|
+
const { setTimeout: sleep } = require("node:timers/promises");
|
|
11
12
|
|
|
12
13
|
const {
|
|
13
14
|
AcknowledgementRegistry,
|
|
@@ -41,20 +42,83 @@ const DEFAULT_EVENTS = [
|
|
|
41
42
|
"waiting-children",
|
|
42
43
|
];
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
// How long a graceful close may take before the underlying sockets are
|
|
46
|
+
// force-disconnected so Node-RED shutdown and redeploy are never blocked by
|
|
47
|
+
// an unreachable Redis server.
|
|
48
|
+
const CLOSE_GRACE_MS = 1000;
|
|
49
|
+
|
|
50
|
+
async function settleWithin(promise, ms) {
|
|
51
|
+
let settled = false;
|
|
52
|
+
(async () => {
|
|
53
|
+
try {
|
|
54
|
+
await promise;
|
|
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);
|
|
64
|
+
}
|
|
65
|
+
return settled ? "settled" : "timeout";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function disconnectClient(client) {
|
|
69
|
+
if (client && typeof client.disconnect === "function") {
|
|
70
|
+
try {
|
|
71
|
+
client.disconnect(false);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// best effort: the socket may already be gone
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function forceDisconnect(resource) {
|
|
45
79
|
if (!resource) {
|
|
46
80
|
return;
|
|
47
81
|
}
|
|
48
|
-
if (typeof resource.
|
|
49
|
-
|
|
82
|
+
if (typeof resource.status === "string") {
|
|
83
|
+
// Raw ioredis connection.
|
|
84
|
+
disconnectClient(resource);
|
|
50
85
|
return;
|
|
51
86
|
}
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
// BullMQ resource. Its public disconnect() awaits a connection promise that
|
|
88
|
+
// never settles while Redis is unreachable, so reach for the underlying
|
|
89
|
+
// ioredis clients directly (BullMQ is pinned to exactly 5.78.0).
|
|
90
|
+
if (resource.connection) {
|
|
91
|
+
disconnectClient(resource.connection._client);
|
|
92
|
+
}
|
|
93
|
+
if (resource.blockingConnection) {
|
|
94
|
+
disconnectClient(resource.blockingConnection._client);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function closeResource(resource) {
|
|
99
|
+
if (!resource) {
|
|
54
100
|
return;
|
|
55
101
|
}
|
|
56
|
-
|
|
57
|
-
|
|
102
|
+
|
|
103
|
+
if (typeof resource.close !== "function") {
|
|
104
|
+
// Raw ioredis connection: quit() never settles (or leaves a reconnect
|
|
105
|
+
// loop running) while the server is unreachable, so only quit ready
|
|
106
|
+
// connections and force-disconnect everything else.
|
|
107
|
+
if (resource.status === "ready" && typeof resource.quit === "function") {
|
|
108
|
+
if ((await settleWithin(resource.quit(), CLOSE_GRACE_MS)) === "timeout") {
|
|
109
|
+
forceDisconnect(resource);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
forceDisconnect(resource);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// BullMQ resource: QueueEvents.close() blocks forever on a connection that
|
|
118
|
+
// never became ready, so cap the graceful close and force-disconnect the
|
|
119
|
+
// sockets when it does not settle in time.
|
|
120
|
+
if ((await settleWithin(resource.close(), CLOSE_GRACE_MS)) === "timeout") {
|
|
121
|
+
forceDisconnect(resource);
|
|
58
122
|
}
|
|
59
123
|
}
|
|
60
124
|
|
|
@@ -91,12 +155,26 @@ function parseEventFilter(value) {
|
|
|
91
155
|
.filter(Boolean);
|
|
92
156
|
}
|
|
93
157
|
|
|
94
|
-
|
|
158
|
+
// One status vocabulary for every queue-backed node: connecting (yellow),
|
|
159
|
+
// connected (green), disconnected (red).
|
|
160
|
+
function setConnecting(node) {
|
|
161
|
+
node.status({ fill: "yellow", shape: "ring", text: "connecting" });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setConnected(node) {
|
|
165
|
+
node.status({ fill: "green", shape: "dot", text: "connected" });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function setDisconnected(node) {
|
|
169
|
+
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function attachErrorListener(resource, node) {
|
|
95
173
|
if (!resource || typeof resource.on !== "function") {
|
|
96
174
|
return;
|
|
97
175
|
}
|
|
98
176
|
resource.on("error", (err) => {
|
|
99
|
-
node
|
|
177
|
+
setDisconnected(node);
|
|
100
178
|
node.error(err);
|
|
101
179
|
});
|
|
102
180
|
}
|
|
@@ -150,7 +228,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
150
228
|
node.createConnection = function createConnection(role, owner = node) {
|
|
151
229
|
const descriptor = buildRedisDescriptor(node.config, role);
|
|
152
230
|
const connection = createRedisConnection(descriptor, IORedis);
|
|
153
|
-
attachErrorListener(connection, owner
|
|
231
|
+
attachErrorListener(connection, owner);
|
|
154
232
|
node.resources.add(connection);
|
|
155
233
|
return connection;
|
|
156
234
|
};
|
|
@@ -162,11 +240,18 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
162
240
|
node.config.queueName,
|
|
163
241
|
buildBullMQOptions(node.config, node.producerConnection)
|
|
164
242
|
);
|
|
165
|
-
attachErrorListener(node.queue, node
|
|
243
|
+
attachErrorListener(node.queue, node);
|
|
166
244
|
}
|
|
167
245
|
return node.queue;
|
|
168
246
|
};
|
|
169
247
|
|
|
248
|
+
// The producer connection backs the shared queue used by bull cmd nodes;
|
|
249
|
+
// exposing it lets those nodes mirror the real connection state.
|
|
250
|
+
node.getProducerConnection = function getProducerConnection() {
|
|
251
|
+
node.getQueue();
|
|
252
|
+
return node.producerConnection;
|
|
253
|
+
};
|
|
254
|
+
|
|
170
255
|
// Runtime nodes pass themselves as owner and attach their own resource
|
|
171
256
|
// error listener, so worker/events/flow errors surface on the visible
|
|
172
257
|
// runtime node rather than the hidden config node.
|
|
@@ -239,7 +324,23 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
239
324
|
}
|
|
240
325
|
|
|
241
326
|
node.bullConn.register(node);
|
|
242
|
-
|
|
327
|
+
|
|
328
|
+
// Watch the shared producer connection so the visible status reflects
|
|
329
|
+
// whether Redis is actually reachable instead of a static "configured".
|
|
330
|
+
const connection = node.bullConn.getProducerConnection();
|
|
331
|
+
const connectionListeners = {
|
|
332
|
+
ready: () => setConnected(node),
|
|
333
|
+
error: () => setDisconnected(node),
|
|
334
|
+
close: () => setDisconnected(node),
|
|
335
|
+
};
|
|
336
|
+
for (const [event, listener] of Object.entries(connectionListeners)) {
|
|
337
|
+
connection.on(event, listener);
|
|
338
|
+
}
|
|
339
|
+
if (connection.status === "ready") {
|
|
340
|
+
setConnected(node);
|
|
341
|
+
} else {
|
|
342
|
+
setConnecting(node);
|
|
343
|
+
}
|
|
243
344
|
|
|
244
345
|
node.on("input", async function onInput(msg, send, done) {
|
|
245
346
|
try {
|
|
@@ -253,6 +354,9 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
253
354
|
});
|
|
254
355
|
|
|
255
356
|
node.on("close", function onClose(removed, done) {
|
|
357
|
+
for (const [event, listener] of Object.entries(connectionListeners)) {
|
|
358
|
+
connection.removeListener(event, listener);
|
|
359
|
+
}
|
|
256
360
|
node.bullConn.deregister(node, done);
|
|
257
361
|
});
|
|
258
362
|
}
|
|
@@ -309,14 +413,10 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
309
413
|
};
|
|
310
414
|
|
|
311
415
|
node.worker = node.bullQueue.createWorker(processor, workerOptions, node);
|
|
312
|
-
attachErrorListener(node.worker, node
|
|
313
|
-
node.worker.on("ready", () =>
|
|
314
|
-
|
|
315
|
-
);
|
|
316
|
-
node.worker.on("closed", () =>
|
|
317
|
-
node.status({ fill: "red", shape: "ring", text: "closed" })
|
|
318
|
-
);
|
|
319
|
-
node.status({ fill: "yellow", shape: "ring", text: "connecting" });
|
|
416
|
+
attachErrorListener(node.worker, node);
|
|
417
|
+
node.worker.on("ready", () => setConnected(node));
|
|
418
|
+
node.worker.on("closed", () => setDisconnected(node));
|
|
419
|
+
setConnecting(node);
|
|
320
420
|
|
|
321
421
|
node.on("close", async function onClose(removed, done) {
|
|
322
422
|
acknowledgements.rejectByRunNode(
|
|
@@ -426,7 +526,7 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
426
526
|
|
|
427
527
|
node.bullConn.register(node);
|
|
428
528
|
node.queueEvents = node.bullConn.createQueueEvents(node);
|
|
429
|
-
attachErrorListener(node.queueEvents, node
|
|
529
|
+
attachErrorListener(node.queueEvents, node);
|
|
430
530
|
const events = parseEventFilter(n.events);
|
|
431
531
|
for (const event of events) {
|
|
432
532
|
node.queueEvents.on(event, (payload, eventId) => {
|
|
@@ -443,9 +543,11 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
443
543
|
}
|
|
444
544
|
async function updateReadyStatus() {
|
|
445
545
|
try {
|
|
546
|
+
setConnecting(node);
|
|
446
547
|
await node.queueEvents.waitUntilReady();
|
|
447
|
-
node
|
|
548
|
+
setConnected(node);
|
|
448
549
|
} catch (err) {
|
|
550
|
+
setDisconnected(node);
|
|
449
551
|
node.error(err);
|
|
450
552
|
}
|
|
451
553
|
}
|
|
@@ -476,14 +578,14 @@ module.exports = function registerBullMQNodes(RED) {
|
|
|
476
578
|
|
|
477
579
|
node.bullConn.register(node);
|
|
478
580
|
node.flowProducer = node.bullConn.createFlowProducer(node);
|
|
479
|
-
attachErrorListener(node.flowProducer, node
|
|
581
|
+
attachErrorListener(node.flowProducer, node);
|
|
480
582
|
async function updateReadyStatus() {
|
|
481
583
|
try {
|
|
482
|
-
node
|
|
584
|
+
setConnecting(node);
|
|
483
585
|
await node.flowProducer.waitUntilReady();
|
|
484
|
-
node
|
|
586
|
+
setConnected(node);
|
|
485
587
|
} catch (err) {
|
|
486
|
-
node
|
|
588
|
+
setDisconnected(node);
|
|
487
589
|
node.error(err);
|
|
488
590
|
}
|
|
489
591
|
}
|
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.1",
|
|
4
4
|
"description": "BullMQ-backed Redis job queue nodes for Node-RED",
|
|
5
5
|
"main": "bull-queue.js",
|
|
6
6
|
"files": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"jobs"
|
|
41
41
|
],
|
|
42
42
|
"node-red": {
|
|
43
|
-
"version": ">=4.1.0 <
|
|
43
|
+
"version": ">=4.1.0 <6",
|
|
44
44
|
"nodes": {
|
|
45
45
|
"bull-queue": "bull-queue.js"
|
|
46
46
|
}
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@playwright/test": "1.60.0",
|
|
66
|
-
"node-red": "
|
|
66
|
+
"node-red": "5.0.0",
|
|
67
67
|
"node-red-node-test-helper": "^0.3.6",
|
|
68
68
|
"prettier": "3.8.3"
|
|
69
69
|
}
|