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