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.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.addingBatchTasksCount = 800;
256
- this.addingBatchKeysLimit = 1e4;
257
- this.workerExecuteLockTimeoutMs = 18e4;
258
- this.workerCacheTaskTimeoutMs = 6e4;
259
- this.approveBatchTasksCount = 2e3;
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
- async onBatchError(err, tasks) {
273
+ signal() {
274
+ return this.abort.signal;
284
275
  }
285
- async onError(err, id, payload, createdAt, job, key) {
276
+ consumer() {
277
+ return this.host + ":" + process.pid;
286
278
  }
287
- async onRetry(err, id, payload, createdAt, job, key, attempts) {
279
+ async runQueue(queueName, from = "0-0") {
280
+ await this.createGroup(queueName, from);
281
+ await this.consumerLoop(queueName, from);
288
282
  }
289
- async runQueue() {
290
- await this.createGroup("0-0");
291
- await this.consumerLoop();
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
- const tasks = await this.select();
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
- await this.batchError(err);
309
- await wait(600);
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 addTasks(queueName, data, opts = {}) {
314
- if (!isArrFilled(data)) {
315
- throw new Error("Tasks is not filled.");
316
- }
317
- if (!isStrFilled(queueName)) {
318
- throw new Error("Queue name is required.");
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
- const runners = Array.from({ length: promises.length }, async () => {
337
- while (promises.length) {
338
- const promise = promises.shift();
339
- if (promise) {
340
- await promise();
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
- if (opts.status) {
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
- await Promise.all(runners);
349
- return result;
350
- }
351
- async loadScripts(full = false) {
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 loadScript(code) {
368
- for (let i = 0; i < 3; i++) {
369
- try {
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
- throw new Error("Load lua script failed.");
379
- }
380
- saveScript(name, codeBody) {
381
- if (!isStrFilled(codeBody)) {
382
- throw new Error("Script body is empty.");
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
- this.scripts[name] = { codeBody };
385
- return codeBody;
359
+ return total;
386
360
  }
387
- async runScript(name, keys, args, defaultCode) {
388
- if (!this.scripts[name]) {
389
- if (!isStrFilled(defaultCode)) {
390
- throw new Error(`Undefined script "${name}". Save it before executing.`);
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
- return await this.redis.evalsha(this.scripts[name].codeReady, keys.length, ...keys, ...args);
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("NOSCRIPT")) {
401
- this.scripts[name].codeReady = await this.loadScript(this.scripts[name].codeBody);
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 xaddBatch(queueName, ...batches) {
408
- return await this.runScript("XAddBulk", [queueName], batches, XAddBulk);
409
- }
410
- payloadBatch(data, opts) {
411
- const maxlen = Math.max(0, Math.floor(opts?.maxlen ?? 0));
412
- const approx = opts?.exact ? 0 : opts?.approx !== false ? 1 : 0;
413
- const exact = opts?.exact ? 1 : 0;
414
- const nomkstream = opts?.nomkstream ? 1 : 0;
415
- const trimLimit = Math.max(0, Math.floor(opts?.trimLimit ?? 0));
416
- const minidWindowMs = Math.max(0, Math.floor(opts?.minidWindowMs ?? 0));
417
- const minidExact = opts?.minidExact ? 1 : 0;
418
- const argv = [
419
- String(maxlen),
420
- String(approx),
421
- String(data.length),
422
- String(exact),
423
- String(nomkstream),
424
- String(trimLimit),
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
- argv.push(String(id));
450
- argv.push(String(pairs));
451
- for (const token of flat) {
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 argv;
403
+ return rows;
456
404
  }
457
- buildBatches(tasks, job, idem) {
458
- const batches = [];
459
- let batch = [], realKeysLength = 0;
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 batches;
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
- keysLength(task) {
500
- if ("flat" in task && Array.isArray(task.flat) && task.flat.length) {
501
- return 2 + task.flat.length;
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 2 + Object.keys(task).length * 2;
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
- async incrAttempts(id) {
426
+ payload(data) {
514
427
  try {
515
- const key = this.attemptsKey(id);
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 0;
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.executeBatchAtOnce) {
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
- try {
580
- if (this.executeBatchAtOnce && promises.length > 0) {
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
- const approveBatchTasksCount = Math.max(500, Math.min(4e3, this.approveBatchTasksCount));
627
- let total = 0, i = 0;
628
- while (i < ids.length) {
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 total;
462
+ return result;
636
463
  }
637
- async idempotency(id, payload, createdAt, job, key) {
638
- const keys = this.idempotencyKeys(key);
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(id, payload, createdAt, job, key, await this.getAttempts(id));
484
+ await this.onExecute(queueName, task);
658
485
  await this.idempotencyDone(keys);
659
- await this.success(id, payload, createdAt, job, key);
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
- await this.attempt(err, id, payload, createdAt, job, key, attempt);
665
- await this.error(err, id, payload, createdAt, job, key, attempt);
666
- if (attempt >= this.workerMaxRetries) {
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:${this.stream.replace(/[^\w:\-]/g, "_")}:`;
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.workerExecuteLockTimeoutMs), keys.token], IdempotencyAllow);
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.workerExecuteLockTimeoutMs)], IdempotencyStart);
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.workerCacheTaskTimeoutMs), keys.token], IdempotencyDone);
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 createGroup(from = "$") {
718
- try {
719
- await this.redis.xgroup("CREATE", this.stream, this.group, from, "MKSTREAM");
720
- } catch (err) {
721
- const msg = String(err?.message || "");
722
- if (!msg.includes("BUSYGROUP")) {
723
- throw err;
724
- }
725
- }
726
- }
727
- async select() {
728
- let entries = await this.selectStuck();
729
- if (!isArrFilled(entries)) {
730
- entries = await this.selectFresh();
731
- }
732
- return this.normalizeEntries(entries);
733
- }
734
- async selectStuck() {
735
- try {
736
- const res = await this.runScript("SelectStuck", [this.stream], [this.group, this.consumer(), String(this.recoveryStuckTasksTimeoutMs), String(this.workerBatchTasksCount), String(this.workerSelectionTimeoutMs)], SelectStuck);
737
- return isArr(res) ? res : [];
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
- return entries;
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.workerExecuteLockTimeoutMs);
802
- const r2 = await this.redis.pexpire(keys.startKey, this.workerExecuteLockTimeoutMs);
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.workerExecuteLockTimeoutMs <= 0) {
604
+ if (this.idemLockTimeout <= 0) {
812
605
  return;
813
606
  }
814
- const workerHeartbeatTimeoutMs = Math.max(1e3, Math.floor(Math.max(5e3, this.workerExecuteLockTimeoutMs | 0) / 4));
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
- normalizeEntries(raw) {
855
- if (!Array.isArray(raw)) {
856
- return [];
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
- values(value) {
869
- const result = {};
870
- for (let i = 0; i < value.length; i += 2) {
871
- result[value[i]] = value[i + 1];
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
- payload(data) {
876
- try {
877
- return jsonDecode(data);
878
- } catch (err) {
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
- return data;
766
+ if (batch.length) {
767
+ batches.push(batch);
768
+ }
769
+ return batches;
881
770
  }
882
- signal() {
883
- return this.abort.signal;
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
- consumer() {
886
- return `${String(this.consumerHost || "host")}:${process.pid}`;
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 {