power-queues 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +671 -659
- package/dist/index.d.cts +83 -275
- package/dist/index.d.ts +83 -275
- package/dist/index.js +670 -668
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,765 +1,767 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import { v4 as uuid } from "uuid";
|
|
3
|
-
import {
|
|
4
|
-
isStrFilled,
|
|
5
|
-
isStr,
|
|
6
|
-
isArrFilled,
|
|
7
|
-
isArr,
|
|
8
|
-
isObjFilled,
|
|
9
|
-
isNumPZ,
|
|
10
|
-
isNumP,
|
|
11
|
-
isFunc,
|
|
12
|
-
wait
|
|
13
|
-
} from "full-utils";
|
|
1
|
+
// src/PowerQueues.ts
|
|
14
2
|
import { PowerRedis } from "power-redis";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
3
|
+
import { wait } from "full-utils";
|
|
4
|
+
import { v4 as uuid } from "uuid";
|
|
5
|
+
|
|
6
|
+
// src/scripts.ts
|
|
7
|
+
var XAddBulk = `
|
|
8
|
+
local UNPACK = table and table.unpack or unpack
|
|
9
|
+
|
|
10
|
+
local stream = KEYS[1]
|
|
11
|
+
local maxlen = tonumber(ARGV[1])
|
|
12
|
+
local approxFlag = tonumber(ARGV[2]) == 1
|
|
13
|
+
local n = tonumber(ARGV[3])
|
|
14
|
+
local exactFlag = tonumber(ARGV[4]) == 1
|
|
15
|
+
local nomkstream = tonumber(ARGV[5]) == 1
|
|
16
|
+
local trimLimit = tonumber(ARGV[6])
|
|
17
|
+
local minidWindowMs = tonumber(ARGV[7]) or 0
|
|
18
|
+
local minidExact = tonumber(ARGV[8]) == 1
|
|
19
|
+
local idx = 9
|
|
20
|
+
local out = {}
|
|
21
|
+
|
|
22
|
+
local common_opts = {}
|
|
23
|
+
local co_len = 0
|
|
24
|
+
|
|
25
|
+
if nomkstream then
|
|
26
|
+
co_len = co_len + 1; common_opts[co_len] = 'NOMKSTREAM'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if minidWindowMs > 0 then
|
|
30
|
+
local tm = redis.call('TIME')
|
|
31
|
+
local now_ms = (tonumber(tm[1]) * 1000) + math.floor(tonumber(tm[2]) / 1000)
|
|
32
|
+
local cutoff_ms = now_ms - minidWindowMs
|
|
33
|
+
if cutoff_ms < 0 then cutoff_ms = 0 end
|
|
34
|
+
local cutoff_id = tostring(cutoff_ms) .. '-0'
|
|
35
|
+
|
|
36
|
+
co_len = co_len + 1; common_opts[co_len] = 'MINID'
|
|
37
|
+
co_len = co_len + 1; common_opts[co_len] = (minidExact and '=' or '~')
|
|
38
|
+
co_len = co_len + 1; common_opts[co_len] = cutoff_id
|
|
39
|
+
if trimLimit and trimLimit > 0 then
|
|
40
|
+
co_len = co_len + 1; common_opts[co_len] = 'LIMIT'
|
|
41
|
+
co_len = co_len + 1; common_opts[co_len] = trimLimit
|
|
42
|
+
end
|
|
43
|
+
elseif maxlen and maxlen > 0 then
|
|
44
|
+
co_len = co_len + 1; common_opts[co_len] = 'MAXLEN'
|
|
45
|
+
if exactFlag then
|
|
46
|
+
co_len = co_len + 1; common_opts[co_len] = '='
|
|
47
|
+
elseif approxFlag then
|
|
48
|
+
co_len = co_len + 1; common_opts[co_len] = '~'
|
|
49
|
+
end
|
|
50
|
+
co_len = co_len + 1; common_opts[co_len] = maxlen
|
|
51
|
+
if trimLimit and trimLimit > 0 then
|
|
52
|
+
co_len = co_len + 1; common_opts[co_len] = 'LIMIT'
|
|
53
|
+
co_len = co_len + 1; common_opts[co_len] = trimLimit
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
for e = 1, n do
|
|
58
|
+
local id = ARGV[idx]; idx = idx + 1
|
|
59
|
+
local num_pairs = tonumber(ARGV[idx]); idx = idx + 1
|
|
60
|
+
|
|
61
|
+
local a = {}
|
|
62
|
+
local a_len = 0
|
|
63
|
+
|
|
64
|
+
for i = 1, co_len do a_len = a_len + 1; a[a_len] = common_opts[i] end
|
|
65
|
+
|
|
66
|
+
a_len = a_len + 1; a[a_len] = id
|
|
67
|
+
|
|
68
|
+
for j = 1, (num_pairs * 2) do
|
|
69
|
+
a_len = a_len + 1; a[a_len] = ARGV[idx]; idx = idx + 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
local addedId = redis.call('XADD', stream, UNPACK(a))
|
|
73
|
+
out[#out+1] = addedId or ''
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
return out
|
|
77
|
+
`;
|
|
78
|
+
var Approve = `
|
|
79
|
+
local stream = KEYS[1]
|
|
80
|
+
local group = ARGV[1]
|
|
81
|
+
local delFlag = tonumber(ARGV[2]) == 1
|
|
82
|
+
|
|
83
|
+
local acked = 0
|
|
84
|
+
local nids = #ARGV - 2
|
|
85
|
+
if nids > 0 then
|
|
86
|
+
acked = tonumber(redis.call('XACK', stream, group, unpack(ARGV, 3))) or 0
|
|
87
|
+
if delFlag and nids > 0 then
|
|
88
|
+
local ok, deln = pcall(redis.call, 'XDEL', stream, unpack(ARGV, 3))
|
|
89
|
+
if not ok then
|
|
90
|
+
deln = 0
|
|
91
|
+
for i = 3, #ARGV do
|
|
92
|
+
deln = deln + (tonumber(redis.call('XDEL', stream, ARGV[i])) or 0)
|
|
64
93
|
end
|
|
65
94
|
end
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
return acked
|
|
98
|
+
`;
|
|
99
|
+
var IdempotencyAllow = `
|
|
100
|
+
local doneKey = KEYS[1]
|
|
101
|
+
local lockKey = KEYS[2]
|
|
102
|
+
local startKey = KEYS[3]
|
|
103
|
+
|
|
104
|
+
if redis.call('EXISTS', doneKey) == 1 then
|
|
105
|
+
return 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
local ttl = tonumber(ARGV[1]) or 0
|
|
109
|
+
if ttl <= 0 then return 0 end
|
|
110
|
+
|
|
111
|
+
local ok = redis.call('SET', lockKey, ARGV[2], 'NX', 'PX', ttl)
|
|
112
|
+
if ok then
|
|
113
|
+
if startKey and startKey ~= '' then
|
|
114
|
+
redis.call('SET', startKey, 1, 'PX', ttl)
|
|
115
|
+
end
|
|
116
|
+
return 2
|
|
117
|
+
else
|
|
118
|
+
return 0
|
|
119
|
+
end
|
|
120
|
+
`;
|
|
121
|
+
var IdempotencyStart = `
|
|
122
|
+
local lockKey = KEYS[1]
|
|
123
|
+
local startKey = KEYS[2]
|
|
124
|
+
if redis.call('GET', lockKey) == ARGV[1] then
|
|
125
|
+
local ttl = tonumber(ARGV[2]) or 0
|
|
126
|
+
if ttl > 0 then
|
|
127
|
+
redis.call('SET', startKey, 1, 'PX', ttl)
|
|
128
|
+
redis.call('PEXPIRE', lockKey, ttl)
|
|
129
|
+
else
|
|
130
|
+
redis.call('SET', startKey, 1)
|
|
131
|
+
end
|
|
132
|
+
return 1
|
|
133
|
+
end
|
|
134
|
+
return 0
|
|
135
|
+
`;
|
|
136
|
+
var IdempotencyDone = `
|
|
137
|
+
local doneKey = KEYS[1]
|
|
138
|
+
local lockKey = KEYS[2]
|
|
139
|
+
local startKey = KEYS[3]
|
|
140
|
+
redis.call('SET', doneKey, 1)
|
|
141
|
+
local ttlSec = tonumber(ARGV[1]) or 0
|
|
142
|
+
if ttlSec > 0 then redis.call('EXPIRE', doneKey, ttlSec) end
|
|
143
|
+
if redis.call('GET', lockKey) == ARGV[2] then
|
|
144
|
+
redis.call('DEL', lockKey)
|
|
145
|
+
if startKey then redis.call('DEL', startKey) end
|
|
146
|
+
end
|
|
147
|
+
return 1
|
|
148
|
+
`;
|
|
149
|
+
var IdempotencyFree = `
|
|
150
|
+
local lockKey = KEYS[1]
|
|
151
|
+
local startKey = KEYS[2]
|
|
152
|
+
if redis.call('GET', lockKey) == ARGV[1] then
|
|
153
|
+
redis.call('DEL', lockKey)
|
|
154
|
+
if startKey then redis.call('DEL', startKey) end
|
|
155
|
+
return 1
|
|
156
|
+
end
|
|
157
|
+
return 0
|
|
158
|
+
`;
|
|
159
|
+
var SelectStuck = `
|
|
160
|
+
local stream = KEYS[1]
|
|
161
|
+
local group = ARGV[1]
|
|
162
|
+
local consumer = ARGV[2]
|
|
163
|
+
local pendingIdleMs = tonumber(ARGV[3])
|
|
164
|
+
local count = tonumber(ARGV[4]) or 0
|
|
165
|
+
if count < 1 then count = 1 end
|
|
166
|
+
|
|
167
|
+
local timeBudgetMs = tonumber(ARGV[5]) or 15
|
|
168
|
+
local t0 = redis.call('TIME')
|
|
169
|
+
local start_ms = (tonumber(t0[1]) * 1000) + math.floor(tonumber(t0[2]) / 1000)
|
|
170
|
+
|
|
171
|
+
local results = {}
|
|
172
|
+
local collected = 0
|
|
173
|
+
local start_id = '0-0'
|
|
174
|
+
local iters = 0
|
|
175
|
+
local max_iters = math.max(16, math.ceil(count / 100))
|
|
176
|
+
|
|
177
|
+
local function time_exceeded()
|
|
178
|
+
local t1 = redis.call('TIME')
|
|
179
|
+
local now_ms = (tonumber(t1[1]) * 1000) + math.floor(tonumber(t1[2]) / 1000)
|
|
180
|
+
return (now_ms - start_ms) >= timeBudgetMs
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
while (collected < count) and (iters < max_iters) do
|
|
184
|
+
local to_claim = count - collected
|
|
185
|
+
if to_claim < 1 then break end
|
|
186
|
+
|
|
187
|
+
local claim = redis.call('XAUTOCLAIM', stream, group, consumer, pendingIdleMs, start_id, 'COUNT', to_claim)
|
|
188
|
+
iters = iters + 1
|
|
189
|
+
|
|
190
|
+
local bucket = nil
|
|
191
|
+
if claim then
|
|
192
|
+
bucket = claim[2]
|
|
193
|
+
end
|
|
194
|
+
if bucket and #bucket > 0 then
|
|
195
|
+
for i = 1, #bucket do
|
|
196
|
+
results[#results+1] = bucket[i]
|
|
86
197
|
end
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
getRequeueScript() {
|
|
91
|
-
return `
|
|
92
|
-
-- KEYS: 1=processing, 2=processingVt, 3=ready
|
|
93
|
-
-- ARGV: 1=now, 2=limit
|
|
94
|
-
local processing = KEYS[1]
|
|
95
|
-
local vt = KEYS[2]
|
|
96
|
-
local ready = KEYS[3]
|
|
97
|
-
local now = tonumber(ARGV[1])
|
|
98
|
-
local limit = tonumber(ARGV[2])
|
|
198
|
+
collected = #results
|
|
199
|
+
end
|
|
99
200
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
201
|
+
local next_id = claim and claim[1] or start_id
|
|
202
|
+
if next_id == start_id then
|
|
203
|
+
local s, seq = string.match(start_id, '^(%d+)%-(%d+)$')
|
|
204
|
+
if s and seq then
|
|
205
|
+
start_id = s .. '-' .. tostring(tonumber(seq) + 1)
|
|
206
|
+
else
|
|
207
|
+
start_id = '0-1'
|
|
106
208
|
end
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
getPromoteScript() {
|
|
111
|
-
return `
|
|
112
|
-
-- KEYS: 1=delayed, 2=ready
|
|
113
|
-
-- ARGV: 1=now, 2=limit
|
|
114
|
-
local delayed = KEYS[1]
|
|
115
|
-
local ready = KEYS[2]
|
|
116
|
-
local now = tonumber(ARGV[1])
|
|
117
|
-
local limit = tonumber(ARGV[2])
|
|
209
|
+
else
|
|
210
|
+
start_id = next_id
|
|
211
|
+
end
|
|
118
212
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
213
|
+
if time_exceeded() then
|
|
214
|
+
break
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
local left = count - collected
|
|
219
|
+
if left > 0 then
|
|
220
|
+
local xr = redis.call('XREADGROUP', 'GROUP', group, consumer, 'COUNT', left, 'STREAMS', stream, '>')
|
|
221
|
+
if xr and xr[1] and xr[1][2] then
|
|
222
|
+
local entries = xr[1][2]
|
|
223
|
+
for i = 1, #entries do
|
|
224
|
+
results[#results+1] = entries[i]
|
|
124
225
|
end
|
|
125
|
-
|
|
126
|
-
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
return results
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
// src/PowerQueues.ts
|
|
233
|
+
var PowerQueues = class extends PowerRedis {
|
|
234
|
+
constructor() {
|
|
235
|
+
super(...arguments);
|
|
236
|
+
this.abort = new AbortController();
|
|
237
|
+
this.strictCheckingConnection = ["true", "on", "yes", "y", "1"].includes(String(process.env.REDIS_STRICT_CHECK_CONNECTION ?? "").trim().toLowerCase());
|
|
238
|
+
this.scripts = {};
|
|
239
|
+
this.addingBatchTasksCount = 800;
|
|
240
|
+
this.addingBatchKeysLimit = 1e4;
|
|
241
|
+
this.idemOn = true;
|
|
242
|
+
this.idemKey = "";
|
|
243
|
+
this.workerExecuteLockTimeoutMs = 18e4;
|
|
244
|
+
this.workerCacheTaskTimeoutMs = 60;
|
|
245
|
+
this.approveBatchTasksCount = 2e3;
|
|
246
|
+
this.removeOnExecuted = false;
|
|
247
|
+
this.executeBatchAtOnce = false;
|
|
248
|
+
this.executeJobStatus = false;
|
|
249
|
+
this.executeJobStatusTtlSec = 300;
|
|
250
|
+
this.consumerHost = "host";
|
|
251
|
+
this.stream = "stream";
|
|
252
|
+
this.group = "group";
|
|
253
|
+
this.workerBatchTasksCount = 200;
|
|
254
|
+
this.recoveryStuckTasksTimeoutMs = 6e4;
|
|
255
|
+
this.workerLoopIntervalMs = 5e3;
|
|
256
|
+
this.workerSelectionTimeoutMs = 80;
|
|
257
|
+
}
|
|
258
|
+
async onSelected(data) {
|
|
259
|
+
return data;
|
|
127
260
|
}
|
|
128
|
-
async
|
|
129
|
-
if (!force && (this.reserveSha || this.reserveShaRpoplpush || !isFunc(this.redis?.script))) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
this.reserveSha = void 0;
|
|
133
|
-
this.reserveShaRpoplpush = void 0;
|
|
134
|
-
try {
|
|
135
|
-
this.reserveSha = await this.redis?.script("LOAD", this.getReserveScriptLMOVE());
|
|
136
|
-
} catch {
|
|
137
|
-
this.reserveShaRpoplpush = await this.redis?.script("LOAD", this.getReserveScriptRPOPLPUSH());
|
|
138
|
-
}
|
|
261
|
+
async onExecute(id, payload, createdAt, job, key) {
|
|
139
262
|
}
|
|
140
|
-
async
|
|
141
|
-
if (!force && this.requeueSha) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const scriptFn = this.redis?.script;
|
|
145
|
-
if (!scriptFn) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
this.requeueSha = await scriptFn("LOAD", this.getRequeueScript());
|
|
263
|
+
async onExecuted(data) {
|
|
149
264
|
}
|
|
150
|
-
async
|
|
151
|
-
if (!force && this.promoteSha) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const scriptFn = this.redis?.script;
|
|
155
|
-
if (!scriptFn) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
this.promoteSha = await scriptFn("LOAD", this.getPromoteScript());
|
|
265
|
+
async onSuccess(id, payload, createdAt, job, key) {
|
|
159
266
|
}
|
|
160
|
-
async
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
267
|
+
async runQueue() {
|
|
268
|
+
await this.createGroup("0-0");
|
|
269
|
+
await this.consumerLoop();
|
|
270
|
+
}
|
|
271
|
+
async consumerLoop() {
|
|
272
|
+
const signal = this.signal();
|
|
273
|
+
while (!signal?.aborted) {
|
|
274
|
+
try {
|
|
275
|
+
const tasks = await this.select();
|
|
276
|
+
if (!Array.isArray(tasks) || !(tasks.length > 0)) {
|
|
277
|
+
await wait(600);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const tasksP = await this.onSelected(tasks);
|
|
281
|
+
const ids = await this.execute(Array.isArray(tasksP) && tasksP.length > 0 ? tasksP : tasks);
|
|
282
|
+
if (Array.isArray(ids) && ids.length > 0) {
|
|
283
|
+
await this.approve(ids);
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
await wait(600);
|
|
173
287
|
}
|
|
174
|
-
} catch {
|
|
175
288
|
}
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
289
|
+
}
|
|
290
|
+
async addTasks(queueName, data, opts = {}) {
|
|
291
|
+
if (!Array.isArray(data) || !(data.length > 0)) {
|
|
292
|
+
throw new Error("Tasks is not filled.");
|
|
293
|
+
}
|
|
294
|
+
if (typeof queueName !== "string" || !(queueName.length > 0)) {
|
|
295
|
+
throw new Error("Queue name is required.");
|
|
296
|
+
}
|
|
297
|
+
const batches = this.buildBatches(data);
|
|
298
|
+
const result = new Array(data.length);
|
|
299
|
+
const promises = [];
|
|
300
|
+
let cursor = 0;
|
|
301
|
+
for (const batch of batches) {
|
|
302
|
+
const start = cursor;
|
|
303
|
+
const end = start + batch.length;
|
|
304
|
+
cursor = end;
|
|
305
|
+
promises.push(async () => {
|
|
306
|
+
const partIds = await this.xaddBatch(queueName, ...this.payloadBatch(batch, opts));
|
|
307
|
+
for (let k = 0; k < partIds.length; k++) {
|
|
308
|
+
result[start + k] = partIds[k];
|
|
309
|
+
}
|
|
310
|
+
});
|
|
184
311
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
await ensure(true);
|
|
191
|
-
const sha2 = shaGetter();
|
|
192
|
-
if (!sha2) {
|
|
193
|
-
throw new Error("EVALSHA NOSCRIPT and reload failed (no SHA)");
|
|
312
|
+
const runners = Array.from({ length: promises.length }, async () => {
|
|
313
|
+
while (promises.length) {
|
|
314
|
+
const promise = promises.shift();
|
|
315
|
+
if (promise) {
|
|
316
|
+
await promise();
|
|
194
317
|
}
|
|
195
|
-
return await evalshaFn(sha2, numKeys, ...keysAndArgs.map(String));
|
|
196
318
|
}
|
|
197
|
-
|
|
198
|
-
|
|
319
|
+
});
|
|
320
|
+
await Promise.all(runners);
|
|
321
|
+
return result;
|
|
199
322
|
}
|
|
200
|
-
async
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
323
|
+
async loadScripts(full = false) {
|
|
324
|
+
const scripts = full ? [
|
|
325
|
+
["XAddBulk", XAddBulk],
|
|
326
|
+
["Approve", Approve],
|
|
327
|
+
["IdempotencyAllow", IdempotencyAllow],
|
|
328
|
+
["IdempotencyStart", IdempotencyStart],
|
|
329
|
+
["IdempotencyDone", IdempotencyDone],
|
|
330
|
+
["IdempotencyFree", IdempotencyFree],
|
|
331
|
+
["SelectStuck", SelectStuck]
|
|
332
|
+
] : [
|
|
333
|
+
["XAddBulk", XAddBulk]
|
|
334
|
+
];
|
|
335
|
+
for (const [name, code] of scripts) {
|
|
336
|
+
await this.loadScript(this.saveScript(name, code));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async loadScript(code) {
|
|
340
|
+
for (let i = 0; i < 3; i++) {
|
|
341
|
+
try {
|
|
342
|
+
return await this.redis.script("LOAD", code);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
if (i === 2) {
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
await new Promise((r) => setTimeout(r, 10 + Math.floor(Math.random() * 40)));
|
|
213
348
|
}
|
|
214
|
-
} catch {
|
|
215
|
-
}
|
|
216
|
-
try {
|
|
217
|
-
await this.redis.zadd(key, score, member);
|
|
218
|
-
} catch {
|
|
219
349
|
}
|
|
350
|
+
throw new Error("Load lua script failed.");
|
|
220
351
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
const vtKey = this.processingVtKey(task.queueName);
|
|
227
|
-
const periodMs = Math.max(1e3, Math.floor(this.visibilityTimeoutSec * 1e3 * 0.4));
|
|
228
|
-
const t = setInterval(() => {
|
|
229
|
-
this.extendVisibility(vtKey, raw, this.visibilityTimeoutSec).catch(() => {
|
|
230
|
-
});
|
|
231
|
-
}, periodMs);
|
|
232
|
-
t.unref?.();
|
|
233
|
-
this.heartbeatTimers.set(task.id, t);
|
|
234
|
-
}
|
|
235
|
-
stopHeartbeat(task) {
|
|
236
|
-
const t = this.heartbeatTimers.get(task.id);
|
|
237
|
-
if (t) {
|
|
238
|
-
clearInterval(t);
|
|
352
|
+
saveScript(name, codeBody) {
|
|
353
|
+
if (typeof codeBody !== "string" || !(codeBody.length > 0)) {
|
|
354
|
+
throw new Error("Script body is empty.");
|
|
239
355
|
}
|
|
240
|
-
this.
|
|
356
|
+
this.scripts[name] = { codeBody };
|
|
357
|
+
return codeBody;
|
|
241
358
|
}
|
|
242
|
-
async
|
|
243
|
-
if (!this.
|
|
244
|
-
|
|
359
|
+
async runScript(name, keys, args, defaultCode) {
|
|
360
|
+
if (!this.scripts[name]) {
|
|
361
|
+
if (typeof defaultCode !== "string" || !(defaultCode.length > 0)) {
|
|
362
|
+
throw new Error(`Undefined script "${name}". Save it before executing.`);
|
|
363
|
+
}
|
|
364
|
+
this.saveScript(name, defaultCode);
|
|
245
365
|
}
|
|
246
|
-
if (!
|
|
247
|
-
|
|
366
|
+
if (!this.scripts[name].codeReady) {
|
|
367
|
+
this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
|
|
248
368
|
}
|
|
249
|
-
|
|
250
|
-
|
|
369
|
+
try {
|
|
370
|
+
return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (String(err?.message || "").includes("NOSCRIPT")) {
|
|
373
|
+
this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
|
|
374
|
+
return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
|
|
375
|
+
}
|
|
376
|
+
throw err;
|
|
251
377
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
378
|
+
}
|
|
379
|
+
async xaddBatch(queueName, ...batches) {
|
|
380
|
+
return await this.runScript("XAddBulk", [queueName], batches, XAddBulk);
|
|
381
|
+
}
|
|
382
|
+
payloadBatch(data, opts) {
|
|
383
|
+
const maxlen = Math.max(0, Math.floor(opts?.maxlen ?? 0));
|
|
384
|
+
const approx = opts?.exact ? 0 : opts?.approx !== false ? 1 : 0;
|
|
385
|
+
const exact = opts?.exact ? 1 : 0;
|
|
386
|
+
const nomkstream = opts?.nomkstream ? 1 : 0;
|
|
387
|
+
const trimLimit = Math.max(0, Math.floor(opts?.trimLimit ?? 0));
|
|
388
|
+
const minidWindowMs = Math.max(0, Math.floor(opts?.minidWindowMs ?? 0));
|
|
389
|
+
const minidExact = opts?.minidExact ? 1 : 0;
|
|
390
|
+
const argv = [
|
|
391
|
+
String(maxlen),
|
|
392
|
+
String(approx),
|
|
393
|
+
String(data.length),
|
|
394
|
+
String(exact),
|
|
395
|
+
String(nomkstream),
|
|
396
|
+
String(trimLimit),
|
|
397
|
+
String(minidWindowMs),
|
|
398
|
+
String(minidExact)
|
|
399
|
+
];
|
|
400
|
+
for (const item of data) {
|
|
401
|
+
const entry = item;
|
|
402
|
+
const id = entry.id ?? "*";
|
|
403
|
+
let flat;
|
|
404
|
+
if ("flat" in entry && Array.isArray(entry.flat) && entry.flat.length > 0) {
|
|
405
|
+
flat = entry.flat;
|
|
406
|
+
if (flat.length % 2 !== 0) {
|
|
407
|
+
throw new Error('Property "flat" must contain an even number of realKeysLength (field/value pairs).');
|
|
258
408
|
}
|
|
259
|
-
|
|
260
|
-
|
|
409
|
+
} else if ("payload" in entry && typeof entry.payload === "object" && Object.keys(entry.payload || {}).length > 0) {
|
|
410
|
+
flat = [];
|
|
411
|
+
for (const [k, v] of Object.entries(entry.payload)) {
|
|
412
|
+
flat.push(k, v);
|
|
261
413
|
}
|
|
414
|
+
} else {
|
|
415
|
+
throw new Error('Task must have "payload" or "flat".');
|
|
262
416
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const res = await tryEval();
|
|
267
|
-
if (isArr(res)) {
|
|
268
|
-
return Array.from(res).map(String);
|
|
417
|
+
const pairs = flat.length / 2;
|
|
418
|
+
if (pairs <= 0) {
|
|
419
|
+
throw new Error('Task must have "payload" or "flat".');
|
|
269
420
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
421
|
+
argv.push(String(id));
|
|
422
|
+
argv.push(String(pairs));
|
|
423
|
+
for (const token of flat) {
|
|
424
|
+
argv.push(!token ? "" : typeof token === "string" && token.length > 0 ? token : String(token));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return argv;
|
|
428
|
+
}
|
|
429
|
+
buildBatches(tasks) {
|
|
430
|
+
const job = uuid();
|
|
431
|
+
const batches = [];
|
|
432
|
+
let batch = [], realKeysLength = 0;
|
|
433
|
+
for (let task of tasks) {
|
|
434
|
+
let entry = task;
|
|
435
|
+
if (this.idemOn) {
|
|
436
|
+
const createdAt = entry?.createdAt || Date.now();
|
|
437
|
+
let idemKey = entry?.idemKey || uuid();
|
|
438
|
+
if (typeof entry.payload === "object") {
|
|
439
|
+
if (this.idemKey && typeof entry.payload[this.idemKey] === "string" && entry.payload[this.idemKey].length > 0) {
|
|
440
|
+
idemKey = entry.payload[this.idemKey];
|
|
277
441
|
}
|
|
278
|
-
|
|
442
|
+
entry = {
|
|
443
|
+
...entry,
|
|
444
|
+
payload: {
|
|
445
|
+
payload: JSON.stringify(entry.payload),
|
|
446
|
+
createdAt,
|
|
447
|
+
job,
|
|
448
|
+
idemKey
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
} else if (Array.isArray(entry.flat)) {
|
|
452
|
+
entry.flat.push("createdAt");
|
|
453
|
+
entry.flat.push(String(createdAt));
|
|
454
|
+
entry.flat.push("job");
|
|
455
|
+
entry.flat.push(job);
|
|
456
|
+
entry.flat.push("idemKey");
|
|
457
|
+
entry.flat.push(idemKey);
|
|
279
458
|
}
|
|
280
459
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
break;
|
|
460
|
+
const reqKeysLength = this.keysLength(entry);
|
|
461
|
+
if (batch.length && (batch.length >= this.addingBatchTasksCount || realKeysLength + reqKeysLength > this.addingBatchKeysLimit)) {
|
|
462
|
+
batches.push(batch);
|
|
463
|
+
batch = [];
|
|
464
|
+
realKeysLength = 0;
|
|
287
465
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
466
|
+
batch.push(entry);
|
|
467
|
+
realKeysLength += reqKeysLength;
|
|
468
|
+
}
|
|
469
|
+
if (batch.length) {
|
|
470
|
+
batches.push(batch);
|
|
471
|
+
}
|
|
472
|
+
return batches;
|
|
473
|
+
}
|
|
474
|
+
keysLength(task) {
|
|
475
|
+
return 2 + ("flat" in task && Array.isArray(task.flat) && task.flat.length ? task.flat.length : Object.keys(task).length * 2);
|
|
476
|
+
}
|
|
477
|
+
async success(id, payload, createdAt, job, key) {
|
|
478
|
+
if (this.executeJobStatus) {
|
|
479
|
+
await this.status(id, payload, createdAt, job, key);
|
|
480
|
+
}
|
|
481
|
+
await this.onSuccess(id, payload, createdAt, job, key);
|
|
482
|
+
}
|
|
483
|
+
async status(id, payload, createdAt, job, key) {
|
|
484
|
+
const prefix = `s:${this.stream}:`;
|
|
485
|
+
const { ready = 0, ok = 0 } = await this.getMany(prefix);
|
|
486
|
+
await this.setMany([{ key: `${prefix}ready`, value: ready + 1 }, { key: `${prefix}ok`, value: ok + 1 }], this.executeJobStatusTtlSec);
|
|
487
|
+
}
|
|
488
|
+
async execute(tasks) {
|
|
489
|
+
const result = [];
|
|
490
|
+
let contended = 0, promises = [];
|
|
491
|
+
for (const [id, payload, createdAt, job, idemKey] of tasks) {
|
|
492
|
+
if (this.executeBatchAtOnce) {
|
|
493
|
+
promises.push((async () => {
|
|
494
|
+
const r = await this.executeProcess(id, payload, createdAt, job, idemKey);
|
|
495
|
+
if (r.id) {
|
|
496
|
+
result.push(id);
|
|
497
|
+
} else if (r.contended) {
|
|
498
|
+
contended++;
|
|
499
|
+
}
|
|
500
|
+
})());
|
|
501
|
+
} else {
|
|
502
|
+
const r = await this.executeProcess(id, payload, createdAt, job, idemKey);
|
|
503
|
+
if (r.id) {
|
|
504
|
+
result.push(id);
|
|
505
|
+
} else if (r.contended) {
|
|
506
|
+
contended++;
|
|
507
|
+
}
|
|
294
508
|
}
|
|
295
|
-
await tx.exec();
|
|
296
509
|
}
|
|
297
|
-
return moved;
|
|
298
|
-
}
|
|
299
|
-
async ackProcessing(processing, processingVt, raw) {
|
|
300
|
-
if (!this.checkConnection()) {
|
|
301
|
-
throw new Error("Redis connection error.");
|
|
302
|
-
}
|
|
303
|
-
const tx = this.redis?.multi();
|
|
304
|
-
tx.lrem(processing, 1, raw);
|
|
305
|
-
tx.zrem(processingVt, raw);
|
|
306
|
-
await tx.exec();
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
async requeueExpired(processing, processingVt, ready, nowTs, chunk = 1e3) {
|
|
310
|
-
if (!this.checkConnection()) {
|
|
311
|
-
throw new Error("Redis connection error.");
|
|
312
|
-
}
|
|
313
|
-
const now = isNumP(nowTs) ? nowTs : this.nowSec();
|
|
314
510
|
try {
|
|
315
|
-
|
|
316
|
-
()
|
|
317
|
-
(force) => this.ensureRequeueScript(!!force),
|
|
318
|
-
3,
|
|
319
|
-
[processing, processingVt, ready, String(now), String(chunk)]
|
|
320
|
-
);
|
|
321
|
-
return isNumP(moved) ? moved : 0;
|
|
322
|
-
} catch {
|
|
323
|
-
const expired = await this.redis?.zrangebyscore(processingVt, 0, now, "LIMIT", 0, chunk);
|
|
324
|
-
if (!isArrFilled(expired)) {
|
|
325
|
-
return 0;
|
|
511
|
+
if (this.executeBatchAtOnce && promises.length > 0) {
|
|
512
|
+
await Promise.all(promises);
|
|
326
513
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
tx.zrem(processingVt, raw);
|
|
331
|
-
tx.rpush(ready, raw);
|
|
514
|
+
await this.onExecuted(tasks);
|
|
515
|
+
if ((!Array.isArray(result) || !(result.length > 0)) && contended > tasks.length >> 1) {
|
|
516
|
+
await this.waitAbortable(15 + Math.floor(Math.random() * 35) + Math.min(250, 15 * contended + Math.floor(Math.random() * 40)));
|
|
332
517
|
}
|
|
333
|
-
|
|
334
|
-
return expired.length;
|
|
518
|
+
} catch (err) {
|
|
335
519
|
}
|
|
520
|
+
return result;
|
|
336
521
|
}
|
|
337
|
-
async
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
2,
|
|
347
|
-
[delayed, ready, String(now), String(chunk)]
|
|
348
|
-
);
|
|
349
|
-
return isNumP(promoted) ? promoted : 0;
|
|
350
|
-
} catch {
|
|
351
|
-
const due = await this.redis?.zrangebyscore(delayed, 0, now, "LIMIT", 0, chunk);
|
|
352
|
-
if (!isArrFilled(due)) {
|
|
353
|
-
return 0;
|
|
354
|
-
}
|
|
355
|
-
const tx = this.redis?.multi();
|
|
356
|
-
for (const raw of due) {
|
|
357
|
-
tx.zrem(delayed, raw);
|
|
358
|
-
tx.rpush(ready, raw);
|
|
522
|
+
async executeProcess(id, payload, createdAt, job, key) {
|
|
523
|
+
if (key) {
|
|
524
|
+
return await this.idempotency(id, payload, createdAt, job, key);
|
|
525
|
+
} else {
|
|
526
|
+
try {
|
|
527
|
+
await this.onExecute(id, payload, createdAt, job, key);
|
|
528
|
+
await this.success(id, payload, createdAt, job, key);
|
|
529
|
+
return { id };
|
|
530
|
+
} catch (err) {
|
|
359
531
|
}
|
|
360
|
-
await tx.exec();
|
|
361
|
-
return due.length;
|
|
362
532
|
}
|
|
533
|
+
return {};
|
|
363
534
|
}
|
|
364
|
-
async
|
|
365
|
-
if (!
|
|
366
|
-
|
|
535
|
+
async approve(ids) {
|
|
536
|
+
if (!Array.isArray(ids) || !(ids.length > 0)) {
|
|
537
|
+
return 0;
|
|
367
538
|
}
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
539
|
+
const approveBatchTasksCount = Math.max(500, Math.min(4e3, this.approveBatchTasksCount));
|
|
540
|
+
let total = 0, i = 0;
|
|
541
|
+
while (i < ids.length) {
|
|
542
|
+
const room = Math.min(approveBatchTasksCount, ids.length - i);
|
|
543
|
+
const part = ids.slice(i, i + room);
|
|
544
|
+
const approved = await this.runScript("Approve", [this.stream], [this.group, this.removeOnExecuted ? "1" : "0", ...part], Approve);
|
|
545
|
+
total += Number(approved || 0);
|
|
546
|
+
i += room;
|
|
372
547
|
}
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
async extendVisibility(processingVt, raw, visibilitySec) {
|
|
376
|
-
const deadline = this.nowSec() + Math.max(1, visibilitySec);
|
|
377
|
-
await this.zaddCompatXXCH(processingVt, deadline, raw);
|
|
548
|
+
return total;
|
|
378
549
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
550
|
+
async idempotency(id, payload, createdAt, job, key) {
|
|
551
|
+
const keys = this.idempotencyKeys(key);
|
|
552
|
+
const allow = await this.idempotencyAllow(keys);
|
|
553
|
+
if (allow === 1) {
|
|
554
|
+
return { id };
|
|
555
|
+
} else if (allow === 0) {
|
|
556
|
+
let ttl = -2;
|
|
557
|
+
try {
|
|
558
|
+
ttl = await this.redis.pttl(keys.startKey);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
}
|
|
561
|
+
await this.waitAbortable(ttl);
|
|
562
|
+
return { contended: true };
|
|
382
563
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
throw new Error(`Queue "${queueName}" already started.`);
|
|
564
|
+
if (!await this.idempotencyStart(keys)) {
|
|
565
|
+
return { contended: true };
|
|
386
566
|
}
|
|
387
|
-
|
|
388
|
-
this.runners.set(queueName, r);
|
|
389
|
-
this.loop(queueName, r).catch(() => {
|
|
390
|
-
r.running = false;
|
|
567
|
+
const heartbeat = this.heartbeat(keys) || (() => {
|
|
391
568
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
569
|
+
try {
|
|
570
|
+
await this.onExecute(id, payload, createdAt, job, key);
|
|
571
|
+
await this.idempotencyDone(keys);
|
|
572
|
+
await this.success(id, payload, createdAt, job, key);
|
|
573
|
+
return { id };
|
|
574
|
+
} catch (err) {
|
|
575
|
+
try {
|
|
576
|
+
await this.idempotencyFree(keys);
|
|
577
|
+
} catch (err2) {
|
|
578
|
+
}
|
|
579
|
+
} finally {
|
|
580
|
+
heartbeat();
|
|
398
581
|
}
|
|
399
582
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
583
|
+
idempotencyKeys(key) {
|
|
584
|
+
const prefix = `q:${this.stream.replace(/[^\w:\-]/g, "_")}:`;
|
|
585
|
+
const keyP = key.replace(/[^\w:\-]/g, "_");
|
|
586
|
+
const doneKey = `${prefix}done:${keyP}`;
|
|
587
|
+
const lockKey = `${prefix}lock:${keyP}`;
|
|
588
|
+
const startKey = `${prefix}start:${keyP}`;
|
|
589
|
+
const token = `${this.consumer()}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
407
590
|
return {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
currentAttempt: isNumPZ(data.currentAttempt) ? data.currentAttempt : 0,
|
|
414
|
-
chain: isObjFilled(data.chain) && isArrFilled(data.chain.queues) && isNumPZ(data.chain.index) ? data.chain : {
|
|
415
|
-
queues: [],
|
|
416
|
-
index: 0
|
|
417
|
-
},
|
|
418
|
-
payload: isObjFilled(data.payload) ? data.payload : {},
|
|
419
|
-
progress: {
|
|
420
|
-
createdAt: Date.now(),
|
|
421
|
-
successAt: 0,
|
|
422
|
-
errorAt: 0,
|
|
423
|
-
failAt: 0,
|
|
424
|
-
fatalAt: 0,
|
|
425
|
-
retries: [],
|
|
426
|
-
chain: [],
|
|
427
|
-
...isObjFilled(data.progress) ? data.progress : {}
|
|
428
|
-
},
|
|
429
|
-
result: isObjFilled(data.result) ? data.result : {}
|
|
591
|
+
prefix,
|
|
592
|
+
doneKey,
|
|
593
|
+
lockKey,
|
|
594
|
+
startKey,
|
|
595
|
+
token
|
|
430
596
|
};
|
|
431
597
|
}
|
|
432
|
-
async
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
return await this.enqueue(ready, delayed, this.buildTask(data), isNumP(delaySec) ? delaySec : 0);
|
|
436
|
-
}
|
|
437
|
-
async addTasks(data) {
|
|
438
|
-
if (!this.checkConnection()) {
|
|
439
|
-
throw new Error("Redis connection error.");
|
|
440
|
-
}
|
|
441
|
-
if (!isObjFilled(data) || !isStrFilled(data.queueName)) {
|
|
442
|
-
throw new Error("Queue name is not valid.");
|
|
443
|
-
}
|
|
444
|
-
if (!isArrFilled(data.payloads)) {
|
|
445
|
-
return 0;
|
|
446
|
-
}
|
|
447
|
-
const queueName = String(data.queueName);
|
|
448
|
-
const ready = this.readyKey(queueName);
|
|
449
|
-
const delayed = this.delayedKey(queueName);
|
|
450
|
-
const now = this.nowSec();
|
|
451
|
-
const uniformDelay = isNumP(data.delaySec) ? Math.max(0, Number(data.delaySec)) : void 0;
|
|
452
|
-
const perItemDelays = isArr(data.delaySec) ? data.delaySec.map((v) => Math.max(0, Number(v || 0))) : void 0;
|
|
453
|
-
const batchSize = Math.max(1, Math.min(this.portionLength, 1e3));
|
|
454
|
-
let idx = 0, total = 0;
|
|
455
|
-
while (idx < data.payloads.length) {
|
|
456
|
-
const end = Math.min(idx + batchSize, data.payloads.length);
|
|
457
|
-
const tx = this.redis?.multi();
|
|
458
|
-
for (let i = idx; i < end; i++) {
|
|
459
|
-
const item = data.payloads[i];
|
|
460
|
-
let partial;
|
|
461
|
-
if (isObjFilled(item) && Object.prototype.hasOwnProperty.call(item, "payload")) {
|
|
462
|
-
partial = { ...item, queueName };
|
|
463
|
-
} else {
|
|
464
|
-
partial = { queueName, payload: item };
|
|
465
|
-
}
|
|
466
|
-
const task = this.buildTask(partial);
|
|
467
|
-
const raw = this.toPayload(task);
|
|
468
|
-
let d = 0;
|
|
469
|
-
if (isNumP(uniformDelay)) {
|
|
470
|
-
d = uniformDelay;
|
|
471
|
-
} else if (isArr(perItemDelays)) {
|
|
472
|
-
d = Number(perItemDelays[i] || 0);
|
|
473
|
-
}
|
|
474
|
-
if (d > 0) {
|
|
475
|
-
tx.zadd(delayed, now + d, raw);
|
|
476
|
-
} else {
|
|
477
|
-
tx.rpush(ready, raw);
|
|
478
|
-
}
|
|
479
|
-
total++;
|
|
480
|
-
}
|
|
481
|
-
await tx.exec();
|
|
482
|
-
idx = end;
|
|
483
|
-
}
|
|
484
|
-
return total;
|
|
485
|
-
}
|
|
486
|
-
async iteration(tasks) {
|
|
487
|
-
const tasksProcessed = await this.beforeIterationExecution(tasks);
|
|
488
|
-
const limit = Math.max(1, Number(this.concurrency) || 1);
|
|
489
|
-
let i = 0;
|
|
490
|
-
while (i < tasksProcessed.length) {
|
|
491
|
-
const slice = tasksProcessed.slice(i, i + limit);
|
|
492
|
-
await Promise.all(slice.map((task) => this.logic(task)));
|
|
493
|
-
i += limit;
|
|
494
|
-
}
|
|
495
|
-
await this.afterIterationExecution(tasksProcessed, tasksProcessed.map((t) => t.result ?? {}));
|
|
496
|
-
}
|
|
497
|
-
async beforeIterationExecution(data) {
|
|
498
|
-
return data;
|
|
499
|
-
}
|
|
500
|
-
async afterIterationExecution(data, results) {
|
|
501
|
-
}
|
|
502
|
-
async beforeExecution(task) {
|
|
503
|
-
return task;
|
|
504
|
-
}
|
|
505
|
-
async afterExecution(task, result) {
|
|
506
|
-
return result;
|
|
507
|
-
}
|
|
508
|
-
async execute(task) {
|
|
509
|
-
return {};
|
|
598
|
+
async idempotencyAllow(keys) {
|
|
599
|
+
const res = await this.runScript("IdempotencyAllow", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.workerExecuteLockTimeoutMs), keys.token], IdempotencyAllow);
|
|
600
|
+
return Number(res || 0);
|
|
510
601
|
}
|
|
511
|
-
async
|
|
602
|
+
async idempotencyStart(keys) {
|
|
603
|
+
const res = await this.runScript("IdempotencyStart", [keys.lockKey, keys.startKey], [keys.token, String(this.workerExecuteLockTimeoutMs)], IdempotencyStart);
|
|
604
|
+
return Number(res || 0) === 1;
|
|
512
605
|
}
|
|
513
|
-
async
|
|
606
|
+
async idempotencyDone(keys) {
|
|
607
|
+
await this.runScript("IdempotencyDone", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.workerCacheTaskTimeoutMs), keys.token], IdempotencyDone);
|
|
514
608
|
}
|
|
515
|
-
async
|
|
609
|
+
async idempotencyFree(keys) {
|
|
610
|
+
await this.runScript("IdempotencyFree", [keys.lockKey, keys.startKey], [keys.token], IdempotencyFree);
|
|
516
611
|
}
|
|
517
|
-
async
|
|
518
|
-
}
|
|
519
|
-
async onSuccess(task, result) {
|
|
520
|
-
}
|
|
521
|
-
async onChainSuccess(task, result) {
|
|
522
|
-
}
|
|
523
|
-
async onIterationError(err, queueName) {
|
|
524
|
-
}
|
|
525
|
-
async logic(task) {
|
|
526
|
-
let data = task;
|
|
612
|
+
async createGroup(from = "$") {
|
|
527
613
|
try {
|
|
528
|
-
|
|
529
|
-
const before = data?.result ?? {};
|
|
530
|
-
const after = await this.execute(data);
|
|
531
|
-
data.result = {
|
|
532
|
-
...isObjFilled(before) ? before : {},
|
|
533
|
-
...isObjFilled(after) ? after : {}
|
|
534
|
-
};
|
|
535
|
-
await this.success(data, data.result);
|
|
536
|
-
return await this.afterExecution(data, data.result);
|
|
614
|
+
await this.redis.xgroup("CREATE", this.stream, this.group, from, "MKSTREAM");
|
|
537
615
|
} catch (err) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
await this.error(err2, data);
|
|
542
|
-
}
|
|
543
|
-
} finally {
|
|
544
|
-
try {
|
|
545
|
-
this.stopHeartbeat(data);
|
|
546
|
-
await this.ack(data).catch(() => {
|
|
547
|
-
});
|
|
548
|
-
} catch {
|
|
616
|
+
const msg = String(err?.message || "");
|
|
617
|
+
if (!msg.includes("BUSYGROUP")) {
|
|
618
|
+
throw err;
|
|
549
619
|
}
|
|
550
620
|
}
|
|
551
|
-
return {};
|
|
552
621
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const jitter = Math.floor(Math.random() * base);
|
|
558
|
-
return Math.min(maxD, pow + jitter);
|
|
559
|
-
}
|
|
560
|
-
async retry(task) {
|
|
561
|
-
if (!isObjFilled(task) || !isStrFilled(task.iterationId) || !isStrFilled(task.id) || !isStrFilled(task.queueName) || !isNumPZ(task.currentAttempt) || !isNumPZ(task.maxAttempts)) {
|
|
562
|
-
await this.error(new Error("Task format error."), task);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
const maxAttempts = task.maxAttempts ?? this.maxAttempts;
|
|
566
|
-
try {
|
|
567
|
-
if (task.currentAttempt < maxAttempts - 1) {
|
|
568
|
-
const taskProcessed = { ...task, currentAttempt: task.currentAttempt + 1 };
|
|
569
|
-
const delaySec = this.jitteredBackoffSec(taskProcessed.currentAttempt);
|
|
570
|
-
await this.addTask(taskProcessed, delaySec);
|
|
571
|
-
await this.onRetry(taskProcessed);
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
} catch (err) {
|
|
575
|
-
await this.fail(err, task);
|
|
576
|
-
return;
|
|
622
|
+
async select() {
|
|
623
|
+
let entries = await this.selectStuck();
|
|
624
|
+
if (!entries?.length) {
|
|
625
|
+
entries = await this.selectFresh();
|
|
577
626
|
}
|
|
578
|
-
|
|
627
|
+
return this.normalizeEntries(entries);
|
|
579
628
|
}
|
|
580
|
-
async
|
|
629
|
+
async selectStuck() {
|
|
581
630
|
try {
|
|
582
|
-
await this.
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
this.processingRaw.delete(t.id);
|
|
631
|
+
const res = await this.runScript("SelectStuck", [this.stream], [this.group, this.consumer(), String(this.recoveryStuckTasksTimeoutMs), String(this.workerBatchTasksCount), String(this.workerSelectionTimeoutMs)], SelectStuck);
|
|
632
|
+
return Array.isArray(res) ? res : [];
|
|
633
|
+
} catch (err) {
|
|
634
|
+
if (String(err?.message || "").includes("NOGROUP")) {
|
|
635
|
+
await this.createGroup();
|
|
588
636
|
}
|
|
589
637
|
}
|
|
638
|
+
return [];
|
|
590
639
|
}
|
|
591
|
-
async
|
|
640
|
+
async selectFresh() {
|
|
641
|
+
let entries = [];
|
|
592
642
|
try {
|
|
593
|
-
await this.
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
643
|
+
const res = await this.redis.xreadgroup(
|
|
644
|
+
"GROUP",
|
|
645
|
+
this.group,
|
|
646
|
+
this.consumer(),
|
|
647
|
+
"BLOCK",
|
|
648
|
+
Math.max(2, this.workerLoopIntervalMs | 0),
|
|
649
|
+
"COUNT",
|
|
650
|
+
this.workerBatchTasksCount,
|
|
651
|
+
"STREAMS",
|
|
652
|
+
this.stream,
|
|
653
|
+
">"
|
|
654
|
+
);
|
|
655
|
+
if (!res?.[0]?.[1]?.length) {
|
|
656
|
+
return [];
|
|
607
657
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
try {
|
|
616
|
-
await this.addTask({
|
|
617
|
-
...task,
|
|
618
|
-
queueName: [task.queueName, task.iterationId, "fail", "list"].join(":"),
|
|
619
|
-
currentAttempt: 0,
|
|
620
|
-
payload: {
|
|
621
|
-
...task.payload,
|
|
622
|
-
errorMessage: String(err?.message ?? "")
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
await this.onFail(err, task);
|
|
626
|
-
} catch (err2) {
|
|
627
|
-
try {
|
|
628
|
-
await this.onFatal(err2, task);
|
|
629
|
-
} catch {
|
|
658
|
+
entries = res?.[0]?.[1] ?? [];
|
|
659
|
+
if (!entries?.length) {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
} catch (err) {
|
|
663
|
+
if (String(err?.message || "").includes("NOGROUP")) {
|
|
664
|
+
await this.createGroup();
|
|
630
665
|
}
|
|
631
666
|
}
|
|
632
|
-
|
|
633
|
-
await this.status(task, "fail");
|
|
634
|
-
} catch {
|
|
635
|
-
}
|
|
667
|
+
return entries;
|
|
636
668
|
}
|
|
637
|
-
async
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
successAt: Date.now()
|
|
669
|
+
async waitAbortable(ttl) {
|
|
670
|
+
return new Promise((resolve) => {
|
|
671
|
+
const signal = this.signal();
|
|
672
|
+
if (signal?.aborted) {
|
|
673
|
+
return resolve();
|
|
643
674
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const currentIndex = taskProcessed.chain.index;
|
|
648
|
-
const newIndex = currentIndex + 1;
|
|
649
|
-
taskProcessed.progress.chain.push(Date.now());
|
|
650
|
-
if (currentIndex === taskProcessed.chain.queues.length - 1) {
|
|
651
|
-
await this.status(taskProcessed, "success");
|
|
652
|
-
await this.onChainSuccess(taskProcessed, result);
|
|
653
|
-
} else if (newIndex <= taskProcessed.chain.queues.length - 1) {
|
|
654
|
-
const newQueueName = taskProcessed.chain.queues[newIndex];
|
|
655
|
-
if (isStrFilled(newQueueName)) {
|
|
656
|
-
await this.addTask({
|
|
657
|
-
...taskProcessed,
|
|
658
|
-
queueName: newQueueName,
|
|
659
|
-
currentAttempt: 0,
|
|
660
|
-
chain: {
|
|
661
|
-
...taskProcessed.chain,
|
|
662
|
-
index: newIndex
|
|
663
|
-
},
|
|
664
|
-
result
|
|
665
|
-
});
|
|
666
|
-
} else {
|
|
667
|
-
await this.fail(new Error("Next queue format error."), taskProcessed);
|
|
668
|
-
}
|
|
675
|
+
const t = setTimeout(() => {
|
|
676
|
+
if (signal) {
|
|
677
|
+
signal.removeEventListener("abort", onAbort);
|
|
669
678
|
}
|
|
670
|
-
|
|
671
|
-
|
|
679
|
+
resolve();
|
|
680
|
+
}, ttl > 0 ? 25 + Math.floor(Math.random() * 50) : 5 + Math.floor(Math.random() * 15));
|
|
681
|
+
t.unref?.();
|
|
682
|
+
function onAbort() {
|
|
683
|
+
clearTimeout(t);
|
|
684
|
+
resolve();
|
|
672
685
|
}
|
|
673
|
-
|
|
674
|
-
}
|
|
675
|
-
try {
|
|
676
|
-
await this.status(taskProcessed, "fatal");
|
|
677
|
-
} catch {
|
|
678
|
-
}
|
|
679
|
-
try {
|
|
680
|
-
await this.onFatal(err, taskProcessed);
|
|
681
|
-
} catch {
|
|
682
|
-
}
|
|
683
|
-
}
|
|
686
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
687
|
+
});
|
|
684
688
|
}
|
|
685
|
-
|
|
686
|
-
if (
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
const processedKey = this.toKeyString(task.queueName, task.iterationId, "processed");
|
|
690
|
-
const categoryKey = this.toKeyString(task.queueName, task.iterationId, category);
|
|
691
|
-
await this.redis?.incr(processedKey);
|
|
692
|
-
await this.redis?.incr(categoryKey);
|
|
693
|
-
await this.redis?.expire(processedKey, this.expireStatusSec);
|
|
694
|
-
await this.redis?.expire(categoryKey, this.expireStatusSec);
|
|
695
|
-
}
|
|
696
|
-
async loop(queueName, runner) {
|
|
697
|
-
if (!isStrFilled(queueName)) {
|
|
698
|
-
throw new Error(`Queue name is not valid: "${queueName}"; Type: "${typeof queueName}".`);
|
|
689
|
+
heartbeat(keys) {
|
|
690
|
+
if (this.workerExecuteLockTimeoutMs <= 0) {
|
|
691
|
+
return;
|
|
699
692
|
}
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
await wait(this.iterationTimeout);
|
|
707
|
-
continue;
|
|
693
|
+
let timer, alive = true, hbFails = 0;
|
|
694
|
+
const workerHeartbeatTimeoutMs = Math.max(1e3, Math.floor(Math.max(5e3, this.workerExecuteLockTimeoutMs | 0) / 4));
|
|
695
|
+
const stop = () => {
|
|
696
|
+
alive = false;
|
|
697
|
+
if (timer) {
|
|
698
|
+
clearTimeout(timer);
|
|
708
699
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
700
|
+
};
|
|
701
|
+
const onAbort = () => stop();
|
|
702
|
+
const signal = this.signal();
|
|
703
|
+
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
704
|
+
const tick = async () => {
|
|
705
|
+
if (!alive) {
|
|
706
|
+
return;
|
|
712
707
|
}
|
|
713
708
|
try {
|
|
714
|
-
await this.
|
|
709
|
+
const r = await this.heartbeat(keys);
|
|
710
|
+
hbFails = r ? 0 : hbFails + 1;
|
|
711
|
+
if (hbFails >= 3) {
|
|
712
|
+
throw new Error("Heartbeat lost.");
|
|
713
|
+
}
|
|
715
714
|
} catch {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (!isArrFilled(data)) {
|
|
721
|
-
await wait(this.iterationTimeout);
|
|
722
|
-
continue;
|
|
715
|
+
hbFails++;
|
|
716
|
+
if (hbFails >= 6) {
|
|
717
|
+
stop();
|
|
718
|
+
return;
|
|
723
719
|
}
|
|
724
|
-
await this.iteration(data);
|
|
725
|
-
} catch (err) {
|
|
726
|
-
await this.iterationError(err, queueName, data);
|
|
727
|
-
await wait(this.iterationTimeout);
|
|
728
720
|
}
|
|
729
|
-
|
|
721
|
+
timer = setTimeout(tick, workerHeartbeatTimeoutMs).unref?.();
|
|
722
|
+
};
|
|
723
|
+
timer = setTimeout(tick, workerHeartbeatTimeoutMs).unref?.();
|
|
724
|
+
return () => {
|
|
725
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
726
|
+
stop();
|
|
727
|
+
};
|
|
730
728
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const processing = this.processingKey(queueName);
|
|
734
|
-
const processingVt = this.processingVtKey(queueName);
|
|
735
|
-
const raws = await this.reserveMany(ready, processing, processingVt, this.portionLength, this.visibilityTimeoutSec);
|
|
736
|
-
if (!isArrFilled(raws)) {
|
|
729
|
+
normalizeEntries(raw) {
|
|
730
|
+
if (!Array.isArray(raw)) {
|
|
737
731
|
return [];
|
|
738
732
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
733
|
+
return Array.from(raw || []).map((e) => {
|
|
734
|
+
const id = Buffer.isBuffer(e?.[0]) ? e[0].toString() : e?.[0];
|
|
735
|
+
const kvRaw = e?.[1] ?? [];
|
|
736
|
+
const kv = Array.isArray(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
|
|
737
|
+
return [id, kv, 0, "", ""];
|
|
738
|
+
}).filter(([id, kv]) => typeof id === "string" && id.length > 0 && Array.isArray(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
|
|
739
|
+
const values = this.values(kv);
|
|
740
|
+
const { idemKey = "", createdAt, job, ...data } = this.payload(values);
|
|
741
|
+
return [id, data, createdAt, job, idemKey];
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
values(value) {
|
|
745
|
+
const result = {};
|
|
746
|
+
for (let i = 0; i < value.length; i += 2) {
|
|
747
|
+
result[value[i]] = value[i + 1];
|
|
748
748
|
}
|
|
749
|
-
return
|
|
749
|
+
return result;
|
|
750
750
|
}
|
|
751
|
-
|
|
751
|
+
payload(data) {
|
|
752
752
|
try {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
this.processingRaw.delete(task.id);
|
|
758
|
-
await this.ackProcessing(this.processingKey(task.queueName), this.processingVtKey(task.queueName), raw);
|
|
759
|
-
} catch {
|
|
753
|
+
return JSON.parse(data?.payload);
|
|
754
|
+
} catch (err) {
|
|
760
755
|
}
|
|
756
|
+
return data;
|
|
757
|
+
}
|
|
758
|
+
signal() {
|
|
759
|
+
return this.abort.signal;
|
|
760
|
+
}
|
|
761
|
+
consumer() {
|
|
762
|
+
return `${String(this.consumerHost || "host")}:${process.pid}`;
|
|
761
763
|
}
|
|
762
764
|
};
|
|
763
765
|
export {
|
|
764
|
-
|
|
766
|
+
PowerQueues
|
|
765
767
|
};
|