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