@blokcert/node-red-contrib-delayed-job 0.1.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/delayed-job/delayed-job-redis.html +50 -0
- package/delayed-job/delayed-job-redis.js +36 -0
- package/delayed-job/delayed-job.html +48 -0
- package/delayed-job/delayed-job.js +191 -0
- package/delayed-job/icons/delayed-job.svg +14 -0
- package/delayed-job/locales/en-US/delayed-job-redis.json +13 -0
- package/delayed-job/locales/en-US/delayed-job.html +48 -0
- package/delayed-job/locales/en-US/delayed-job.json +16 -0
- package/delayed-job/locales/zh-TW/delayed-job-redis.json +13 -0
- package/delayed-job/locales/zh-TW/delayed-job.html +48 -0
- package/delayed-job/locales/zh-TW/delayed-job.json +16 -0
- package/package.json +34 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('delayed-job-redis', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
name: { value: '' },
|
|
6
|
+
host: { value: '127.0.0.1', required: true },
|
|
7
|
+
port: { value: 6379, required: true, validate: RED.validators.number() },
|
|
8
|
+
db: { value: 0, validate: RED.validators.number() },
|
|
9
|
+
tls: { value: false },
|
|
10
|
+
queueName: { value: 'default' },
|
|
11
|
+
},
|
|
12
|
+
credentials: {
|
|
13
|
+
password: { type: 'password' },
|
|
14
|
+
},
|
|
15
|
+
label: function () {
|
|
16
|
+
return this.name || this.host + ':' + this.port + '/' + (this.queueName || 'default');
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script type="text/html" data-template-name="delayed-job-redis">
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="delayed-job-redis.label.name"></span></label>
|
|
24
|
+
<input type="text" id="node-config-input-name" data-i18n="[placeholder]delayed-job-redis.label.name">
|
|
25
|
+
</div>
|
|
26
|
+
<div class="form-row">
|
|
27
|
+
<label for="node-config-input-host"><i class="fa fa-server"></i> <span data-i18n="delayed-job-redis.label.host"></span></label>
|
|
28
|
+
<input type="text" id="node-config-input-host" placeholder="127.0.0.1">
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<label for="node-config-input-port"><i class="fa fa-plug"></i> <span data-i18n="delayed-job-redis.label.port"></span></label>
|
|
32
|
+
<input type="number" id="node-config-input-port" placeholder="6379">
|
|
33
|
+
</div>
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-config-input-password"><i class="fa fa-lock"></i> <span data-i18n="delayed-job-redis.label.password"></span></label>
|
|
36
|
+
<input type="password" id="node-config-input-password">
|
|
37
|
+
</div>
|
|
38
|
+
<div class="form-row">
|
|
39
|
+
<label for="node-config-input-db"><i class="fa fa-database"></i> <span data-i18n="delayed-job-redis.label.db"></span></label>
|
|
40
|
+
<input type="number" id="node-config-input-db" placeholder="0">
|
|
41
|
+
</div>
|
|
42
|
+
<div class="form-row">
|
|
43
|
+
<label for="node-config-input-tls"><i class="fa fa-shield"></i> <span data-i18n="delayed-job-redis.label.tls"></span></label>
|
|
44
|
+
<input type="checkbox" id="node-config-input-tls" style="width: auto">
|
|
45
|
+
</div>
|
|
46
|
+
<div class="form-row">
|
|
47
|
+
<label for="node-config-input-queueName"><i class="fa fa-list"></i> <span data-i18n="delayed-job-redis.label.queueName"></span></label>
|
|
48
|
+
<input type="text" id="node-config-input-queueName" placeholder="default">
|
|
49
|
+
</div>
|
|
50
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
function RedisConfigNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
|
|
5
|
+
this.host = config.host || '127.0.0.1';
|
|
6
|
+
this.port = parseInt(config.port, 10) || 6379;
|
|
7
|
+
this.db = parseInt(config.db, 10) || 0;
|
|
8
|
+
this.tls = config.tls || false;
|
|
9
|
+
this.queueName = config.queueName || 'default';
|
|
10
|
+
|
|
11
|
+
const password = this.credentials.password || undefined;
|
|
12
|
+
|
|
13
|
+
this.connectionOptions = {
|
|
14
|
+
host: this.host,
|
|
15
|
+
port: this.port,
|
|
16
|
+
db: this.db,
|
|
17
|
+
password: password,
|
|
18
|
+
maxRetriesPerRequest: null,
|
|
19
|
+
enableReadyCheck: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (this.tls) {
|
|
23
|
+
this.connectionOptions.tls = {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.on('close', function (done) {
|
|
27
|
+
done();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
RED.nodes.registerType('delayed-job-redis', RedisConfigNode, {
|
|
32
|
+
credentials: {
|
|
33
|
+
password: { type: 'password' },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('delayed-job', {
|
|
3
|
+
category: 'function',
|
|
4
|
+
color: '#E67E22',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
redis: { value: '', type: 'delayed-job-redis', required: true },
|
|
8
|
+
concurrency: { value: 1, validate: RED.validators.number() },
|
|
9
|
+
intervalValue: { value: 0, validate: RED.validators.number() },
|
|
10
|
+
intervalUnit: { value: 'ms' },
|
|
11
|
+
},
|
|
12
|
+
inputs: 1,
|
|
13
|
+
outputs: 1,
|
|
14
|
+
icon: 'delayed-job.svg',
|
|
15
|
+
paletteLabel: 'delayed job',
|
|
16
|
+
label: function () {
|
|
17
|
+
return this.name || 'delayed job';
|
|
18
|
+
},
|
|
19
|
+
labelStyle: function () {
|
|
20
|
+
return this.name ? 'node_label_italic' : '';
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<script type="text/html" data-template-name="delayed-job">
|
|
26
|
+
<div class="form-row">
|
|
27
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="delayed-job.label.name"></span></label>
|
|
28
|
+
<input type="text" id="node-input-name" data-i18n="[placeholder]delayed-job.label.name">
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<label for="node-input-redis"><i class="fa fa-server"></i> <span data-i18n="delayed-job.label.redis"></span></label>
|
|
32
|
+
<input type="text" id="node-input-redis">
|
|
33
|
+
</div>
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-input-concurrency"><i class="fa fa-tasks"></i> <span data-i18n="delayed-job.label.concurrency"></span></label>
|
|
36
|
+
<input type="number" id="node-input-concurrency" min="1" max="100" placeholder="1">
|
|
37
|
+
</div>
|
|
38
|
+
<div class="form-row">
|
|
39
|
+
<label for="node-input-intervalValue"><i class="fa fa-clock-o"></i> <span data-i18n="delayed-job.label.interval"></span></label>
|
|
40
|
+
<input type="number" id="node-input-intervalValue" min="0" placeholder="0" style="width: 100px">
|
|
41
|
+
<select id="node-input-intervalUnit" style="width: 80px">
|
|
42
|
+
<option value="ms" data-i18n="delayed-job.interval.milliseconds"></option>
|
|
43
|
+
<option value="s" data-i18n="delayed-job.interval.seconds"></option>
|
|
44
|
+
<option value="m" data-i18n="delayed-job.interval.minutes"></option>
|
|
45
|
+
<option value="h" data-i18n="delayed-job.interval.hours"></option>
|
|
46
|
+
</select>
|
|
47
|
+
</div>
|
|
48
|
+
</script>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
const { Queue, Worker } = require('bullmq');
|
|
3
|
+
|
|
4
|
+
function DelayedJobNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
node.concurrency = parseInt(config.concurrency, 10) || 1;
|
|
9
|
+
|
|
10
|
+
// --- Rate limit (interval between jobs) ---
|
|
11
|
+
const raw = parseInt(config.intervalValue, 10);
|
|
12
|
+
const intervalValue = isNaN(raw) ? 0 : raw;
|
|
13
|
+
const intervalUnit = config.intervalUnit || 'ms';
|
|
14
|
+
const unitMultiplier = { ms: 1, s: 1000, m: 60000, h: 3600000 };
|
|
15
|
+
const intervalMs = intervalValue * (unitMultiplier[intervalUnit] || 1);
|
|
16
|
+
|
|
17
|
+
node.redisConfig = RED.nodes.getNode(config.redis);
|
|
18
|
+
if (!node.redisConfig) {
|
|
19
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no redis config' });
|
|
20
|
+
node.error('No Redis configuration node selected');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const prefix = node.redisConfig.queueName || 'default';
|
|
25
|
+
node.queueName = prefix + '-' + node.id;
|
|
26
|
+
|
|
27
|
+
const connectionOpts = node.redisConfig.connectionOptions;
|
|
28
|
+
let queue = null;
|
|
29
|
+
let worker = null;
|
|
30
|
+
let closing = false;
|
|
31
|
+
|
|
32
|
+
// --- Serialization helpers ---
|
|
33
|
+
|
|
34
|
+
function serializeMsg(msg) {
|
|
35
|
+
const clean = Object.assign({}, msg);
|
|
36
|
+
delete clean._msgid;
|
|
37
|
+
return JSON.parse(JSON.stringify(clean));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deserializeMsg(data) {
|
|
41
|
+
return reviveBuffers(data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function reviveBuffers(obj) {
|
|
45
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
46
|
+
if (obj.type === 'Buffer' && Array.isArray(obj.data)) {
|
|
47
|
+
return Buffer.from(obj.data);
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(obj)) {
|
|
50
|
+
return obj.map(reviveBuffers);
|
|
51
|
+
}
|
|
52
|
+
for (const key of Object.keys(obj)) {
|
|
53
|
+
obj[key] = reviveBuffers(obj[key]);
|
|
54
|
+
}
|
|
55
|
+
return obj;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Initialize Queue (Producer) ---
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
queue = new Queue(node.queueName, {
|
|
62
|
+
connection: connectionOpts,
|
|
63
|
+
defaultJobOptions: {
|
|
64
|
+
removeOnComplete: false,
|
|
65
|
+
removeOnFail: false,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
} catch (err) {
|
|
69
|
+
node.error('Failed to create queue: ' + err.message);
|
|
70
|
+
node.status({ fill: 'red', shape: 'ring', text: 'queue init fail' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Initialize Worker (Consumer) ---
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
worker = new Worker(
|
|
78
|
+
node.queueName,
|
|
79
|
+
async (job) => {
|
|
80
|
+
if (closing) return;
|
|
81
|
+
const ttlMs = job.data._ttl;
|
|
82
|
+
if (ttlMs && Date.now() - job.timestamp > ttlMs) {
|
|
83
|
+
node.warn('Job ' + job.id + ' expired (TTL ' + (ttlMs / 1000) + 's)');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const msg = deserializeMsg(job.data);
|
|
87
|
+
delete msg._ttl;
|
|
88
|
+
node.send(msg);
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
connection: connectionOpts,
|
|
92
|
+
concurrency: node.concurrency,
|
|
93
|
+
stalledInterval: 30000,
|
|
94
|
+
maxStalledCount: 2,
|
|
95
|
+
...(intervalMs > 0 ? { limiter: { max: node.concurrency, duration: intervalMs } } : {}),
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
worker.on('ready', () => {
|
|
100
|
+
node.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
worker.on('failed', (job, err) => {
|
|
104
|
+
node.warn('Job ' + (job ? job.id : '?') + ' failed: ' + err.message);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
worker.on('error', (err) => {
|
|
108
|
+
node.error('Worker error: ' + err.message);
|
|
109
|
+
node.status({
|
|
110
|
+
fill: 'red',
|
|
111
|
+
shape: 'ring',
|
|
112
|
+
text: err.message.substring(0, 30),
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
worker.on('stalled', (jobId) => {
|
|
117
|
+
node.warn('Job stalled: ' + jobId);
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
node.error('Failed to create worker: ' + err.message);
|
|
121
|
+
node.status({ fill: 'red', shape: 'ring', text: 'worker init fail' });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Input Handler (Producer) ---
|
|
126
|
+
|
|
127
|
+
node.on('input', async function (msg, send, done) {
|
|
128
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
129
|
+
|
|
130
|
+
if (closing) {
|
|
131
|
+
if (done) done(new Error('Node is closing, job not queued'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const ttlSec = parseInt(msg.ttl, 10) || 0;
|
|
137
|
+
const serialized = serializeMsg(msg);
|
|
138
|
+
if (ttlSec > 0) {
|
|
139
|
+
serialized._ttl = ttlSec * 1000;
|
|
140
|
+
}
|
|
141
|
+
const jobName = 'job';
|
|
142
|
+
const jobOpts = {};
|
|
143
|
+
|
|
144
|
+
const delay = parseInt(msg.delay, 10) || 0;
|
|
145
|
+
if (delay > 0) {
|
|
146
|
+
jobOpts.delay = delay;
|
|
147
|
+
}
|
|
148
|
+
if (msg.priority !== undefined) {
|
|
149
|
+
jobOpts.priority = parseInt(msg.priority, 10);
|
|
150
|
+
}
|
|
151
|
+
const keepAfterSec = parseInt(msg.keepAfter, 10) || 0;
|
|
152
|
+
if (keepAfterSec > 0) {
|
|
153
|
+
jobOpts.removeOnComplete = { age: keepAfterSec };
|
|
154
|
+
jobOpts.removeOnFail = { age: keepAfterSec };
|
|
155
|
+
}
|
|
156
|
+
await queue.add(jobName, serialized, jobOpts);
|
|
157
|
+
|
|
158
|
+
if (done) done();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (done) {
|
|
161
|
+
done(err);
|
|
162
|
+
} else {
|
|
163
|
+
node.error(err, msg);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// --- Close Handler (Graceful Shutdown) ---
|
|
169
|
+
|
|
170
|
+
node.on('close', async function (removed, done) {
|
|
171
|
+
closing = true;
|
|
172
|
+
node.status({ fill: 'yellow', shape: 'ring', text: 'closing...' });
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
if (worker) {
|
|
176
|
+
await worker.close();
|
|
177
|
+
}
|
|
178
|
+
if (queue) {
|
|
179
|
+
await queue.close();
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
node.error('Error during shutdown: ' + err.message);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
node.status({});
|
|
186
|
+
done();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
RED.nodes.registerType('delayed-job', DelayedJobNode);
|
|
191
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40">
|
|
2
|
+
<!-- Clock face -->
|
|
3
|
+
<circle cx="18" cy="20" r="12" fill="none" stroke="#ffffff" stroke-width="2"/>
|
|
4
|
+
<!-- Clock hands - hour -->
|
|
5
|
+
<line x1="18" y1="20" x2="18" y2="13" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
|
|
6
|
+
<!-- Clock hands - minute -->
|
|
7
|
+
<line x1="18" y1="20" x2="23" y2="17" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round"/>
|
|
8
|
+
<!-- Clock center dot -->
|
|
9
|
+
<circle cx="18" cy="20" r="1.2" fill="#ffffff"/>
|
|
10
|
+
<!-- Queue lines (right side) -->
|
|
11
|
+
<line x1="32" y1="15" x2="38" y2="15" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
|
|
12
|
+
<line x1="32" y1="20" x2="38" y2="20" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
|
|
13
|
+
<line x1="32" y1="25" x2="38" y2="25" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="delayed-job">
|
|
2
|
+
<p>A Redis-backed job queue node using BullMQ. Acts as both producer and consumer.</p>
|
|
3
|
+
|
|
4
|
+
<h3>Input</h3>
|
|
5
|
+
<p>Messages received on the input are serialized and added to the named Redis queue.
|
|
6
|
+
The following <code>msg</code> properties control job behavior:</p>
|
|
7
|
+
<ul>
|
|
8
|
+
<li><code>msg.delay</code> - Delay in milliseconds before the job is processed</li>
|
|
9
|
+
<li><code>msg.priority</code> - Job priority (lower number = higher priority)</li>
|
|
10
|
+
<li><code>msg.ttl</code> - Time-to-live in seconds. If the job is not processed within this time, it will be discarded. Default: no limit</li>
|
|
11
|
+
<li><code>msg.keepAfter</code> - Keep the job in Redis for this many seconds after completion. Default: keep forever</li>
|
|
12
|
+
</ul>
|
|
13
|
+
|
|
14
|
+
<h3>Output</h3>
|
|
15
|
+
<p>When a job is picked up by the worker, the deserialized message is sent from the output.
|
|
16
|
+
A new <code>_msgid</code> is assigned by Node-RED.</p>
|
|
17
|
+
|
|
18
|
+
<h3>Details</h3>
|
|
19
|
+
<p>This node acts as both producer and consumer:</p>
|
|
20
|
+
<ul>
|
|
21
|
+
<li><b>On input</b>: the message is serialized and queued in Redis</li>
|
|
22
|
+
<li><b>Background worker</b>: polls the queue and sends messages to the output (rate limited by Interval setting)</li>
|
|
23
|
+
</ul>
|
|
24
|
+
|
|
25
|
+
<p>Multiple Node-RED instances sharing the same Redis server and queue name
|
|
26
|
+
form a distributed worker pool. Any instance can process any job.</p>
|
|
27
|
+
|
|
28
|
+
<h3>Crash Recovery</h3>
|
|
29
|
+
<p>Jobs are persisted in Redis. If Node-RED crashes, jobs remain in the queue and
|
|
30
|
+
will be picked up when any instance restarts.</p>
|
|
31
|
+
|
|
32
|
+
<h3>Limitations</h3>
|
|
33
|
+
<p>Non-serializable properties like <code>msg.req</code> and <code>msg.res</code>
|
|
34
|
+
(from http-in nodes) cannot survive queue serialization and will be lost.</p>
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<script type="text/html" data-help-name="delayed-job-redis">
|
|
38
|
+
<p>Configuration for a Redis server connection used by delayed-job nodes.</p>
|
|
39
|
+
<h3>Properties</h3>
|
|
40
|
+
<ul>
|
|
41
|
+
<li><b>Host</b> - Redis server hostname (default: 127.0.0.1)</li>
|
|
42
|
+
<li><b>Port</b> - Redis server port (default: 6379)</li>
|
|
43
|
+
<li><b>Password</b> - Redis authentication password (optional, stored encrypted)</li>
|
|
44
|
+
<li><b>DB</b> - Redis database index (default: 0)</li>
|
|
45
|
+
<li><b>TLS</b> - Enable TLS/SSL connection</li>
|
|
46
|
+
<li><b>Queue Name</b> - Name of the job queue (default: default)</li>
|
|
47
|
+
</ul>
|
|
48
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"delayed-job": {
|
|
3
|
+
"label": {
|
|
4
|
+
"name": "Name",
|
|
5
|
+
"redis": "Redis",
|
|
6
|
+
"concurrency": "Concurrency",
|
|
7
|
+
"interval": "Interval"
|
|
8
|
+
},
|
|
9
|
+
"interval": {
|
|
10
|
+
"milliseconds": "ms",
|
|
11
|
+
"seconds": "seconds",
|
|
12
|
+
"minutes": "minutes",
|
|
13
|
+
"hours": "hours"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script type="text/html" data-help-name="delayed-job">
|
|
2
|
+
<p>使用 BullMQ 的 Redis 佇列節點,同時扮演生產者和消費者。</p>
|
|
3
|
+
|
|
4
|
+
<h3>輸入</h3>
|
|
5
|
+
<p>收到的訊息會被序列化後加入 Redis 佇列。
|
|
6
|
+
以下 <code>msg</code> 屬性可控制工作行為:</p>
|
|
7
|
+
<ul>
|
|
8
|
+
<li><code>msg.delay</code> - 延遲毫秒數,工作將在指定時間後才被處理</li>
|
|
9
|
+
<li><code>msg.priority</code> - 工作優先級(數字越小,優先級越高)</li>
|
|
10
|
+
<li><code>msg.ttl</code> - 存活時間(秒)。若工作在此時間內未被處理,將被丟棄。預設:不限制</li>
|
|
11
|
+
<li><code>msg.keepAfter</code> - 工作完成後在 Redis 中保留的秒數。預設:永久保留</li>
|
|
12
|
+
</ul>
|
|
13
|
+
|
|
14
|
+
<h3>輸出</h3>
|
|
15
|
+
<p>當工作被 worker 取出處理時,反序列化後的訊息會從輸出端送出。
|
|
16
|
+
Node-RED 會指派新的 <code>_msgid</code>。</p>
|
|
17
|
+
|
|
18
|
+
<h3>詳細說明</h3>
|
|
19
|
+
<p>此節點同時扮演生產者和消費者:</p>
|
|
20
|
+
<ul>
|
|
21
|
+
<li><b>收到輸入時</b>:訊息被序列化後放入 Redis 佇列</li>
|
|
22
|
+
<li><b>背景 worker</b>:從佇列取出工作並送到輸出端(受間隔設定的速率限制)</li>
|
|
23
|
+
</ul>
|
|
24
|
+
|
|
25
|
+
<p>多個 Node-RED 實體共用相同的 Redis 伺服器和佇列名稱,
|
|
26
|
+
可組成分散式工作池。任何實體都可以處理任何工作。</p>
|
|
27
|
+
|
|
28
|
+
<h3>故障恢復</h3>
|
|
29
|
+
<p>工作持久化儲存在 Redis 中。如果 Node-RED 當機,工作會留在佇列中,
|
|
30
|
+
當任何實體重新啟動時會被取出處理。</p>
|
|
31
|
+
|
|
32
|
+
<h3>限制</h3>
|
|
33
|
+
<p>無法序列化的屬性(如 http-in 節點的 <code>msg.req</code> 和 <code>msg.res</code>)
|
|
34
|
+
無法通過佇列序列化,將會遺失。</p>
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<script type="text/html" data-help-name="delayed-job-redis">
|
|
38
|
+
<p>delayed-job 節點使用的 Redis 伺服器連線設定。</p>
|
|
39
|
+
<h3>屬性</h3>
|
|
40
|
+
<ul>
|
|
41
|
+
<li><b>主機</b> - Redis 伺服器主機名稱(預設:127.0.0.1)</li>
|
|
42
|
+
<li><b>連接埠</b> - Redis 伺服器連接埠(預設:6379)</li>
|
|
43
|
+
<li><b>密碼</b> - Redis 認證密碼(選填,加密儲存)</li>
|
|
44
|
+
<li><b>資料庫</b> - Redis 資料庫索引(預設:0)</li>
|
|
45
|
+
<li><b>TLS</b> - 啟用 TLS/SSL 連線</li>
|
|
46
|
+
<li><b>佇列名稱</b> - 工作佇列名稱(預設:default)</li>
|
|
47
|
+
</ul>
|
|
48
|
+
</script>
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blokcert/node-red-contrib-delayed-job",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Node-RED node for Redis-backed delayed job queues using BullMQ",
|
|
5
|
+
"keywords": ["node-red"],
|
|
6
|
+
"author": "caspar.wei",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/MingShyanWei/node-red-contrib-delayed-job.git"
|
|
10
|
+
},
|
|
11
|
+
"node-red": {
|
|
12
|
+
"version": ">=2.0.0",
|
|
13
|
+
"nodes": {
|
|
14
|
+
"delayed-job-redis": "delayed-job/delayed-job-redis.js",
|
|
15
|
+
"delayed-job": "delayed-job/delayed-job.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"bullmq": "^5.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"node-red": ">=3.0.0",
|
|
23
|
+
"node-red-node-test-helper": "^0.3.0",
|
|
24
|
+
"mocha": "^10.0.0",
|
|
25
|
+
"chai": "^4.3.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "mocha --timeout 10000 test/**/*_spec.js"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|