power-queues 2.0.22 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +374 -429
- package/dist/index.d.cts +50 -62
- package/dist/index.d.ts +50 -62
- package/dist/index.js +375 -430
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
isArrFilled,
|
|
7
7
|
isArr,
|
|
8
8
|
isStrFilled,
|
|
9
|
+
isExists,
|
|
9
10
|
isNumNZ,
|
|
10
|
-
jsonDecode,
|
|
11
11
|
wait
|
|
12
12
|
} from "full-utils";
|
|
13
13
|
import { v4 as uuid } from "uuid";
|
|
@@ -252,315 +252,191 @@ var PowerQueues = class extends PowerRedis {
|
|
|
252
252
|
super(...arguments);
|
|
253
253
|
this.abort = new AbortController();
|
|
254
254
|
this.scripts = {};
|
|
255
|
-
this.
|
|
256
|
-
this.
|
|
257
|
-
this.
|
|
258
|
-
this.
|
|
259
|
-
this.
|
|
255
|
+
this.host = "host";
|
|
256
|
+
this.group = "gr1";
|
|
257
|
+
this.selectStuckCount = 200;
|
|
258
|
+
this.selectStuckTimeout = 6e4;
|
|
259
|
+
this.selectStuckMaxTimeout = 80;
|
|
260
|
+
this.selectCount = 200;
|
|
261
|
+
this.selectTimeout = 5e3;
|
|
262
|
+
this.buildBatchCount = 800;
|
|
263
|
+
this.buildBatchMaxCount = 1e4;
|
|
264
|
+
this.retryCount = 1;
|
|
265
|
+
this.executeSync = false;
|
|
266
|
+
this.idemLockTimeout = 18e4;
|
|
267
|
+
this.idemDoneTimeout = 6e4;
|
|
268
|
+
this.logStatus = false;
|
|
269
|
+
this.logStatusTimeout = 3e5;
|
|
270
|
+
this.approveCount = 2e3;
|
|
260
271
|
this.removeOnExecuted = false;
|
|
261
|
-
this.executeBatchAtOnce = false;
|
|
262
|
-
this.executeJobStatus = false;
|
|
263
|
-
this.executeJobStatusTtlMs = 3e5;
|
|
264
|
-
this.consumerHost = "host";
|
|
265
|
-
this.stream = "stream";
|
|
266
|
-
this.group = "group";
|
|
267
|
-
this.workerBatchTasksCount = 200;
|
|
268
|
-
this.recoveryStuckTasksTimeoutMs = 6e4;
|
|
269
|
-
this.workerLoopIntervalMs = 5e3;
|
|
270
|
-
this.workerSelectionTimeoutMs = 80;
|
|
271
|
-
this.workerMaxRetries = 1;
|
|
272
|
-
this.workerClearAttemptsTimeoutMs = 864e5;
|
|
273
|
-
}
|
|
274
|
-
async onSelected(data) {
|
|
275
|
-
return data;
|
|
276
|
-
}
|
|
277
|
-
async onExecute(id, payload, createdAt, job, key, attempt) {
|
|
278
|
-
}
|
|
279
|
-
async onReady(data) {
|
|
280
|
-
}
|
|
281
|
-
async onSuccess(id, payload, createdAt, job, key) {
|
|
282
272
|
}
|
|
283
|
-
|
|
273
|
+
signal() {
|
|
274
|
+
return this.abort.signal;
|
|
284
275
|
}
|
|
285
|
-
|
|
276
|
+
consumer() {
|
|
277
|
+
return this.host + ":" + process.pid;
|
|
286
278
|
}
|
|
287
|
-
async
|
|
279
|
+
async runQueue(queueName, from = "0-0") {
|
|
280
|
+
await this.createGroup(queueName, from);
|
|
281
|
+
await this.consumerLoop(queueName, from);
|
|
288
282
|
}
|
|
289
|
-
async
|
|
290
|
-
|
|
291
|
-
|
|
283
|
+
async createGroup(queueName, from = "0-0") {
|
|
284
|
+
try {
|
|
285
|
+
await this.redis.xgroup("CREATE", queueName, this.group, from, "MKSTREAM");
|
|
286
|
+
} catch (err) {
|
|
287
|
+
const msg = String(err?.message || "");
|
|
288
|
+
if (!msg.includes("BUSYGROUP")) {
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
292
|
}
|
|
293
|
-
async consumerLoop() {
|
|
293
|
+
async consumerLoop(queueName, from = "0-0") {
|
|
294
294
|
const signal = this.signal();
|
|
295
295
|
while (!signal?.aborted) {
|
|
296
|
+
let tasks = [];
|
|
296
297
|
try {
|
|
297
|
-
|
|
298
|
-
if (!isArrFilled(tasks)) {
|
|
299
|
-
await wait(600);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
const tasksP = await this.onSelected(tasks);
|
|
303
|
-
const ids = await this.execute(isArrFilled(tasksP) ? tasksP : tasks);
|
|
304
|
-
if (isArrFilled(ids)) {
|
|
305
|
-
await this.approve(ids);
|
|
306
|
-
}
|
|
298
|
+
tasks = await this.select(queueName, from);
|
|
307
299
|
} catch (err) {
|
|
308
|
-
|
|
309
|
-
|
|
300
|
+
}
|
|
301
|
+
if (!isArrFilled(tasks)) {
|
|
302
|
+
await wait(400);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
await this.approve(queueName, await this.execute(queueName, await this.beforeExecute(queueName, tasks)));
|
|
307
|
+
} catch (err) {
|
|
308
|
+
await this.batchError(err, queueName, tasks);
|
|
309
|
+
try {
|
|
310
|
+
await this.approve(queueName, tasks.map((task) => task[0]));
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
await wait(400);
|
|
310
314
|
}
|
|
311
315
|
}
|
|
312
316
|
}
|
|
313
|
-
async
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const job = opts.id ?? uuid();
|
|
321
|
-
const batches = this.buildBatches(data, job, opts.idem);
|
|
322
|
-
const result = new Array(data.length);
|
|
323
|
-
const promises = [];
|
|
324
|
-
let cursor = 0;
|
|
325
|
-
for (const batch of batches) {
|
|
326
|
-
const start = cursor;
|
|
327
|
-
const end = start + batch.length;
|
|
328
|
-
cursor = end;
|
|
329
|
-
promises.push(async () => {
|
|
330
|
-
const partIds = await this.xaddBatch(queueName, ...this.payloadBatch(batch, opts));
|
|
331
|
-
for (let k = 0; k < partIds.length; k++) {
|
|
332
|
-
result[start + k] = partIds[k];
|
|
317
|
+
async batchError(err, queueName, tasks) {
|
|
318
|
+
try {
|
|
319
|
+
const filtered = {};
|
|
320
|
+
tasks.forEach((task, index) => {
|
|
321
|
+
const key = String(task[5] + ":" + task[2] + ":" + task[3]);
|
|
322
|
+
if (!filtered[key]) {
|
|
323
|
+
filtered[key] = [];
|
|
333
324
|
}
|
|
325
|
+
filtered[key].push(tasks[index]);
|
|
334
326
|
});
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
327
|
+
for (let key in filtered) {
|
|
328
|
+
const filteredTasks = filtered[key];
|
|
329
|
+
const keySplit = key.split(":");
|
|
330
|
+
const attempt = Number(keySplit[0]);
|
|
331
|
+
await this.addTasks(queueName + (attempt + 1 >= this.retryCount ? ":dlq" : ""), filteredTasks.map((task) => ({ ...task[1] })), {
|
|
332
|
+
createdAt: Number(keySplit[1]),
|
|
333
|
+
job: keySplit[2],
|
|
334
|
+
attempt: attempt + 1
|
|
335
|
+
});
|
|
342
336
|
}
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
await this.redis.set(`${queueName}:${job}:total`, data.length);
|
|
346
|
-
await this.redis.pexpire(`${queueName}:${job}:total`, opts.statusTimeoutMs || 3e5);
|
|
337
|
+
} catch (err2) {
|
|
338
|
+
throw new Error(`Batch error. ${err2.message}`);
|
|
347
339
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const scripts = full ? [
|
|
353
|
-
["XAddBulk", XAddBulk],
|
|
354
|
-
["Approve", Approve],
|
|
355
|
-
["IdempotencyAllow", IdempotencyAllow],
|
|
356
|
-
["IdempotencyStart", IdempotencyStart],
|
|
357
|
-
["IdempotencyDone", IdempotencyDone],
|
|
358
|
-
["IdempotencyFree", IdempotencyFree],
|
|
359
|
-
["SelectStuck", SelectStuck]
|
|
360
|
-
] : [
|
|
361
|
-
["XAddBulk", XAddBulk]
|
|
362
|
-
];
|
|
363
|
-
for (const [name, code] of scripts) {
|
|
364
|
-
await this.loadScript(this.saveScript(name, code));
|
|
340
|
+
try {
|
|
341
|
+
await this.onBatchError(err, queueName, tasks);
|
|
342
|
+
} catch (err2) {
|
|
343
|
+
throw new Error(`Batch error. ${err2.message}`);
|
|
365
344
|
}
|
|
366
345
|
}
|
|
367
|
-
async
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return await this.redis.script("LOAD", code);
|
|
371
|
-
} catch (e) {
|
|
372
|
-
if (i === 2) {
|
|
373
|
-
throw e;
|
|
374
|
-
}
|
|
375
|
-
await new Promise((r) => setTimeout(r, 10 + Math.floor(Math.random() * 40)));
|
|
376
|
-
}
|
|
346
|
+
async approve(queueName, ids) {
|
|
347
|
+
if (!isArrFilled(ids)) {
|
|
348
|
+
return 0;
|
|
377
349
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
350
|
+
const approveCount = Math.max(500, Math.min(4e3, this.approveCount));
|
|
351
|
+
let total = 0, i = 0;
|
|
352
|
+
while (i < ids.length) {
|
|
353
|
+
const room = Math.min(approveCount, ids.length - i);
|
|
354
|
+
const part = ids.slice(i, i + room);
|
|
355
|
+
const approved = await this.runScript("Approve", [queueName], [this.group, this.removeOnExecuted ? "1" : "0", ...part], Approve);
|
|
356
|
+
total += Number(approved || 0);
|
|
357
|
+
i += room;
|
|
383
358
|
}
|
|
384
|
-
|
|
385
|
-
return codeBody;
|
|
359
|
+
return total;
|
|
386
360
|
}
|
|
387
|
-
async
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
this.saveScript(name, defaultCode);
|
|
393
|
-
}
|
|
394
|
-
if (!this.scripts[name].codeReady) {
|
|
395
|
-
this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
|
|
361
|
+
async select(queueName, from = "0-0") {
|
|
362
|
+
let selected = await this.selectS(queueName, from);
|
|
363
|
+
if (!isArrFilled(selected)) {
|
|
364
|
+
selected = await this.selectF(queueName, from);
|
|
396
365
|
}
|
|
366
|
+
return this.selectP(selected);
|
|
367
|
+
}
|
|
368
|
+
async selectS(queueName, from = "0-0") {
|
|
397
369
|
try {
|
|
398
|
-
|
|
370
|
+
const res = await this.runScript("SelectStuck", [queueName], [this.group, this.consumer(), String(this.selectStuckTimeout), String(this.selectStuckCount), String(this.selectStuckMaxTimeout)], SelectStuck);
|
|
371
|
+
return isArr(res) ? res : [];
|
|
399
372
|
} catch (err) {
|
|
400
|
-
if (String(err?.message || "").includes("
|
|
401
|
-
|
|
402
|
-
return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
|
|
373
|
+
if (String(err?.message || "").includes("NOGROUP")) {
|
|
374
|
+
await this.createGroup(queueName, from);
|
|
403
375
|
}
|
|
404
|
-
throw err;
|
|
405
376
|
}
|
|
377
|
+
return [];
|
|
406
378
|
}
|
|
407
|
-
async
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
String(minidWindowMs),
|
|
426
|
-
String(minidExact)
|
|
427
|
-
];
|
|
428
|
-
for (const item of data) {
|
|
429
|
-
const entry = item;
|
|
430
|
-
const id = entry.id ?? "*";
|
|
431
|
-
let flat;
|
|
432
|
-
if ("flat" in entry && isArrFilled(entry.flat)) {
|
|
433
|
-
flat = entry.flat;
|
|
434
|
-
if (flat.length % 2 !== 0) {
|
|
435
|
-
throw new Error('Property "flat" must contain an even number of realKeysLength (field/value pairs).');
|
|
436
|
-
}
|
|
437
|
-
} else if ("payload" in entry && isObjFilled(entry.payload)) {
|
|
438
|
-
flat = [];
|
|
439
|
-
for (const [k, v] of Object.entries(entry.payload)) {
|
|
440
|
-
flat.push(k, v);
|
|
441
|
-
}
|
|
442
|
-
} else {
|
|
443
|
-
throw new Error('Task must have "payload" or "flat".');
|
|
444
|
-
}
|
|
445
|
-
const pairs = flat.length / 2;
|
|
446
|
-
if (isNumNZ(pairs)) {
|
|
447
|
-
throw new Error('Task "flat" must contain at least one field/value pair.');
|
|
379
|
+
async selectF(queueName, from = "0-0") {
|
|
380
|
+
let rows = [];
|
|
381
|
+
try {
|
|
382
|
+
const res = await this.redis.xreadgroup(
|
|
383
|
+
"GROUP",
|
|
384
|
+
this.group,
|
|
385
|
+
this.consumer(),
|
|
386
|
+
"BLOCK",
|
|
387
|
+
Math.max(2, this.selectTimeout | 0),
|
|
388
|
+
"COUNT",
|
|
389
|
+
this.selectCount,
|
|
390
|
+
"STREAMS",
|
|
391
|
+
queueName,
|
|
392
|
+
">"
|
|
393
|
+
);
|
|
394
|
+
rows = res?.[0]?.[1] ?? [];
|
|
395
|
+
if (!isArrFilled(rows)) {
|
|
396
|
+
return [];
|
|
448
397
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
argv.push(!token ? "" : isStrFilled(token) ? token : String(token));
|
|
398
|
+
} catch (err) {
|
|
399
|
+
if (String(err?.message || "").includes("NOGROUP")) {
|
|
400
|
+
await this.createGroup(queueName, from);
|
|
453
401
|
}
|
|
454
402
|
}
|
|
455
|
-
return
|
|
403
|
+
return rows;
|
|
456
404
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
for (let task of tasks) {
|
|
461
|
-
const createdAt = task?.createdAt || Date.now();
|
|
462
|
-
let entry = task;
|
|
463
|
-
if (isObj(entry.payload)) {
|
|
464
|
-
entry = {
|
|
465
|
-
...entry,
|
|
466
|
-
payload: {
|
|
467
|
-
payload: JSON.stringify(entry.payload),
|
|
468
|
-
createdAt,
|
|
469
|
-
job
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
if (idem) {
|
|
473
|
-
entry.payload["idemKey"] = entry?.idemKey || uuid();
|
|
474
|
-
}
|
|
475
|
-
} else if (Array.isArray(entry.flat)) {
|
|
476
|
-
entry.flat.push("createdAt");
|
|
477
|
-
entry.flat.push(String(createdAt));
|
|
478
|
-
entry.flat.push("job");
|
|
479
|
-
entry.flat.push(job);
|
|
480
|
-
if (idem) {
|
|
481
|
-
entry.flat.push("idemKey");
|
|
482
|
-
entry.flat.push(entry?.idemKey || uuid());
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const reqKeysLength = this.keysLength(entry);
|
|
486
|
-
if (batch.length && (batch.length >= this.addingBatchTasksCount || realKeysLength + reqKeysLength > this.addingBatchKeysLimit)) {
|
|
487
|
-
batches.push(batch);
|
|
488
|
-
batch = [];
|
|
489
|
-
realKeysLength = 0;
|
|
490
|
-
}
|
|
491
|
-
batch.push(entry);
|
|
492
|
-
realKeysLength += reqKeysLength;
|
|
493
|
-
}
|
|
494
|
-
if (batch.length) {
|
|
495
|
-
batches.push(batch);
|
|
405
|
+
selectP(raw) {
|
|
406
|
+
if (!Array.isArray(raw)) {
|
|
407
|
+
return [];
|
|
496
408
|
}
|
|
497
|
-
return
|
|
409
|
+
return Array.from(raw || []).map((e) => {
|
|
410
|
+
const id = Buffer.isBuffer(e?.[0]) ? e[0].toString() : e?.[0];
|
|
411
|
+
const kvRaw = e?.[1] ?? [];
|
|
412
|
+
const kv = isArr(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
|
|
413
|
+
return [id, kv];
|
|
414
|
+
}).filter(([id, kv]) => isStrFilled(id) && isArr(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
|
|
415
|
+
const { payload, createdAt, job, idemKey = "", attempt } = this.values(kv);
|
|
416
|
+
return [id, this.payload(payload), createdAt, job, idemKey, Number(attempt)];
|
|
417
|
+
});
|
|
498
418
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if ("payload" in task && isObj(task.payload)) {
|
|
504
|
-
return 2 + Object.keys(task.payload).length * 2;
|
|
419
|
+
values(value) {
|
|
420
|
+
const result = {};
|
|
421
|
+
for (let i = 0; i < value.length; i += 2) {
|
|
422
|
+
result[value[i]] = value[i + 1];
|
|
505
423
|
}
|
|
506
|
-
return
|
|
507
|
-
}
|
|
508
|
-
attemptsKey(id) {
|
|
509
|
-
const safeStream = this.stream.replace(/[^\w:\-]/g, "_");
|
|
510
|
-
const safeId = id.replace(/[^\w:\-]/g, "_");
|
|
511
|
-
return `q:${safeStream}:attempts:${safeId}`;
|
|
424
|
+
return result;
|
|
512
425
|
}
|
|
513
|
-
|
|
426
|
+
payload(data) {
|
|
514
427
|
try {
|
|
515
|
-
|
|
516
|
-
const attempts = await this.redis.incr(key);
|
|
517
|
-
await this.redis.pexpire(key, this.workerClearAttemptsTimeoutMs);
|
|
518
|
-
return attempts;
|
|
428
|
+
return JSON.parse(data);
|
|
519
429
|
} catch (err) {
|
|
520
430
|
}
|
|
521
|
-
return
|
|
522
|
-
}
|
|
523
|
-
async getAttempts(id) {
|
|
524
|
-
const key = this.attemptsKey(id);
|
|
525
|
-
const v = await this.redis.get(key);
|
|
526
|
-
return Number(v || 0);
|
|
527
|
-
}
|
|
528
|
-
async clearAttempts(id) {
|
|
529
|
-
const key = this.attemptsKey(id);
|
|
530
|
-
try {
|
|
531
|
-
await this.redis.del(key);
|
|
532
|
-
} catch (e) {
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
async success(id, payload, createdAt, job, key) {
|
|
536
|
-
if (this.executeJobStatus) {
|
|
537
|
-
const prefix = `${this.stream}:${job}:`;
|
|
538
|
-
await this.incr(`${prefix}ok`, this.executeJobStatusTtlMs);
|
|
539
|
-
await this.incr(`${prefix}ready`, this.executeJobStatusTtlMs);
|
|
540
|
-
}
|
|
541
|
-
await this.onSuccess(id, payload, createdAt, job, key);
|
|
542
|
-
}
|
|
543
|
-
async batchError(err, tasks) {
|
|
544
|
-
await this.onBatchError(err, tasks);
|
|
545
|
-
}
|
|
546
|
-
async error(err, id, payload, createdAt, job, key, attempt) {
|
|
547
|
-
if (this.executeJobStatus && attempt >= this.workerMaxRetries) {
|
|
548
|
-
const prefix = `${this.stream}:${job}:`;
|
|
549
|
-
await this.incr(`${prefix}err`, this.executeJobStatusTtlMs);
|
|
550
|
-
await this.incr(`${prefix}ready`, this.executeJobStatusTtlMs);
|
|
551
|
-
}
|
|
552
|
-
await this.onError(err, id, payload, createdAt, job, key);
|
|
553
|
-
}
|
|
554
|
-
async attempt(err, id, payload, createdAt, job, key, attempt) {
|
|
555
|
-
await this.onRetry(err, id, payload, createdAt, job, key, attempt);
|
|
431
|
+
return data;
|
|
556
432
|
}
|
|
557
|
-
async execute(tasks) {
|
|
433
|
+
async execute(queueName, tasks) {
|
|
558
434
|
const result = [];
|
|
559
435
|
let contended = 0, promises = [];
|
|
560
|
-
for (const [id, payload, createdAt, job, idemKey] of tasks) {
|
|
561
|
-
if (this.
|
|
436
|
+
for (const [id, payload, createdAt, job, idemKey, attempt] of tasks) {
|
|
437
|
+
if (!this.executeSync) {
|
|
562
438
|
promises.push((async () => {
|
|
563
|
-
const r = await this.executeProcess(id, payload, createdAt, job, idemKey);
|
|
439
|
+
const r = await this.executeProcess(queueName, { id, payload, createdAt, job, idemKey, attempt });
|
|
564
440
|
if (r.id) {
|
|
565
441
|
result.push(id);
|
|
566
442
|
} else if (r.contended) {
|
|
@@ -568,7 +444,7 @@ var PowerQueues = class extends PowerRedis {
|
|
|
568
444
|
}
|
|
569
445
|
})());
|
|
570
446
|
} else {
|
|
571
|
-
const r = await this.executeProcess(id, payload, createdAt, job, idemKey);
|
|
447
|
+
const r = await this.executeProcess(queueName, { id, payload, createdAt, job, idemKey, attempt });
|
|
572
448
|
if (r.id) {
|
|
573
449
|
result.push(id);
|
|
574
450
|
} else if (r.contended) {
|
|
@@ -576,69 +452,20 @@ var PowerQueues = class extends PowerRedis {
|
|
|
576
452
|
}
|
|
577
453
|
}
|
|
578
454
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
await Promise.all(promises);
|
|
582
|
-
}
|
|
583
|
-
await this.onReady(tasks);
|
|
584
|
-
if (!isArrFilled(result) && contended > tasks.length >> 1) {
|
|
585
|
-
await this.waitAbortable(15 + Math.floor(Math.random() * 35) + Math.min(250, 15 * contended + Math.floor(Math.random() * 40)));
|
|
586
|
-
}
|
|
587
|
-
} catch (err) {
|
|
588
|
-
await this.batchError(err, tasks);
|
|
589
|
-
}
|
|
590
|
-
return result;
|
|
591
|
-
}
|
|
592
|
-
async executeProcess(id, payload, createdAt, job, key) {
|
|
593
|
-
if (key) {
|
|
594
|
-
return await this.idempotency(id, payload, createdAt, job, key);
|
|
595
|
-
} else {
|
|
596
|
-
try {
|
|
597
|
-
await this.onExecute(id, payload, createdAt, job, key, await this.getAttempts(id));
|
|
598
|
-
await this.success(id, payload, createdAt, job, key);
|
|
599
|
-
return { id };
|
|
600
|
-
} catch (err) {
|
|
601
|
-
const attempt = await this.incrAttempts(id);
|
|
602
|
-
await this.attempt(err, id, payload, createdAt, job, key, attempt);
|
|
603
|
-
await this.error(err, id, payload, createdAt, job, key, attempt);
|
|
604
|
-
if (attempt >= this.workerMaxRetries) {
|
|
605
|
-
await this.addTasks(`${this.stream}:dlq`, [{
|
|
606
|
-
payload: {
|
|
607
|
-
...payload,
|
|
608
|
-
error: String(err?.message || err),
|
|
609
|
-
createdAt,
|
|
610
|
-
job,
|
|
611
|
-
id,
|
|
612
|
-
attempt
|
|
613
|
-
}
|
|
614
|
-
}]);
|
|
615
|
-
await this.clearAttempts(id);
|
|
616
|
-
return { id };
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return {};
|
|
621
|
-
}
|
|
622
|
-
async approve(ids) {
|
|
623
|
-
if (!Array.isArray(ids) || !(ids.length > 0)) {
|
|
624
|
-
return 0;
|
|
455
|
+
if (!this.executeSync && promises.length > 0) {
|
|
456
|
+
await Promise.all(promises);
|
|
625
457
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const room = Math.min(approveBatchTasksCount, ids.length - i);
|
|
630
|
-
const part = ids.slice(i, i + room);
|
|
631
|
-
const approved = await this.runScript("Approve", [this.stream], [this.group, this.removeOnExecuted ? "1" : "0", ...part], Approve);
|
|
632
|
-
total += Number(approved || 0);
|
|
633
|
-
i += room;
|
|
458
|
+
await this.onBatchSuccess(queueName, tasks);
|
|
459
|
+
if (!isArrFilled(result) && contended > tasks.length >> 1) {
|
|
460
|
+
await this.waitAbortable(15 + Math.floor(Math.random() * 35) + Math.min(250, 15 * contended + Math.floor(Math.random() * 40)));
|
|
634
461
|
}
|
|
635
|
-
return
|
|
462
|
+
return result;
|
|
636
463
|
}
|
|
637
|
-
async
|
|
638
|
-
const keys = this.idempotencyKeys(
|
|
464
|
+
async executeProcess(queueName, task) {
|
|
465
|
+
const keys = this.idempotencyKeys(queueName, String(task.idemKey));
|
|
639
466
|
const allow = await this.idempotencyAllow(keys);
|
|
640
467
|
if (allow === 1) {
|
|
641
|
-
return { id };
|
|
468
|
+
return { id: task.id };
|
|
642
469
|
} else if (allow === 0) {
|
|
643
470
|
let ttl = -2;
|
|
644
471
|
try {
|
|
@@ -654,29 +481,17 @@ var PowerQueues = class extends PowerRedis {
|
|
|
654
481
|
const heartbeat = this.heartbeat(keys) || (() => {
|
|
655
482
|
});
|
|
656
483
|
try {
|
|
657
|
-
await this.onExecute(
|
|
484
|
+
await this.onExecute(queueName, task);
|
|
658
485
|
await this.idempotencyDone(keys);
|
|
659
|
-
await this.success(
|
|
660
|
-
return { id };
|
|
486
|
+
await this.success(queueName, task);
|
|
487
|
+
return { id: task.id };
|
|
661
488
|
} catch (err) {
|
|
662
|
-
const attempt = await this.incrAttempts(id);
|
|
663
489
|
try {
|
|
664
|
-
|
|
665
|
-
await this.error(err,
|
|
666
|
-
if (attempt >= this.
|
|
667
|
-
await this.addTasks(`${this.stream}:dlq`, [{
|
|
668
|
-
payload: {
|
|
669
|
-
...payload,
|
|
670
|
-
error: String(err?.message || err),
|
|
671
|
-
createdAt,
|
|
672
|
-
job,
|
|
673
|
-
id,
|
|
674
|
-
attempt: 0
|
|
675
|
-
}
|
|
676
|
-
}]);
|
|
677
|
-
await this.clearAttempts(id);
|
|
490
|
+
task.attempt = task.attempt + 1;
|
|
491
|
+
await this.error(err, queueName, task);
|
|
492
|
+
if (task.attempt >= this.retryCount) {
|
|
678
493
|
await this.idempotencyFree(keys);
|
|
679
|
-
return { id };
|
|
494
|
+
return { id: task.id };
|
|
680
495
|
}
|
|
681
496
|
await this.idempotencyFree(keys);
|
|
682
497
|
} catch (err2) {
|
|
@@ -685,8 +500,8 @@ var PowerQueues = class extends PowerRedis {
|
|
|
685
500
|
heartbeat();
|
|
686
501
|
}
|
|
687
502
|
}
|
|
688
|
-
idempotencyKeys(key) {
|
|
689
|
-
const prefix = `q:${
|
|
503
|
+
idempotencyKeys(queueName, key) {
|
|
504
|
+
const prefix = `q:${queueName.replace(/[^\w:\-]/g, "_")}:`;
|
|
690
505
|
const keyP = key.replace(/[^\w:\-]/g, "_");
|
|
691
506
|
const doneKey = `${prefix}done:${keyP}`;
|
|
692
507
|
const lockKey = `${prefix}lock:${keyP}`;
|
|
@@ -701,72 +516,50 @@ var PowerQueues = class extends PowerRedis {
|
|
|
701
516
|
};
|
|
702
517
|
}
|
|
703
518
|
async idempotencyAllow(keys) {
|
|
704
|
-
const res = await this.runScript("IdempotencyAllow", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.
|
|
519
|
+
const res = await this.runScript("IdempotencyAllow", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.idemLockTimeout), keys.token], IdempotencyAllow);
|
|
705
520
|
return Number(res || 0);
|
|
706
521
|
}
|
|
707
522
|
async idempotencyStart(keys) {
|
|
708
|
-
const res = await this.runScript("IdempotencyStart", [keys.lockKey, keys.startKey], [keys.token, String(this.
|
|
523
|
+
const res = await this.runScript("IdempotencyStart", [keys.lockKey, keys.startKey], [keys.token, String(this.idemLockTimeout)], IdempotencyStart);
|
|
709
524
|
return Number(res || 0) === 1;
|
|
710
525
|
}
|
|
711
526
|
async idempotencyDone(keys) {
|
|
712
|
-
await this.runScript("IdempotencyDone", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.
|
|
527
|
+
await this.runScript("IdempotencyDone", [keys.doneKey, keys.lockKey, keys.startKey], [String(this.idemDoneTimeout), keys.token], IdempotencyDone);
|
|
713
528
|
}
|
|
714
529
|
async idempotencyFree(keys) {
|
|
715
530
|
await this.runScript("IdempotencyFree", [keys.lockKey, keys.startKey], [keys.token], IdempotencyFree);
|
|
716
531
|
}
|
|
717
|
-
async
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
} catch (err) {
|
|
739
|
-
if (String(err?.message || "").includes("NOGROUP")) {
|
|
740
|
-
await this.createGroup();
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
return [];
|
|
744
|
-
}
|
|
745
|
-
async selectFresh() {
|
|
746
|
-
let entries = [];
|
|
747
|
-
try {
|
|
748
|
-
const res = await this.redis.xreadgroup(
|
|
749
|
-
"GROUP",
|
|
750
|
-
this.group,
|
|
751
|
-
this.consumer(),
|
|
752
|
-
"BLOCK",
|
|
753
|
-
Math.max(2, this.workerLoopIntervalMs | 0),
|
|
754
|
-
"COUNT",
|
|
755
|
-
this.workerBatchTasksCount,
|
|
756
|
-
"STREAMS",
|
|
757
|
-
this.stream,
|
|
758
|
-
">"
|
|
759
|
-
);
|
|
760
|
-
entries = res?.[0]?.[1] ?? [];
|
|
761
|
-
if (!isArrFilled(entries)) {
|
|
762
|
-
return [];
|
|
763
|
-
}
|
|
764
|
-
} catch (err) {
|
|
765
|
-
if (String(err?.message || "").includes("NOGROUP")) {
|
|
766
|
-
await this.createGroup();
|
|
532
|
+
async success(queueName, task) {
|
|
533
|
+
if (this.logStatus) {
|
|
534
|
+
const statusKey = `${queueName}:${task.job}:`;
|
|
535
|
+
await this.incr(statusKey + "ok", this.logStatusTimeout);
|
|
536
|
+
await this.incr(statusKey + "ready", this.logStatusTimeout);
|
|
537
|
+
}
|
|
538
|
+
await this.onSuccess(queueName, task);
|
|
539
|
+
}
|
|
540
|
+
async error(err, queueName, task) {
|
|
541
|
+
const dlqKey = queueName + ":dlq";
|
|
542
|
+
const taskP = { ...task };
|
|
543
|
+
if (task.attempt >= this.retryCount) {
|
|
544
|
+
const statusKey = `${queueName}:${task.job}:`;
|
|
545
|
+
await this.addTasks(dlqKey, [{ ...taskP.payload }], {
|
|
546
|
+
createdAt: task.createdAt,
|
|
547
|
+
job: task.job,
|
|
548
|
+
attempt: task.attempt
|
|
549
|
+
});
|
|
550
|
+
if (this.logStatus) {
|
|
551
|
+
await this.incr(statusKey + "err", this.logStatusTimeout);
|
|
552
|
+
await this.incr(statusKey + "ready", this.logStatusTimeout);
|
|
767
553
|
}
|
|
554
|
+
} else {
|
|
555
|
+
await this.onRetry(err, queueName, task);
|
|
556
|
+
await this.addTasks(queueName, [{ ...taskP.payload }], {
|
|
557
|
+
createdAt: task.createdAt,
|
|
558
|
+
job: task.job,
|
|
559
|
+
attempt: task.attempt
|
|
560
|
+
});
|
|
768
561
|
}
|
|
769
|
-
|
|
562
|
+
await this.onError(err, queueName, task);
|
|
770
563
|
}
|
|
771
564
|
async waitAbortable(ttl) {
|
|
772
565
|
return new Promise((resolve) => {
|
|
@@ -798,8 +591,8 @@ var PowerQueues = class extends PowerRedis {
|
|
|
798
591
|
}
|
|
799
592
|
async sendHeartbeat(keys) {
|
|
800
593
|
try {
|
|
801
|
-
const r1 = await this.redis.pexpire(keys.lockKey, this.
|
|
802
|
-
const r2 = await this.redis.pexpire(keys.startKey, this.
|
|
594
|
+
const r1 = await this.redis.pexpire(keys.lockKey, this.idemLockTimeout);
|
|
595
|
+
const r2 = await this.redis.pexpire(keys.startKey, this.idemLockTimeout);
|
|
803
596
|
const ok1 = Number(r1 || 0) === 1;
|
|
804
597
|
const ok2 = Number(r2 || 0) === 1;
|
|
805
598
|
return ok1 || ok2;
|
|
@@ -808,10 +601,10 @@ var PowerQueues = class extends PowerRedis {
|
|
|
808
601
|
}
|
|
809
602
|
}
|
|
810
603
|
heartbeat(keys) {
|
|
811
|
-
if (this.
|
|
604
|
+
if (this.idemLockTimeout <= 0) {
|
|
812
605
|
return;
|
|
813
606
|
}
|
|
814
|
-
const workerHeartbeatTimeoutMs = Math.max(1e3, Math.floor(Math.max(5e3, this.
|
|
607
|
+
const workerHeartbeatTimeoutMs = Math.max(1e3, Math.floor(Math.max(5e3, this.idemLockTimeout | 0) / 4));
|
|
815
608
|
let timer;
|
|
816
609
|
let alive = true;
|
|
817
610
|
let hbFails = 0;
|
|
@@ -851,39 +644,191 @@ var PowerQueues = class extends PowerRedis {
|
|
|
851
644
|
stop();
|
|
852
645
|
};
|
|
853
646
|
}
|
|
854
|
-
|
|
855
|
-
if (!
|
|
856
|
-
|
|
647
|
+
async runScript(name, keys, args, defaultCode) {
|
|
648
|
+
if (!this.scripts[name]) {
|
|
649
|
+
if (!isStrFilled(defaultCode)) {
|
|
650
|
+
throw new Error(`Undefined script "${name}". Save it before executing.`);
|
|
651
|
+
}
|
|
652
|
+
this.saveScript(name, defaultCode);
|
|
653
|
+
}
|
|
654
|
+
if (!this.scripts[name].codeReady) {
|
|
655
|
+
this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
if (String(err?.message || "").includes("NOSCRIPT")) {
|
|
661
|
+
this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
|
|
662
|
+
return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
|
|
663
|
+
}
|
|
664
|
+
throw err;
|
|
857
665
|
}
|
|
858
|
-
return Array.from(raw || []).map((e) => {
|
|
859
|
-
const id = Buffer.isBuffer(e?.[0]) ? e[0].toString() : e?.[0];
|
|
860
|
-
const kvRaw = e?.[1] ?? [];
|
|
861
|
-
const kv = isArr(kvRaw) ? kvRaw.map((x) => Buffer.isBuffer(x) ? x.toString() : x) : [];
|
|
862
|
-
return [id, kv];
|
|
863
|
-
}).filter(([id, kv]) => isStrFilled(id) && isArr(kv) && (kv.length & 1) === 0).map(([id, kv]) => {
|
|
864
|
-
const { idemKey = "", job, createdAt, payload } = this.values(kv);
|
|
865
|
-
return [id, this.payload(payload), createdAt, job, idemKey];
|
|
866
|
-
});
|
|
867
666
|
}
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
667
|
+
async loadScripts(full = false) {
|
|
668
|
+
const scripts = full ? [
|
|
669
|
+
["XAddBulk", XAddBulk],
|
|
670
|
+
["Approve", Approve],
|
|
671
|
+
["IdempotencyAllow", IdempotencyAllow],
|
|
672
|
+
["IdempotencyStart", IdempotencyStart],
|
|
673
|
+
["IdempotencyDone", IdempotencyDone],
|
|
674
|
+
["IdempotencyFree", IdempotencyFree],
|
|
675
|
+
["SelectStuck", SelectStuck]
|
|
676
|
+
] : [
|
|
677
|
+
["XAddBulk", XAddBulk]
|
|
678
|
+
];
|
|
679
|
+
for (const [name, code] of scripts) {
|
|
680
|
+
await this.loadScript(this.saveScript(name, code));
|
|
872
681
|
}
|
|
682
|
+
}
|
|
683
|
+
async loadScript(code) {
|
|
684
|
+
for (let i = 0; i < 3; i++) {
|
|
685
|
+
try {
|
|
686
|
+
return await this.redis.script("LOAD", code);
|
|
687
|
+
} catch (e) {
|
|
688
|
+
if (i === 2) {
|
|
689
|
+
throw e;
|
|
690
|
+
}
|
|
691
|
+
await new Promise((r) => setTimeout(r, 10 + Math.floor(Math.random() * 40)));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
throw new Error("Load lua script failed.");
|
|
695
|
+
}
|
|
696
|
+
saveScript(name, codeBody) {
|
|
697
|
+
if (!isStrFilled(codeBody)) {
|
|
698
|
+
throw new Error("Script body is empty.");
|
|
699
|
+
}
|
|
700
|
+
this.scripts[name] = { codeBody };
|
|
701
|
+
return codeBody;
|
|
702
|
+
}
|
|
703
|
+
async addTasks(queueName, data, opts = {}) {
|
|
704
|
+
if (!isArrFilled(data)) {
|
|
705
|
+
throw new Error("Tasks is not filled.");
|
|
706
|
+
}
|
|
707
|
+
if (!isStrFilled(queueName)) {
|
|
708
|
+
throw new Error("Queue name is required.");
|
|
709
|
+
}
|
|
710
|
+
opts.job = opts.job ?? uuid();
|
|
711
|
+
const batches = this.buildBatches(data, opts);
|
|
712
|
+
const result = new Array(data.length);
|
|
713
|
+
const promises = [];
|
|
714
|
+
let cursor = 0;
|
|
715
|
+
for (const batch of batches) {
|
|
716
|
+
const start = cursor;
|
|
717
|
+
const end = start + batch.length;
|
|
718
|
+
cursor = end;
|
|
719
|
+
promises.push(async () => {
|
|
720
|
+
const payload = this.payloadBatch(batch, opts);
|
|
721
|
+
const partIds = await this.runScript("XAddBulk", [queueName], payload, XAddBulk);
|
|
722
|
+
for (let k = 0; k < partIds.length; k++) {
|
|
723
|
+
result[start + k] = partIds[k];
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
const runners = Array.from({ length: promises.length }, async () => {
|
|
728
|
+
while (promises.length) {
|
|
729
|
+
const promise = promises.shift();
|
|
730
|
+
if (promise) {
|
|
731
|
+
await promise();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
if (opts.status) {
|
|
736
|
+
await this.redis.set(`${queueName}:${opts.job}:total`, data.length);
|
|
737
|
+
await this.redis.pexpire(`${queueName}:${opts.job}:total`, this.logStatusTimeout);
|
|
738
|
+
}
|
|
739
|
+
await Promise.all(runners);
|
|
873
740
|
return result;
|
|
874
741
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
742
|
+
buildBatches(tasks, opts = {}) {
|
|
743
|
+
const batches = [];
|
|
744
|
+
let batch = [], realKeysLength = 0;
|
|
745
|
+
for (let task of tasks) {
|
|
746
|
+
const createdAt = opts?.createdAt || Date.now();
|
|
747
|
+
let entry = {};
|
|
748
|
+
if (isObj(entry)) {
|
|
749
|
+
entry = {
|
|
750
|
+
payload: JSON.stringify(task),
|
|
751
|
+
attempt: Number(opts.attempt || 0),
|
|
752
|
+
job: opts.job ?? uuid(),
|
|
753
|
+
idemKey: uuid(),
|
|
754
|
+
createdAt
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const reqKeysLength = this.keysLength(entry);
|
|
758
|
+
if (batch.length && (batch.length >= this.buildBatchCount || realKeysLength + reqKeysLength > this.buildBatchMaxCount)) {
|
|
759
|
+
batches.push(batch);
|
|
760
|
+
batch = [];
|
|
761
|
+
realKeysLength = 0;
|
|
762
|
+
}
|
|
763
|
+
batch.push(entry);
|
|
764
|
+
realKeysLength += reqKeysLength;
|
|
879
765
|
}
|
|
880
|
-
|
|
766
|
+
if (batch.length) {
|
|
767
|
+
batches.push(batch);
|
|
768
|
+
}
|
|
769
|
+
return batches;
|
|
881
770
|
}
|
|
882
|
-
|
|
883
|
-
|
|
771
|
+
keysLength(task) {
|
|
772
|
+
if ("payload" in task && isObj(task.payload)) {
|
|
773
|
+
return 2 + Object.keys(task.payload).length * 2;
|
|
774
|
+
}
|
|
775
|
+
return 2 + Object.keys(task).length * 2;
|
|
884
776
|
}
|
|
885
|
-
|
|
886
|
-
|
|
777
|
+
payloadBatch(data, opts) {
|
|
778
|
+
const maxlen = Math.max(0, Math.floor(opts?.maxlen ?? 0));
|
|
779
|
+
const approx = opts?.exact ? 0 : opts?.approx !== false ? 1 : 0;
|
|
780
|
+
const exact = opts?.exact ? 1 : 0;
|
|
781
|
+
const nomkstream = opts?.nomkstream ? 1 : 0;
|
|
782
|
+
const trimLimit = Math.max(0, Math.floor(opts?.trimLimit ?? 0));
|
|
783
|
+
const minidWindowMs = Math.max(0, Math.floor(opts?.minidWindowMs ?? 0));
|
|
784
|
+
const minidExact = opts?.minidExact ? 1 : 0;
|
|
785
|
+
const argv = [
|
|
786
|
+
String(maxlen),
|
|
787
|
+
String(approx),
|
|
788
|
+
String(data.length),
|
|
789
|
+
String(exact),
|
|
790
|
+
String(nomkstream),
|
|
791
|
+
String(trimLimit),
|
|
792
|
+
String(minidWindowMs),
|
|
793
|
+
String(minidExact)
|
|
794
|
+
];
|
|
795
|
+
for (const item of data) {
|
|
796
|
+
const entry = item;
|
|
797
|
+
const id = entry.id ?? "*";
|
|
798
|
+
let flat = [];
|
|
799
|
+
if ("payload" in entry && isObjFilled(entry)) {
|
|
800
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
801
|
+
flat.push(k, v);
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
throw new Error('Task must have "payload" or "flat".');
|
|
805
|
+
}
|
|
806
|
+
const pairs = flat.length / 2;
|
|
807
|
+
if (isNumNZ(pairs)) {
|
|
808
|
+
throw new Error('Task "flat" must contain at least one field/value pair.');
|
|
809
|
+
}
|
|
810
|
+
argv.push(String(id));
|
|
811
|
+
argv.push(String(pairs));
|
|
812
|
+
for (const token of flat) {
|
|
813
|
+
argv.push(!isExists(token) ? "" : isStrFilled(token) ? token : String(token));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return argv;
|
|
817
|
+
}
|
|
818
|
+
async beforeExecute(queueName, tasks) {
|
|
819
|
+
return tasks;
|
|
820
|
+
}
|
|
821
|
+
async onExecute(queueName, task) {
|
|
822
|
+
}
|
|
823
|
+
async onBatchSuccess(queueName, tasks) {
|
|
824
|
+
}
|
|
825
|
+
async onSuccess(queueName, task) {
|
|
826
|
+
}
|
|
827
|
+
async onError(err, queueName, task) {
|
|
828
|
+
}
|
|
829
|
+
async onBatchError(err, queueName, tasks) {
|
|
830
|
+
}
|
|
831
|
+
async onRetry(err, queueName, task) {
|
|
887
832
|
}
|
|
888
833
|
};
|
|
889
834
|
export {
|