@nicnocquee/dataqueue 1.34.0 → 1.35.0-beta.20260224110011
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/ai/docs-content.json +27 -15
- package/ai/rules/advanced.md +78 -1
- package/ai/rules/basic.md +73 -3
- package/ai/rules/react-dashboard.md +5 -1
- package/ai/skills/dataqueue-advanced/SKILL.md +181 -0
- package/ai/skills/dataqueue-core/SKILL.md +109 -3
- package/ai/skills/dataqueue-react/SKILL.md +19 -7
- package/dist/index.cjs +1168 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +394 -13
- package/dist/index.d.ts +394 -13
- package/dist/index.js +1168 -173
- package/dist/index.js.map +1 -1
- package/migrations/1781200000005_add_retry_config_to_job_queue.sql +17 -0
- package/migrations/1781200000006_add_output_to_job_queue.sql +3 -0
- package/migrations/1781200000007_add_group_fields_to_job_queue.sql +16 -0
- package/package.json +1 -1
- package/src/backend.ts +37 -3
- package/src/backends/postgres.ts +458 -76
- package/src/backends/redis-scripts.ts +273 -37
- package/src/backends/redis.test.ts +753 -0
- package/src/backends/redis.ts +253 -15
- package/src/db-util.ts +1 -1
- package/src/index.test.ts +811 -12
- package/src/index.ts +106 -14
- package/src/processor.test.ts +18 -0
- package/src/processor.ts +147 -49
- package/src/queue.test.ts +584 -0
- package/src/queue.ts +22 -3
- package/src/supervisor.test.ts +340 -0
- package/src/supervisor.ts +177 -0
- package/src/types.ts +353 -3
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var events = require('events');
|
|
3
4
|
var worker_threads = require('worker_threads');
|
|
4
5
|
var async_hooks = require('async_hooks');
|
|
5
6
|
var pg = require('pg');
|
|
@@ -14,7 +15,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
14
15
|
|
|
15
16
|
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
16
17
|
|
|
17
|
-
// src/
|
|
18
|
+
// src/index.ts
|
|
18
19
|
|
|
19
20
|
// src/types.ts
|
|
20
21
|
var JobEventType = /* @__PURE__ */ ((JobEventType2) => {
|
|
@@ -150,9 +151,9 @@ async function runHandlerInWorker(handler, payload, timeoutMs, jobType) {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
handlerFn(payload, signal)
|
|
153
|
-
.then(() => {
|
|
154
|
+
.then((result) => {
|
|
154
155
|
clearTimeout(timeoutId);
|
|
155
|
-
parentPort.postMessage({ type: 'success' });
|
|
156
|
+
parentPort.postMessage({ type: 'success', output: result });
|
|
156
157
|
})
|
|
157
158
|
.catch((error) => {
|
|
158
159
|
clearTimeout(timeoutId);
|
|
@@ -187,24 +188,27 @@ async function runHandlerInWorker(handler, payload, timeoutMs, jobType) {
|
|
|
187
188
|
}
|
|
188
189
|
});
|
|
189
190
|
let resolved = false;
|
|
190
|
-
worker.on(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
191
|
+
worker.on(
|
|
192
|
+
"message",
|
|
193
|
+
(message) => {
|
|
194
|
+
if (resolved) return;
|
|
195
|
+
resolved = true;
|
|
196
|
+
if (message.type === "success") {
|
|
197
|
+
resolve(message.output);
|
|
198
|
+
} else if (message.type === "timeout") {
|
|
199
|
+
const timeoutError = new Error(
|
|
200
|
+
`Job timed out after ${timeoutMs} ms and was forcefully terminated`
|
|
201
|
+
);
|
|
202
|
+
timeoutError.failureReason = "timeout" /* Timeout */;
|
|
203
|
+
reject(timeoutError);
|
|
204
|
+
} else if (message.type === "error") {
|
|
205
|
+
const error = new Error(message.error.message);
|
|
206
|
+
error.stack = message.error.stack;
|
|
207
|
+
error.name = message.error.name;
|
|
208
|
+
reject(error);
|
|
209
|
+
}
|
|
206
210
|
}
|
|
207
|
-
|
|
211
|
+
);
|
|
208
212
|
worker.on("error", (error) => {
|
|
209
213
|
if (resolved) return;
|
|
210
214
|
resolved = true;
|
|
@@ -361,22 +365,30 @@ function buildWaitContext(backend, jobId, stepData, baseCtx) {
|
|
|
361
365
|
if (percent < 0 || percent > 100)
|
|
362
366
|
throw new Error("Progress must be between 0 and 100");
|
|
363
367
|
await backend.updateProgress(jobId, Math.round(percent));
|
|
368
|
+
},
|
|
369
|
+
setOutput: async (data) => {
|
|
370
|
+
await backend.updateOutput(jobId, data);
|
|
364
371
|
}
|
|
365
372
|
};
|
|
366
373
|
return ctx;
|
|
367
374
|
}
|
|
368
|
-
async function processJobWithHandlers(backend, job, jobHandlers) {
|
|
375
|
+
async function processJobWithHandlers(backend, job, jobHandlers, emit) {
|
|
369
376
|
const handler = jobHandlers[job.jobType];
|
|
370
377
|
if (!handler) {
|
|
371
378
|
await backend.setPendingReasonForUnpickedJobs(
|
|
372
379
|
`No handler registered for job type: ${job.jobType}`,
|
|
373
380
|
job.jobType
|
|
374
381
|
);
|
|
375
|
-
|
|
376
|
-
job.
|
|
377
|
-
new Error(`No handler registered for job type: ${job.jobType}`),
|
|
378
|
-
"no_handler" /* NoHandler */
|
|
382
|
+
const noHandlerError = new Error(
|
|
383
|
+
`No handler registered for job type: ${job.jobType}`
|
|
379
384
|
);
|
|
385
|
+
await backend.failJob(job.id, noHandlerError, "no_handler" /* NoHandler */);
|
|
386
|
+
emit?.("job:failed", {
|
|
387
|
+
jobId: job.id,
|
|
388
|
+
jobType: job.jobType,
|
|
389
|
+
error: noHandlerError,
|
|
390
|
+
willRetry: false
|
|
391
|
+
});
|
|
380
392
|
return;
|
|
381
393
|
}
|
|
382
394
|
const stepData = { ...job.stepData || {} };
|
|
@@ -391,9 +403,16 @@ async function processJobWithHandlers(backend, job, jobHandlers) {
|
|
|
391
403
|
const forceKillOnTimeout = job.forceKillOnTimeout ?? false;
|
|
392
404
|
let timeoutId;
|
|
393
405
|
const controller = new AbortController();
|
|
406
|
+
let setOutputCalled = false;
|
|
407
|
+
let handlerReturnValue;
|
|
394
408
|
try {
|
|
395
409
|
if (forceKillOnTimeout && timeoutMs && timeoutMs > 0) {
|
|
396
|
-
await runHandlerInWorker(
|
|
410
|
+
handlerReturnValue = await runHandlerInWorker(
|
|
411
|
+
handler,
|
|
412
|
+
job.payload,
|
|
413
|
+
timeoutMs,
|
|
414
|
+
job.jobType
|
|
415
|
+
);
|
|
397
416
|
} else {
|
|
398
417
|
let onTimeoutCallback;
|
|
399
418
|
let timeoutReject;
|
|
@@ -445,6 +464,22 @@ async function processJobWithHandlers(backend, job, jobHandlers) {
|
|
|
445
464
|
}
|
|
446
465
|
};
|
|
447
466
|
const ctx = buildWaitContext(backend, job.id, stepData, baseCtx);
|
|
467
|
+
if (emit) {
|
|
468
|
+
const originalSetProgress = ctx.setProgress;
|
|
469
|
+
ctx.setProgress = async (percent) => {
|
|
470
|
+
await originalSetProgress(percent);
|
|
471
|
+
emit("job:progress", {
|
|
472
|
+
jobId: job.id,
|
|
473
|
+
progress: Math.round(percent)
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const originalSetOutput = ctx.setOutput;
|
|
478
|
+
ctx.setOutput = async (data) => {
|
|
479
|
+
setOutputCalled = true;
|
|
480
|
+
await originalSetOutput(data);
|
|
481
|
+
emit?.("job:output", { jobId: job.id, output: data });
|
|
482
|
+
};
|
|
448
483
|
if (forceKillOnTimeout && !hasTimeout) {
|
|
449
484
|
log(
|
|
450
485
|
`forceKillOnTimeout is set but no timeoutMs for job ${job.id}, running without force kill`
|
|
@@ -452,7 +487,7 @@ async function processJobWithHandlers(backend, job, jobHandlers) {
|
|
|
452
487
|
}
|
|
453
488
|
const jobPromise = handler(job.payload, controller.signal, ctx);
|
|
454
489
|
if (hasTimeout) {
|
|
455
|
-
await Promise.race([
|
|
490
|
+
handlerReturnValue = await Promise.race([
|
|
456
491
|
jobPromise,
|
|
457
492
|
new Promise((_, reject) => {
|
|
458
493
|
timeoutReject = reject;
|
|
@@ -460,11 +495,13 @@ async function processJobWithHandlers(backend, job, jobHandlers) {
|
|
|
460
495
|
})
|
|
461
496
|
]);
|
|
462
497
|
} else {
|
|
463
|
-
await jobPromise;
|
|
498
|
+
handlerReturnValue = await jobPromise;
|
|
464
499
|
}
|
|
465
500
|
}
|
|
466
501
|
if (timeoutId) clearTimeout(timeoutId);
|
|
467
|
-
|
|
502
|
+
const completionOutput = setOutputCalled || handlerReturnValue === void 0 ? void 0 : handlerReturnValue;
|
|
503
|
+
await backend.completeJob(job.id, completionOutput);
|
|
504
|
+
emit?.("job:completed", { jobId: job.id, jobType: job.jobType });
|
|
468
505
|
} catch (error) {
|
|
469
506
|
if (timeoutId) clearTimeout(timeoutId);
|
|
470
507
|
if (error instanceof WaitSignal) {
|
|
@@ -476,6 +513,7 @@ async function processJobWithHandlers(backend, job, jobHandlers) {
|
|
|
476
513
|
waitTokenId: error.tokenId,
|
|
477
514
|
stepData: error.stepData
|
|
478
515
|
});
|
|
516
|
+
emit?.("job:waiting", { jobId: job.id, jobType: job.jobType });
|
|
479
517
|
return;
|
|
480
518
|
}
|
|
481
519
|
console.error(`Error processing job ${job.id}:`, error);
|
|
@@ -483,22 +521,33 @@ async function processJobWithHandlers(backend, job, jobHandlers) {
|
|
|
483
521
|
if (error && typeof error === "object" && "failureReason" in error && error.failureReason === "timeout" /* Timeout */) {
|
|
484
522
|
failureReason = "timeout" /* Timeout */;
|
|
485
523
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
524
|
+
const failError = error instanceof Error ? error : new Error(String(error));
|
|
525
|
+
await backend.failJob(job.id, failError, failureReason);
|
|
526
|
+
emit?.("job:failed", {
|
|
527
|
+
jobId: job.id,
|
|
528
|
+
jobType: job.jobType,
|
|
529
|
+
error: failError,
|
|
530
|
+
willRetry: job.attempts + 1 < job.maxAttempts
|
|
531
|
+
});
|
|
491
532
|
}
|
|
492
533
|
}
|
|
493
|
-
async function processBatchWithHandlers(backend, workerId, batchSize, jobType, jobHandlers, concurrency, onError) {
|
|
534
|
+
async function processBatchWithHandlers(backend, workerId, batchSize, jobType, jobHandlers, concurrency, groupConcurrency, onError, emit) {
|
|
494
535
|
const jobs = await backend.getNextBatch(
|
|
495
536
|
workerId,
|
|
496
537
|
batchSize,
|
|
497
|
-
jobType
|
|
538
|
+
jobType,
|
|
539
|
+
groupConcurrency
|
|
498
540
|
);
|
|
541
|
+
if (emit) {
|
|
542
|
+
for (const job of jobs) {
|
|
543
|
+
emit("job:processing", { jobId: job.id, jobType: job.jobType });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
499
546
|
if (!concurrency || concurrency >= jobs.length) {
|
|
500
547
|
await Promise.all(
|
|
501
|
-
jobs.map(
|
|
548
|
+
jobs.map(
|
|
549
|
+
(job) => processJobWithHandlers(backend, job, jobHandlers, emit)
|
|
550
|
+
)
|
|
502
551
|
);
|
|
503
552
|
return jobs.length;
|
|
504
553
|
}
|
|
@@ -511,7 +560,7 @@ async function processBatchWithHandlers(backend, workerId, batchSize, jobType, j
|
|
|
511
560
|
while (running < concurrency && idx < jobs.length) {
|
|
512
561
|
const job = jobs[idx++];
|
|
513
562
|
running++;
|
|
514
|
-
processJobWithHandlers(backend, job, jobHandlers).then(() => {
|
|
563
|
+
processJobWithHandlers(backend, job, jobHandlers, emit).then(() => {
|
|
515
564
|
running--;
|
|
516
565
|
finished++;
|
|
517
566
|
next();
|
|
@@ -528,15 +577,21 @@ async function processBatchWithHandlers(backend, workerId, batchSize, jobType, j
|
|
|
528
577
|
next();
|
|
529
578
|
});
|
|
530
579
|
}
|
|
531
|
-
var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
580
|
+
var createProcessor = (backend, handlers, options = {}, onBeforeBatch, emit) => {
|
|
532
581
|
const {
|
|
533
582
|
workerId = `worker-${Math.random().toString(36).substring(2, 9)}`,
|
|
534
583
|
batchSize = 10,
|
|
535
584
|
pollInterval = 5e3,
|
|
536
585
|
onError = (error) => console.error("Job processor error:", error),
|
|
537
586
|
jobType,
|
|
538
|
-
concurrency = 3
|
|
587
|
+
concurrency = 3,
|
|
588
|
+
groupConcurrency
|
|
539
589
|
} = options;
|
|
590
|
+
if (groupConcurrency !== void 0 && (!Number.isInteger(groupConcurrency) || groupConcurrency <= 0)) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
'Processor option "groupConcurrency" must be a positive integer when provided.'
|
|
593
|
+
);
|
|
594
|
+
}
|
|
540
595
|
let running = false;
|
|
541
596
|
let intervalId = null;
|
|
542
597
|
let currentBatchPromise = null;
|
|
@@ -548,11 +603,11 @@ var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
|
548
603
|
await onBeforeBatch();
|
|
549
604
|
} catch (hookError) {
|
|
550
605
|
log(`onBeforeBatch hook error: ${hookError}`);
|
|
606
|
+
const err = hookError instanceof Error ? hookError : new Error(String(hookError));
|
|
551
607
|
if (onError) {
|
|
552
|
-
onError(
|
|
553
|
-
hookError instanceof Error ? hookError : new Error(String(hookError))
|
|
554
|
-
);
|
|
608
|
+
onError(err);
|
|
555
609
|
}
|
|
610
|
+
emit?.("error", err);
|
|
556
611
|
}
|
|
557
612
|
}
|
|
558
613
|
log(
|
|
@@ -566,11 +621,15 @@ var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
|
566
621
|
jobType,
|
|
567
622
|
handlers,
|
|
568
623
|
concurrency,
|
|
569
|
-
|
|
624
|
+
groupConcurrency,
|
|
625
|
+
onError,
|
|
626
|
+
emit
|
|
570
627
|
);
|
|
571
628
|
return processed;
|
|
572
629
|
} catch (error) {
|
|
573
|
-
|
|
630
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
631
|
+
onError(err);
|
|
632
|
+
emit?.("error", err);
|
|
574
633
|
}
|
|
575
634
|
return 0;
|
|
576
635
|
};
|
|
@@ -649,6 +708,138 @@ var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
|
649
708
|
isRunning: () => running
|
|
650
709
|
};
|
|
651
710
|
};
|
|
711
|
+
|
|
712
|
+
// src/supervisor.ts
|
|
713
|
+
var createSupervisor = (backend, options = {}, emit) => {
|
|
714
|
+
const {
|
|
715
|
+
intervalMs = 6e4,
|
|
716
|
+
stuckJobsTimeoutMinutes = 10,
|
|
717
|
+
cleanupJobsDaysToKeep = 30,
|
|
718
|
+
cleanupEventsDaysToKeep = 30,
|
|
719
|
+
cleanupBatchSize = 1e3,
|
|
720
|
+
reclaimStuckJobs = true,
|
|
721
|
+
expireTimedOutTokens = true,
|
|
722
|
+
onError = (error) => console.error("Supervisor maintenance error:", error),
|
|
723
|
+
verbose = false
|
|
724
|
+
} = options;
|
|
725
|
+
let running = false;
|
|
726
|
+
let timeoutId = null;
|
|
727
|
+
let currentRunPromise = null;
|
|
728
|
+
setLogContext(verbose);
|
|
729
|
+
const runOnce = async () => {
|
|
730
|
+
setLogContext(verbose);
|
|
731
|
+
const result = {
|
|
732
|
+
reclaimedJobs: 0,
|
|
733
|
+
cleanedUpJobs: 0,
|
|
734
|
+
cleanedUpEvents: 0,
|
|
735
|
+
expiredTokens: 0
|
|
736
|
+
};
|
|
737
|
+
if (reclaimStuckJobs) {
|
|
738
|
+
try {
|
|
739
|
+
result.reclaimedJobs = await backend.reclaimStuckJobs(
|
|
740
|
+
stuckJobsTimeoutMinutes
|
|
741
|
+
);
|
|
742
|
+
if (result.reclaimedJobs > 0) {
|
|
743
|
+
log(`Supervisor: reclaimed ${result.reclaimedJobs} stuck jobs`);
|
|
744
|
+
}
|
|
745
|
+
} catch (e) {
|
|
746
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
747
|
+
onError(err);
|
|
748
|
+
emit?.("error", err);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (cleanupJobsDaysToKeep > 0) {
|
|
752
|
+
try {
|
|
753
|
+
result.cleanedUpJobs = await backend.cleanupOldJobs(
|
|
754
|
+
cleanupJobsDaysToKeep,
|
|
755
|
+
cleanupBatchSize
|
|
756
|
+
);
|
|
757
|
+
if (result.cleanedUpJobs > 0) {
|
|
758
|
+
log(`Supervisor: cleaned up ${result.cleanedUpJobs} old jobs`);
|
|
759
|
+
}
|
|
760
|
+
} catch (e) {
|
|
761
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
762
|
+
onError(err);
|
|
763
|
+
emit?.("error", err);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (cleanupEventsDaysToKeep > 0) {
|
|
767
|
+
try {
|
|
768
|
+
result.cleanedUpEvents = await backend.cleanupOldJobEvents(
|
|
769
|
+
cleanupEventsDaysToKeep,
|
|
770
|
+
cleanupBatchSize
|
|
771
|
+
);
|
|
772
|
+
if (result.cleanedUpEvents > 0) {
|
|
773
|
+
log(
|
|
774
|
+
`Supervisor: cleaned up ${result.cleanedUpEvents} old job events`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
} catch (e) {
|
|
778
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
779
|
+
onError(err);
|
|
780
|
+
emit?.("error", err);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (expireTimedOutTokens) {
|
|
784
|
+
try {
|
|
785
|
+
result.expiredTokens = await backend.expireTimedOutWaitpoints();
|
|
786
|
+
if (result.expiredTokens > 0) {
|
|
787
|
+
log(`Supervisor: expired ${result.expiredTokens} timed-out tokens`);
|
|
788
|
+
}
|
|
789
|
+
} catch (e) {
|
|
790
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
791
|
+
onError(err);
|
|
792
|
+
emit?.("error", err);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return result;
|
|
796
|
+
};
|
|
797
|
+
return {
|
|
798
|
+
start: async () => {
|
|
799
|
+
return runOnce();
|
|
800
|
+
},
|
|
801
|
+
startInBackground: () => {
|
|
802
|
+
if (running) return;
|
|
803
|
+
log("Supervisor: starting background maintenance loop");
|
|
804
|
+
running = true;
|
|
805
|
+
const loop = async () => {
|
|
806
|
+
if (!running) return;
|
|
807
|
+
currentRunPromise = runOnce();
|
|
808
|
+
await currentRunPromise;
|
|
809
|
+
currentRunPromise = null;
|
|
810
|
+
if (running) {
|
|
811
|
+
timeoutId = setTimeout(loop, intervalMs);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
loop();
|
|
815
|
+
},
|
|
816
|
+
stop: () => {
|
|
817
|
+
running = false;
|
|
818
|
+
if (timeoutId !== null) {
|
|
819
|
+
clearTimeout(timeoutId);
|
|
820
|
+
timeoutId = null;
|
|
821
|
+
}
|
|
822
|
+
log("Supervisor: stopped");
|
|
823
|
+
},
|
|
824
|
+
stopAndDrain: async (timeoutMs = 3e4) => {
|
|
825
|
+
running = false;
|
|
826
|
+
if (timeoutId !== null) {
|
|
827
|
+
clearTimeout(timeoutId);
|
|
828
|
+
timeoutId = null;
|
|
829
|
+
}
|
|
830
|
+
if (currentRunPromise) {
|
|
831
|
+
log("Supervisor: draining current maintenance run\u2026");
|
|
832
|
+
await Promise.race([
|
|
833
|
+
currentRunPromise,
|
|
834
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
835
|
+
]);
|
|
836
|
+
currentRunPromise = null;
|
|
837
|
+
}
|
|
838
|
+
log("Supervisor: drained and stopped");
|
|
839
|
+
},
|
|
840
|
+
isRunning: () => running
|
|
841
|
+
};
|
|
842
|
+
};
|
|
652
843
|
function loadPemOrFile(value) {
|
|
653
844
|
if (!value) return void 0;
|
|
654
845
|
if (value.startsWith("file://")) {
|
|
@@ -800,6 +991,14 @@ var PostgresBackend = class {
|
|
|
800
991
|
}
|
|
801
992
|
}
|
|
802
993
|
// ── Job CRUD ──────────────────────────────────────────────────────────
|
|
994
|
+
/**
|
|
995
|
+
* Add a job and return its numeric ID.
|
|
996
|
+
*
|
|
997
|
+
* @param job - Job configuration.
|
|
998
|
+
* @param options - Optional. Pass `{ db }` to run the INSERT on an external
|
|
999
|
+
* client (e.g., inside a transaction) so the job is part of the caller's
|
|
1000
|
+
* transaction. The event INSERT also uses the same client.
|
|
1001
|
+
*/
|
|
803
1002
|
async addJob({
|
|
804
1003
|
jobType,
|
|
805
1004
|
payload,
|
|
@@ -809,17 +1008,22 @@ var PostgresBackend = class {
|
|
|
809
1008
|
timeoutMs = void 0,
|
|
810
1009
|
forceKillOnTimeout = false,
|
|
811
1010
|
tags = void 0,
|
|
812
|
-
idempotencyKey = void 0
|
|
813
|
-
|
|
814
|
-
|
|
1011
|
+
idempotencyKey = void 0,
|
|
1012
|
+
retryDelay = void 0,
|
|
1013
|
+
retryBackoff = void 0,
|
|
1014
|
+
retryDelayMax = void 0,
|
|
1015
|
+
group = void 0
|
|
1016
|
+
}, options) {
|
|
1017
|
+
const externalClient = options?.db;
|
|
1018
|
+
const client = externalClient ?? await this.pool.connect();
|
|
815
1019
|
try {
|
|
816
1020
|
let result;
|
|
817
1021
|
const onConflict = idempotencyKey ? `ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING` : "";
|
|
818
1022
|
if (runAt) {
|
|
819
1023
|
result = await client.query(
|
|
820
1024
|
`INSERT INTO job_queue
|
|
821
|
-
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key)
|
|
822
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
1025
|
+
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, group_id, group_tier)
|
|
1026
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
823
1027
|
${onConflict}
|
|
824
1028
|
RETURNING id`,
|
|
825
1029
|
[
|
|
@@ -831,14 +1035,19 @@ var PostgresBackend = class {
|
|
|
831
1035
|
timeoutMs ?? null,
|
|
832
1036
|
forceKillOnTimeout ?? false,
|
|
833
1037
|
tags ?? null,
|
|
834
|
-
idempotencyKey ?? null
|
|
1038
|
+
idempotencyKey ?? null,
|
|
1039
|
+
retryDelay ?? null,
|
|
1040
|
+
retryBackoff ?? null,
|
|
1041
|
+
retryDelayMax ?? null,
|
|
1042
|
+
group?.id ?? null,
|
|
1043
|
+
group?.tier ?? null
|
|
835
1044
|
]
|
|
836
1045
|
);
|
|
837
1046
|
} else {
|
|
838
1047
|
result = await client.query(
|
|
839
1048
|
`INSERT INTO job_queue
|
|
840
|
-
(job_type, payload, max_attempts, priority, timeout_ms, force_kill_on_timeout, tags, idempotency_key)
|
|
841
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
1049
|
+
(job_type, payload, max_attempts, priority, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, group_id, group_tier)
|
|
1050
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
842
1051
|
${onConflict}
|
|
843
1052
|
RETURNING id`,
|
|
844
1053
|
[
|
|
@@ -849,7 +1058,12 @@ var PostgresBackend = class {
|
|
|
849
1058
|
timeoutMs ?? null,
|
|
850
1059
|
forceKillOnTimeout ?? false,
|
|
851
1060
|
tags ?? null,
|
|
852
|
-
idempotencyKey ?? null
|
|
1061
|
+
idempotencyKey ?? null,
|
|
1062
|
+
retryDelay ?? null,
|
|
1063
|
+
retryBackoff ?? null,
|
|
1064
|
+
retryDelayMax ?? null,
|
|
1065
|
+
group?.id ?? null,
|
|
1066
|
+
group?.tier ?? null
|
|
853
1067
|
]
|
|
854
1068
|
);
|
|
855
1069
|
}
|
|
@@ -872,25 +1086,191 @@ var PostgresBackend = class {
|
|
|
872
1086
|
log(
|
|
873
1087
|
`Added job ${jobId}: payload ${JSON.stringify(payload)}, ${runAt ? `runAt ${runAt.toISOString()}, ` : ""}priority ${priority}, maxAttempts ${maxAttempts}, jobType ${jobType}, tags ${JSON.stringify(tags)}${idempotencyKey ? `, idempotencyKey "${idempotencyKey}"` : ""}`
|
|
874
1088
|
);
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1089
|
+
if (externalClient) {
|
|
1090
|
+
try {
|
|
1091
|
+
await client.query(
|
|
1092
|
+
`INSERT INTO job_events (job_id, event_type, metadata) VALUES ($1, $2, $3)`,
|
|
1093
|
+
[
|
|
1094
|
+
jobId,
|
|
1095
|
+
"added" /* Added */,
|
|
1096
|
+
JSON.stringify({ jobType, payload, tags, idempotencyKey })
|
|
1097
|
+
]
|
|
1098
|
+
);
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
log(`Error recording job event for job ${jobId}: ${error}`);
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
await this.recordJobEvent(jobId, "added" /* Added */, {
|
|
1104
|
+
jobType,
|
|
1105
|
+
payload,
|
|
1106
|
+
tags,
|
|
1107
|
+
idempotencyKey
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
881
1110
|
return jobId;
|
|
882
1111
|
} catch (error) {
|
|
883
1112
|
log(`Error adding job: ${error}`);
|
|
884
1113
|
throw error;
|
|
885
1114
|
} finally {
|
|
886
|
-
client.release();
|
|
1115
|
+
if (!externalClient) client.release();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Insert multiple jobs in a single database round-trip.
|
|
1120
|
+
*
|
|
1121
|
+
* Uses a multi-row INSERT with ON CONFLICT handling for idempotency keys.
|
|
1122
|
+
* Returns IDs in the same order as the input array.
|
|
1123
|
+
*/
|
|
1124
|
+
async addJobs(jobs, options) {
|
|
1125
|
+
if (jobs.length === 0) return [];
|
|
1126
|
+
const externalClient = options?.db;
|
|
1127
|
+
const client = externalClient ?? await this.pool.connect();
|
|
1128
|
+
try {
|
|
1129
|
+
const COLS_PER_JOB = 14;
|
|
1130
|
+
const valueClauses = [];
|
|
1131
|
+
const params = [];
|
|
1132
|
+
const hasAnyIdempotencyKey = jobs.some((j) => j.idempotencyKey);
|
|
1133
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
1134
|
+
const {
|
|
1135
|
+
jobType,
|
|
1136
|
+
payload,
|
|
1137
|
+
maxAttempts = 3,
|
|
1138
|
+
priority = 0,
|
|
1139
|
+
runAt = null,
|
|
1140
|
+
timeoutMs = void 0,
|
|
1141
|
+
forceKillOnTimeout = false,
|
|
1142
|
+
tags = void 0,
|
|
1143
|
+
idempotencyKey = void 0,
|
|
1144
|
+
retryDelay = void 0,
|
|
1145
|
+
retryBackoff = void 0,
|
|
1146
|
+
retryDelayMax = void 0,
|
|
1147
|
+
group = void 0
|
|
1148
|
+
} = jobs[i];
|
|
1149
|
+
const base = i * COLS_PER_JOB;
|
|
1150
|
+
valueClauses.push(
|
|
1151
|
+
`($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, COALESCE($${base + 5}::timestamptz, CURRENT_TIMESTAMP), $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11}, $${base + 12}, $${base + 13}, $${base + 14})`
|
|
1152
|
+
);
|
|
1153
|
+
params.push(
|
|
1154
|
+
jobType,
|
|
1155
|
+
payload,
|
|
1156
|
+
maxAttempts,
|
|
1157
|
+
priority,
|
|
1158
|
+
runAt,
|
|
1159
|
+
timeoutMs ?? null,
|
|
1160
|
+
forceKillOnTimeout ?? false,
|
|
1161
|
+
tags ?? null,
|
|
1162
|
+
idempotencyKey ?? null,
|
|
1163
|
+
retryDelay ?? null,
|
|
1164
|
+
retryBackoff ?? null,
|
|
1165
|
+
retryDelayMax ?? null,
|
|
1166
|
+
group?.id ?? null,
|
|
1167
|
+
group?.tier ?? null
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
const onConflict = hasAnyIdempotencyKey ? `ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING` : "";
|
|
1171
|
+
const result = await client.query(
|
|
1172
|
+
`INSERT INTO job_queue
|
|
1173
|
+
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max, group_id, group_tier)
|
|
1174
|
+
VALUES ${valueClauses.join(", ")}
|
|
1175
|
+
${onConflict}
|
|
1176
|
+
RETURNING id, idempotency_key`,
|
|
1177
|
+
params
|
|
1178
|
+
);
|
|
1179
|
+
const returnedKeyToId = /* @__PURE__ */ new Map();
|
|
1180
|
+
const returnedNullKeyIds = [];
|
|
1181
|
+
for (const row of result.rows) {
|
|
1182
|
+
if (row.idempotency_key != null) {
|
|
1183
|
+
returnedKeyToId.set(row.idempotency_key, row.id);
|
|
1184
|
+
} else {
|
|
1185
|
+
returnedNullKeyIds.push(row.id);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const missingKeys = [];
|
|
1189
|
+
for (const job of jobs) {
|
|
1190
|
+
if (job.idempotencyKey && !returnedKeyToId.has(job.idempotencyKey)) {
|
|
1191
|
+
missingKeys.push(job.idempotencyKey);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (missingKeys.length > 0) {
|
|
1195
|
+
const existing = await client.query(
|
|
1196
|
+
`SELECT id, idempotency_key FROM job_queue WHERE idempotency_key = ANY($1)`,
|
|
1197
|
+
[missingKeys]
|
|
1198
|
+
);
|
|
1199
|
+
for (const row of existing.rows) {
|
|
1200
|
+
returnedKeyToId.set(row.idempotency_key, row.id);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
let nullKeyIdx = 0;
|
|
1204
|
+
const ids = [];
|
|
1205
|
+
for (const job of jobs) {
|
|
1206
|
+
if (job.idempotencyKey) {
|
|
1207
|
+
const id = returnedKeyToId.get(job.idempotencyKey);
|
|
1208
|
+
if (id === void 0) {
|
|
1209
|
+
throw new Error(
|
|
1210
|
+
`Failed to resolve job ID for idempotency key "${job.idempotencyKey}"`
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
ids.push(id);
|
|
1214
|
+
} else {
|
|
1215
|
+
ids.push(returnedNullKeyIds[nullKeyIdx++]);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
log(`Batch-inserted ${jobs.length} jobs, IDs: [${ids.join(", ")}]`);
|
|
1219
|
+
const newJobEvents = [];
|
|
1220
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
1221
|
+
const job = jobs[i];
|
|
1222
|
+
const wasInserted = !job.idempotencyKey || !missingKeys.includes(job.idempotencyKey);
|
|
1223
|
+
if (wasInserted) {
|
|
1224
|
+
newJobEvents.push({
|
|
1225
|
+
jobId: ids[i],
|
|
1226
|
+
eventType: "added" /* Added */,
|
|
1227
|
+
metadata: {
|
|
1228
|
+
jobType: job.jobType,
|
|
1229
|
+
payload: job.payload,
|
|
1230
|
+
tags: job.tags,
|
|
1231
|
+
idempotencyKey: job.idempotencyKey
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (newJobEvents.length > 0) {
|
|
1237
|
+
if (externalClient) {
|
|
1238
|
+
const evtValues = [];
|
|
1239
|
+
const evtParams = [];
|
|
1240
|
+
let evtIdx = 1;
|
|
1241
|
+
for (const evt of newJobEvents) {
|
|
1242
|
+
evtValues.push(`($${evtIdx++}, $${evtIdx++}, $${evtIdx++})`);
|
|
1243
|
+
evtParams.push(
|
|
1244
|
+
evt.jobId,
|
|
1245
|
+
evt.eventType,
|
|
1246
|
+
evt.metadata ? JSON.stringify(evt.metadata) : null
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
await client.query(
|
|
1251
|
+
`INSERT INTO job_events (job_id, event_type, metadata) VALUES ${evtValues.join(", ")}`,
|
|
1252
|
+
evtParams
|
|
1253
|
+
);
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
log(`Error recording batch job events: ${error}`);
|
|
1256
|
+
}
|
|
1257
|
+
} else {
|
|
1258
|
+
await this.recordJobEventsBatch(newJobEvents);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return ids;
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
log(`Error batch-inserting jobs: ${error}`);
|
|
1264
|
+
throw error;
|
|
1265
|
+
} finally {
|
|
1266
|
+
if (!externalClient) client.release();
|
|
887
1267
|
}
|
|
888
1268
|
}
|
|
889
1269
|
async getJob(id) {
|
|
890
1270
|
const client = await this.pool.connect();
|
|
891
1271
|
try {
|
|
892
1272
|
const result = await client.query(
|
|
893
|
-
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress FROM job_queue WHERE id = $1`,
|
|
1273
|
+
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", group_id AS "groupId", group_tier AS "groupTier", output FROM job_queue WHERE id = $1`,
|
|
894
1274
|
[id]
|
|
895
1275
|
);
|
|
896
1276
|
if (result.rows.length === 0) {
|
|
@@ -917,7 +1297,7 @@ var PostgresBackend = class {
|
|
|
917
1297
|
const client = await this.pool.connect();
|
|
918
1298
|
try {
|
|
919
1299
|
const result = await client.query(
|
|
920
|
-
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress FROM job_queue WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
|
|
1300
|
+
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", group_id AS "groupId", group_tier AS "groupTier", output FROM job_queue WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
|
|
921
1301
|
[status, limit, offset]
|
|
922
1302
|
);
|
|
923
1303
|
log(`Found ${result.rows.length} jobs by status ${status}`);
|
|
@@ -939,7 +1319,7 @@ var PostgresBackend = class {
|
|
|
939
1319
|
const client = await this.pool.connect();
|
|
940
1320
|
try {
|
|
941
1321
|
const result = await client.query(
|
|
942
|
-
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress FROM job_queue ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
1322
|
+
`SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", group_id AS "groupId", group_tier AS "groupTier", output FROM job_queue ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
943
1323
|
[limit, offset]
|
|
944
1324
|
);
|
|
945
1325
|
log(`Found ${result.rows.length} jobs (all)`);
|
|
@@ -959,7 +1339,7 @@ var PostgresBackend = class {
|
|
|
959
1339
|
async getJobs(filters, limit = 100, offset = 0) {
|
|
960
1340
|
const client = await this.pool.connect();
|
|
961
1341
|
try {
|
|
962
|
-
let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress FROM job_queue`;
|
|
1342
|
+
let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", group_id AS "groupId", group_tier AS "groupTier", output FROM job_queue`;
|
|
963
1343
|
const params = [];
|
|
964
1344
|
const where = [];
|
|
965
1345
|
let paramIdx = 1;
|
|
@@ -1060,7 +1440,7 @@ var PostgresBackend = class {
|
|
|
1060
1440
|
async getJobsByTags(tags, mode = "all", limit = 100, offset = 0) {
|
|
1061
1441
|
const client = await this.pool.connect();
|
|
1062
1442
|
try {
|
|
1063
|
-
let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress
|
|
1443
|
+
let query = `SELECT id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_failed_at AS "lastFailedAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", tags, idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", group_id AS "groupId", group_tier AS "groupTier", output
|
|
1064
1444
|
FROM job_queue`;
|
|
1065
1445
|
let params = [];
|
|
1066
1446
|
switch (mode) {
|
|
@@ -1107,7 +1487,7 @@ var PostgresBackend = class {
|
|
|
1107
1487
|
}
|
|
1108
1488
|
}
|
|
1109
1489
|
// ── Processing lifecycle ──────────────────────────────────────────────
|
|
1110
|
-
async getNextBatch(workerId, batchSize = 10, jobType) {
|
|
1490
|
+
async getNextBatch(workerId, batchSize = 10, jobType, groupConcurrency) {
|
|
1111
1491
|
const client = await this.pool.connect();
|
|
1112
1492
|
try {
|
|
1113
1493
|
await client.query("BEGIN");
|
|
@@ -1115,49 +1495,120 @@ var PostgresBackend = class {
|
|
|
1115
1495
|
const params = [workerId, batchSize];
|
|
1116
1496
|
if (jobType) {
|
|
1117
1497
|
if (Array.isArray(jobType)) {
|
|
1118
|
-
jobTypeFilter = ` AND job_type = ANY($3)`;
|
|
1498
|
+
jobTypeFilter = ` AND candidate.job_type = ANY($3)`;
|
|
1119
1499
|
params.push(jobType);
|
|
1120
1500
|
} else {
|
|
1121
|
-
jobTypeFilter = ` AND job_type = $3`;
|
|
1501
|
+
jobTypeFilter = ` AND candidate.job_type = $3`;
|
|
1122
1502
|
params.push(jobType);
|
|
1123
1503
|
}
|
|
1124
1504
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
WHERE (
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1505
|
+
let result;
|
|
1506
|
+
if (groupConcurrency === void 0) {
|
|
1507
|
+
result = await client.query(
|
|
1508
|
+
`
|
|
1509
|
+
UPDATE job_queue
|
|
1510
|
+
SET status = 'processing',
|
|
1511
|
+
locked_at = NOW(),
|
|
1512
|
+
locked_by = $1,
|
|
1513
|
+
attempts = CASE WHEN status = 'waiting' THEN attempts ELSE attempts + 1 END,
|
|
1514
|
+
updated_at = NOW(),
|
|
1515
|
+
pending_reason = NULL,
|
|
1516
|
+
started_at = COALESCE(started_at, NOW()),
|
|
1517
|
+
last_retried_at = CASE WHEN status != 'waiting' AND attempts > 0 THEN NOW() ELSE last_retried_at END,
|
|
1518
|
+
wait_until = NULL
|
|
1519
|
+
WHERE id IN (
|
|
1520
|
+
SELECT id FROM job_queue candidate
|
|
1521
|
+
WHERE (
|
|
1522
|
+
(
|
|
1523
|
+
(candidate.status = 'pending' OR (candidate.status = 'failed' AND candidate.next_attempt_at <= NOW()))
|
|
1524
|
+
AND (candidate.attempts < candidate.max_attempts)
|
|
1525
|
+
AND candidate.run_at <= NOW()
|
|
1526
|
+
)
|
|
1527
|
+
OR (
|
|
1528
|
+
candidate.status = 'waiting'
|
|
1529
|
+
AND candidate.wait_until IS NOT NULL
|
|
1530
|
+
AND candidate.wait_until <= NOW()
|
|
1531
|
+
AND candidate.wait_token_id IS NULL
|
|
1532
|
+
)
|
|
1144
1533
|
)
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1534
|
+
${jobTypeFilter}
|
|
1535
|
+
ORDER BY candidate.priority DESC, candidate.created_at ASC
|
|
1536
|
+
LIMIT $2
|
|
1537
|
+
FOR UPDATE SKIP LOCKED
|
|
1538
|
+
)
|
|
1539
|
+
RETURNING id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", group_id AS "groupId", group_tier AS "groupTier", output
|
|
1540
|
+
`,
|
|
1541
|
+
params
|
|
1542
|
+
);
|
|
1543
|
+
} else {
|
|
1544
|
+
const constrainedParams = [...params, groupConcurrency];
|
|
1545
|
+
const groupConcurrencyParamIndex = constrainedParams.length;
|
|
1546
|
+
result = await client.query(
|
|
1547
|
+
`
|
|
1548
|
+
WITH eligible AS (
|
|
1549
|
+
SELECT candidate.id, candidate.group_id, candidate.priority, candidate.created_at
|
|
1550
|
+
FROM job_queue candidate
|
|
1551
|
+
WHERE (
|
|
1552
|
+
(
|
|
1553
|
+
(candidate.status = 'pending' OR (candidate.status = 'failed' AND candidate.next_attempt_at <= NOW()))
|
|
1554
|
+
AND (candidate.attempts < candidate.max_attempts)
|
|
1555
|
+
AND candidate.run_at <= NOW()
|
|
1556
|
+
)
|
|
1557
|
+
OR (
|
|
1558
|
+
candidate.status = 'waiting'
|
|
1559
|
+
AND candidate.wait_until IS NOT NULL
|
|
1560
|
+
AND candidate.wait_until <= NOW()
|
|
1561
|
+
AND candidate.wait_token_id IS NULL
|
|
1562
|
+
)
|
|
1150
1563
|
)
|
|
1564
|
+
${jobTypeFilter}
|
|
1565
|
+
FOR UPDATE SKIP LOCKED
|
|
1566
|
+
),
|
|
1567
|
+
ranked AS (
|
|
1568
|
+
SELECT
|
|
1569
|
+
eligible.id,
|
|
1570
|
+
eligible.group_id,
|
|
1571
|
+
eligible.priority,
|
|
1572
|
+
eligible.created_at,
|
|
1573
|
+
ROW_NUMBER() OVER (
|
|
1574
|
+
PARTITION BY eligible.group_id
|
|
1575
|
+
ORDER BY eligible.priority DESC, eligible.created_at ASC
|
|
1576
|
+
) AS group_rank,
|
|
1577
|
+
COALESCE((
|
|
1578
|
+
SELECT COUNT(*)
|
|
1579
|
+
FROM job_queue processing_jobs
|
|
1580
|
+
WHERE processing_jobs.status = 'processing'
|
|
1581
|
+
AND processing_jobs.group_id = eligible.group_id
|
|
1582
|
+
), 0) AS active_group_count
|
|
1583
|
+
FROM eligible
|
|
1584
|
+
),
|
|
1585
|
+
selected AS (
|
|
1586
|
+
SELECT ranked.id
|
|
1587
|
+
FROM ranked
|
|
1588
|
+
WHERE ranked.group_id IS NULL
|
|
1589
|
+
OR (
|
|
1590
|
+
ranked.active_group_count < $${groupConcurrencyParamIndex}
|
|
1591
|
+
AND ranked.group_rank <= ($${groupConcurrencyParamIndex} - ranked.active_group_count)
|
|
1592
|
+
)
|
|
1593
|
+
ORDER BY ranked.priority DESC, ranked.created_at ASC
|
|
1594
|
+
LIMIT $2
|
|
1151
1595
|
)
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1596
|
+
UPDATE job_queue
|
|
1597
|
+
SET status = 'processing',
|
|
1598
|
+
locked_at = NOW(),
|
|
1599
|
+
locked_by = $1,
|
|
1600
|
+
attempts = CASE WHEN status = 'waiting' THEN attempts ELSE attempts + 1 END,
|
|
1601
|
+
updated_at = NOW(),
|
|
1602
|
+
pending_reason = NULL,
|
|
1603
|
+
started_at = COALESCE(started_at, NOW()),
|
|
1604
|
+
last_retried_at = CASE WHEN status != 'waiting' AND attempts > 0 THEN NOW() ELSE last_retried_at END,
|
|
1605
|
+
wait_until = NULL
|
|
1606
|
+
WHERE id IN (SELECT id FROM selected)
|
|
1607
|
+
RETURNING id, job_type AS "jobType", payload, status, max_attempts AS "maxAttempts", attempts, priority, run_at AS "runAt", timeout_ms AS "timeoutMs", force_kill_on_timeout AS "forceKillOnTimeout", created_at AS "createdAt", updated_at AS "updatedAt", started_at AS "startedAt", completed_at AS "completedAt", last_failed_at AS "lastFailedAt", locked_at AS "lockedAt", locked_by AS "lockedBy", error_history AS "errorHistory", failure_reason AS "failureReason", next_attempt_at AS "nextAttemptAt", last_retried_at AS "lastRetriedAt", last_cancelled_at AS "lastCancelledAt", pending_reason AS "pendingReason", idempotency_key AS "idempotencyKey", wait_until AS "waitUntil", wait_token_id AS "waitTokenId", step_data AS "stepData", progress, retry_delay AS "retryDelay", retry_backoff AS "retryBackoff", retry_delay_max AS "retryDelayMax", group_id AS "groupId", group_tier AS "groupTier", output
|
|
1608
|
+
`,
|
|
1609
|
+
constrainedParams
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1161
1612
|
log(`Found ${result.rows.length} jobs to process`);
|
|
1162
1613
|
await client.query("COMMIT");
|
|
1163
1614
|
if (result.rows.length > 0) {
|
|
@@ -1182,17 +1633,19 @@ var PostgresBackend = class {
|
|
|
1182
1633
|
client.release();
|
|
1183
1634
|
}
|
|
1184
1635
|
}
|
|
1185
|
-
async completeJob(jobId) {
|
|
1636
|
+
async completeJob(jobId, output) {
|
|
1186
1637
|
const client = await this.pool.connect();
|
|
1187
1638
|
try {
|
|
1639
|
+
const outputJson = output !== void 0 ? JSON.stringify(output) : null;
|
|
1188
1640
|
const result = await client.query(
|
|
1189
1641
|
`
|
|
1190
1642
|
UPDATE job_queue
|
|
1191
1643
|
SET status = 'completed', updated_at = NOW(), completed_at = NOW(),
|
|
1192
|
-
step_data = NULL, wait_until = NULL, wait_token_id = NULL
|
|
1644
|
+
step_data = NULL, wait_until = NULL, wait_token_id = NULL,
|
|
1645
|
+
output = COALESCE($2::jsonb, output)
|
|
1193
1646
|
WHERE id = $1 AND status = 'processing'
|
|
1194
1647
|
`,
|
|
1195
|
-
[jobId]
|
|
1648
|
+
[jobId, outputJson]
|
|
1196
1649
|
);
|
|
1197
1650
|
if (result.rowCount === 0) {
|
|
1198
1651
|
log(
|
|
@@ -1216,9 +1669,17 @@ var PostgresBackend = class {
|
|
|
1216
1669
|
UPDATE job_queue
|
|
1217
1670
|
SET status = 'failed',
|
|
1218
1671
|
updated_at = NOW(),
|
|
1219
|
-
next_attempt_at = CASE
|
|
1220
|
-
WHEN attempts
|
|
1221
|
-
|
|
1672
|
+
next_attempt_at = CASE
|
|
1673
|
+
WHEN attempts >= max_attempts THEN NULL
|
|
1674
|
+
WHEN retry_delay IS NULL AND retry_backoff IS NULL AND retry_delay_max IS NULL
|
|
1675
|
+
THEN NOW() + (POWER(2, attempts) * INTERVAL '1 minute')
|
|
1676
|
+
WHEN COALESCE(retry_backoff, true) = true
|
|
1677
|
+
THEN NOW() + (LEAST(
|
|
1678
|
+
COALESCE(retry_delay_max, 2147483647),
|
|
1679
|
+
COALESCE(retry_delay, 60) * POWER(2, attempts)
|
|
1680
|
+
) * (0.5 + 0.5 * random()) * INTERVAL '1 second')
|
|
1681
|
+
ELSE
|
|
1682
|
+
NOW() + (COALESCE(retry_delay, 60) * INTERVAL '1 second')
|
|
1222
1683
|
END,
|
|
1223
1684
|
error_history = COALESCE(error_history, '[]'::jsonb) || $2::jsonb,
|
|
1224
1685
|
failure_reason = $3,
|
|
@@ -1287,6 +1748,21 @@ var PostgresBackend = class {
|
|
|
1287
1748
|
client.release();
|
|
1288
1749
|
}
|
|
1289
1750
|
}
|
|
1751
|
+
// ── Output ────────────────────────────────────────────────────────────
|
|
1752
|
+
async updateOutput(jobId, output) {
|
|
1753
|
+
const client = await this.pool.connect();
|
|
1754
|
+
try {
|
|
1755
|
+
await client.query(
|
|
1756
|
+
`UPDATE job_queue SET output = $2::jsonb, updated_at = NOW() WHERE id = $1`,
|
|
1757
|
+
[jobId, JSON.stringify(output)]
|
|
1758
|
+
);
|
|
1759
|
+
log(`Updated output for job ${jobId}`);
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
log(`Error updating output for job ${jobId}: ${error}`);
|
|
1762
|
+
} finally {
|
|
1763
|
+
client.release();
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1290
1766
|
// ── Job management ────────────────────────────────────────────────────
|
|
1291
1767
|
async retryJob(jobId) {
|
|
1292
1768
|
const client = await this.pool.connect();
|
|
@@ -1456,6 +1932,18 @@ var PostgresBackend = class {
|
|
|
1456
1932
|
updateFields.push(`tags = $${paramIdx++}`);
|
|
1457
1933
|
params.push(updates.tags ?? null);
|
|
1458
1934
|
}
|
|
1935
|
+
if (updates.retryDelay !== void 0) {
|
|
1936
|
+
updateFields.push(`retry_delay = $${paramIdx++}`);
|
|
1937
|
+
params.push(updates.retryDelay ?? null);
|
|
1938
|
+
}
|
|
1939
|
+
if (updates.retryBackoff !== void 0) {
|
|
1940
|
+
updateFields.push(`retry_backoff = $${paramIdx++}`);
|
|
1941
|
+
params.push(updates.retryBackoff ?? null);
|
|
1942
|
+
}
|
|
1943
|
+
if (updates.retryDelayMax !== void 0) {
|
|
1944
|
+
updateFields.push(`retry_delay_max = $${paramIdx++}`);
|
|
1945
|
+
params.push(updates.retryDelayMax ?? null);
|
|
1946
|
+
}
|
|
1459
1947
|
if (updateFields.length === 0) {
|
|
1460
1948
|
log(`No fields to update for job ${jobId}`);
|
|
1461
1949
|
return;
|
|
@@ -1477,6 +1965,12 @@ var PostgresBackend = class {
|
|
|
1477
1965
|
if (updates.timeoutMs !== void 0)
|
|
1478
1966
|
metadata.timeoutMs = updates.timeoutMs;
|
|
1479
1967
|
if (updates.tags !== void 0) metadata.tags = updates.tags;
|
|
1968
|
+
if (updates.retryDelay !== void 0)
|
|
1969
|
+
metadata.retryDelay = updates.retryDelay;
|
|
1970
|
+
if (updates.retryBackoff !== void 0)
|
|
1971
|
+
metadata.retryBackoff = updates.retryBackoff;
|
|
1972
|
+
if (updates.retryDelayMax !== void 0)
|
|
1973
|
+
metadata.retryDelayMax = updates.retryDelayMax;
|
|
1480
1974
|
await this.recordJobEvent(jobId, "edited" /* Edited */, metadata);
|
|
1481
1975
|
log(`Edited job ${jobId}: ${JSON.stringify(metadata)}`);
|
|
1482
1976
|
} catch (error) {
|
|
@@ -1520,6 +2014,18 @@ var PostgresBackend = class {
|
|
|
1520
2014
|
updateFields.push(`tags = $${paramIdx++}`);
|
|
1521
2015
|
params.push(updates.tags ?? null);
|
|
1522
2016
|
}
|
|
2017
|
+
if (updates.retryDelay !== void 0) {
|
|
2018
|
+
updateFields.push(`retry_delay = $${paramIdx++}`);
|
|
2019
|
+
params.push(updates.retryDelay ?? null);
|
|
2020
|
+
}
|
|
2021
|
+
if (updates.retryBackoff !== void 0) {
|
|
2022
|
+
updateFields.push(`retry_backoff = $${paramIdx++}`);
|
|
2023
|
+
params.push(updates.retryBackoff ?? null);
|
|
2024
|
+
}
|
|
2025
|
+
if (updates.retryDelayMax !== void 0) {
|
|
2026
|
+
updateFields.push(`retry_delay_max = $${paramIdx++}`);
|
|
2027
|
+
params.push(updates.retryDelayMax ?? null);
|
|
2028
|
+
}
|
|
1523
2029
|
if (updateFields.length === 0) {
|
|
1524
2030
|
log(`No fields to update for batch edit`);
|
|
1525
2031
|
return 0;
|
|
@@ -1761,8 +2267,8 @@ var PostgresBackend = class {
|
|
|
1761
2267
|
`INSERT INTO cron_schedules
|
|
1762
2268
|
(schedule_name, cron_expression, job_type, payload, max_attempts,
|
|
1763
2269
|
priority, timeout_ms, force_kill_on_timeout, tags, timezone,
|
|
1764
|
-
allow_overlap, next_run_at)
|
|
1765
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
2270
|
+
allow_overlap, next_run_at, retry_delay, retry_backoff, retry_delay_max)
|
|
2271
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
1766
2272
|
RETURNING id`,
|
|
1767
2273
|
[
|
|
1768
2274
|
input.scheduleName,
|
|
@@ -1776,7 +2282,10 @@ var PostgresBackend = class {
|
|
|
1776
2282
|
input.tags ?? null,
|
|
1777
2283
|
input.timezone,
|
|
1778
2284
|
input.allowOverlap,
|
|
1779
|
-
input.nextRunAt
|
|
2285
|
+
input.nextRunAt,
|
|
2286
|
+
input.retryDelay,
|
|
2287
|
+
input.retryBackoff,
|
|
2288
|
+
input.retryDelayMax
|
|
1780
2289
|
]
|
|
1781
2290
|
);
|
|
1782
2291
|
const id = result.rows[0].id;
|
|
@@ -1806,7 +2315,9 @@ var PostgresBackend = class {
|
|
|
1806
2315
|
timezone, allow_overlap AS "allowOverlap", status,
|
|
1807
2316
|
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1808
2317
|
next_run_at AS "nextRunAt",
|
|
1809
|
-
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
2318
|
+
created_at AS "createdAt", updated_at AS "updatedAt",
|
|
2319
|
+
retry_delay AS "retryDelay", retry_backoff AS "retryBackoff",
|
|
2320
|
+
retry_delay_max AS "retryDelayMax"
|
|
1810
2321
|
FROM cron_schedules WHERE id = $1`,
|
|
1811
2322
|
[id]
|
|
1812
2323
|
);
|
|
@@ -1831,7 +2342,9 @@ var PostgresBackend = class {
|
|
|
1831
2342
|
timezone, allow_overlap AS "allowOverlap", status,
|
|
1832
2343
|
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1833
2344
|
next_run_at AS "nextRunAt",
|
|
1834
|
-
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
2345
|
+
created_at AS "createdAt", updated_at AS "updatedAt",
|
|
2346
|
+
retry_delay AS "retryDelay", retry_backoff AS "retryBackoff",
|
|
2347
|
+
retry_delay_max AS "retryDelayMax"
|
|
1835
2348
|
FROM cron_schedules WHERE schedule_name = $1`,
|
|
1836
2349
|
[name]
|
|
1837
2350
|
);
|
|
@@ -1855,7 +2368,9 @@ var PostgresBackend = class {
|
|
|
1855
2368
|
timezone, allow_overlap AS "allowOverlap", status,
|
|
1856
2369
|
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1857
2370
|
next_run_at AS "nextRunAt",
|
|
1858
|
-
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
2371
|
+
created_at AS "createdAt", updated_at AS "updatedAt",
|
|
2372
|
+
retry_delay AS "retryDelay", retry_backoff AS "retryBackoff",
|
|
2373
|
+
retry_delay_max AS "retryDelayMax"
|
|
1859
2374
|
FROM cron_schedules`;
|
|
1860
2375
|
const params = [];
|
|
1861
2376
|
if (status) {
|
|
@@ -1960,6 +2475,18 @@ var PostgresBackend = class {
|
|
|
1960
2475
|
updateFields.push(`allow_overlap = $${paramIdx++}`);
|
|
1961
2476
|
params.push(updates.allowOverlap);
|
|
1962
2477
|
}
|
|
2478
|
+
if (updates.retryDelay !== void 0) {
|
|
2479
|
+
updateFields.push(`retry_delay = $${paramIdx++}`);
|
|
2480
|
+
params.push(updates.retryDelay);
|
|
2481
|
+
}
|
|
2482
|
+
if (updates.retryBackoff !== void 0) {
|
|
2483
|
+
updateFields.push(`retry_backoff = $${paramIdx++}`);
|
|
2484
|
+
params.push(updates.retryBackoff);
|
|
2485
|
+
}
|
|
2486
|
+
if (updates.retryDelayMax !== void 0) {
|
|
2487
|
+
updateFields.push(`retry_delay_max = $${paramIdx++}`);
|
|
2488
|
+
params.push(updates.retryDelayMax);
|
|
2489
|
+
}
|
|
1963
2490
|
if (nextRunAt !== void 0) {
|
|
1964
2491
|
updateFields.push(`next_run_at = $${paramIdx++}`);
|
|
1965
2492
|
params.push(nextRunAt);
|
|
@@ -1995,7 +2522,9 @@ var PostgresBackend = class {
|
|
|
1995
2522
|
timezone, allow_overlap AS "allowOverlap", status,
|
|
1996
2523
|
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1997
2524
|
next_run_at AS "nextRunAt",
|
|
1998
|
-
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
2525
|
+
created_at AS "createdAt", updated_at AS "updatedAt",
|
|
2526
|
+
retry_delay AS "retryDelay", retry_backoff AS "retryBackoff",
|
|
2527
|
+
retry_delay_max AS "retryDelayMax"
|
|
1999
2528
|
FROM cron_schedules
|
|
2000
2529
|
WHERE status = 'active'
|
|
2001
2530
|
AND next_run_at IS NOT NULL
|
|
@@ -2279,6 +2808,11 @@ local forceKillOnTimeout = ARGV[7]
|
|
|
2279
2808
|
local tagsJson = ARGV[8] -- "null" or JSON array string
|
|
2280
2809
|
local idempotencyKey = ARGV[9] -- "null" string if not set
|
|
2281
2810
|
local nowMs = tonumber(ARGV[10])
|
|
2811
|
+
local retryDelay = ARGV[11] -- "null" or seconds string
|
|
2812
|
+
local retryBackoff = ARGV[12] -- "null" or "true"/"false"
|
|
2813
|
+
local retryDelayMax = ARGV[13] -- "null" or seconds string
|
|
2814
|
+
local groupId = ARGV[14] -- "null" or group ID
|
|
2815
|
+
local groupTier = ARGV[15] -- "null" or group tier
|
|
2282
2816
|
|
|
2283
2817
|
-- Idempotency check
|
|
2284
2818
|
if idempotencyKey ~= "null" then
|
|
@@ -2322,7 +2856,12 @@ redis.call('HMSET', jobKey,
|
|
|
2322
2856
|
'idempotencyKey', idempotencyKey,
|
|
2323
2857
|
'waitUntil', 'null',
|
|
2324
2858
|
'waitTokenId', 'null',
|
|
2325
|
-
'stepData', 'null'
|
|
2859
|
+
'stepData', 'null',
|
|
2860
|
+
'retryDelay', retryDelay,
|
|
2861
|
+
'retryBackoff', retryBackoff,
|
|
2862
|
+
'retryDelayMax', retryDelayMax,
|
|
2863
|
+
'groupId', groupId,
|
|
2864
|
+
'groupTier', groupTier
|
|
2326
2865
|
)
|
|
2327
2866
|
|
|
2328
2867
|
-- Status index
|
|
@@ -2363,12 +2902,134 @@ end
|
|
|
2363
2902
|
|
|
2364
2903
|
return id
|
|
2365
2904
|
`;
|
|
2905
|
+
var ADD_JOBS_SCRIPT = `
|
|
2906
|
+
local prefix = KEYS[1]
|
|
2907
|
+
local jobsJson = ARGV[1]
|
|
2908
|
+
local nowMs = tonumber(ARGV[2])
|
|
2909
|
+
|
|
2910
|
+
local jobs = cjson.decode(jobsJson)
|
|
2911
|
+
local results = {}
|
|
2912
|
+
|
|
2913
|
+
for i, job in ipairs(jobs) do
|
|
2914
|
+
local jobType = job.jobType
|
|
2915
|
+
local payloadJson = job.payload
|
|
2916
|
+
local maxAttempts = tonumber(job.maxAttempts)
|
|
2917
|
+
local priority = tonumber(job.priority)
|
|
2918
|
+
local runAtMs = tostring(job.runAtMs)
|
|
2919
|
+
local timeoutMs = tostring(job.timeoutMs)
|
|
2920
|
+
local forceKillOnTimeout = tostring(job.forceKillOnTimeout)
|
|
2921
|
+
local tagsJson = tostring(job.tags)
|
|
2922
|
+
local idempotencyKey = tostring(job.idempotencyKey)
|
|
2923
|
+
local retryDelay = tostring(job.retryDelay)
|
|
2924
|
+
local retryBackoff = tostring(job.retryBackoff)
|
|
2925
|
+
local retryDelayMax = tostring(job.retryDelayMax)
|
|
2926
|
+
local groupId = tostring(job.groupId)
|
|
2927
|
+
local groupTier = tostring(job.groupTier)
|
|
2928
|
+
|
|
2929
|
+
-- Idempotency check
|
|
2930
|
+
local skip = false
|
|
2931
|
+
if idempotencyKey ~= "null" then
|
|
2932
|
+
local existing = redis.call('GET', prefix .. 'idempotency:' .. idempotencyKey)
|
|
2933
|
+
if existing then
|
|
2934
|
+
results[i] = tonumber(existing)
|
|
2935
|
+
skip = true
|
|
2936
|
+
end
|
|
2937
|
+
end
|
|
2938
|
+
|
|
2939
|
+
if not skip then
|
|
2940
|
+
-- Generate ID
|
|
2941
|
+
local id = redis.call('INCR', prefix .. 'id_seq')
|
|
2942
|
+
local jobKey = prefix .. 'job:' .. id
|
|
2943
|
+
local runAt = runAtMs ~= "0" and tonumber(runAtMs) or nowMs
|
|
2944
|
+
|
|
2945
|
+
-- Store the job hash
|
|
2946
|
+
redis.call('HMSET', jobKey,
|
|
2947
|
+
'id', id,
|
|
2948
|
+
'jobType', jobType,
|
|
2949
|
+
'payload', payloadJson,
|
|
2950
|
+
'status', 'pending',
|
|
2951
|
+
'maxAttempts', maxAttempts,
|
|
2952
|
+
'attempts', 0,
|
|
2953
|
+
'priority', priority,
|
|
2954
|
+
'runAt', runAt,
|
|
2955
|
+
'timeoutMs', timeoutMs,
|
|
2956
|
+
'forceKillOnTimeout', forceKillOnTimeout,
|
|
2957
|
+
'createdAt', nowMs,
|
|
2958
|
+
'updatedAt', nowMs,
|
|
2959
|
+
'lockedAt', 'null',
|
|
2960
|
+
'lockedBy', 'null',
|
|
2961
|
+
'nextAttemptAt', 'null',
|
|
2962
|
+
'pendingReason', 'null',
|
|
2963
|
+
'errorHistory', '[]',
|
|
2964
|
+
'failureReason', 'null',
|
|
2965
|
+
'completedAt', 'null',
|
|
2966
|
+
'startedAt', 'null',
|
|
2967
|
+
'lastRetriedAt', 'null',
|
|
2968
|
+
'lastFailedAt', 'null',
|
|
2969
|
+
'lastCancelledAt', 'null',
|
|
2970
|
+
'tags', tagsJson,
|
|
2971
|
+
'idempotencyKey', idempotencyKey,
|
|
2972
|
+
'waitUntil', 'null',
|
|
2973
|
+
'waitTokenId', 'null',
|
|
2974
|
+
'stepData', 'null',
|
|
2975
|
+
'retryDelay', retryDelay,
|
|
2976
|
+
'retryBackoff', retryBackoff,
|
|
2977
|
+
'retryDelayMax', retryDelayMax,
|
|
2978
|
+
'groupId', groupId,
|
|
2979
|
+
'groupTier', groupTier
|
|
2980
|
+
)
|
|
2981
|
+
|
|
2982
|
+
-- Status index
|
|
2983
|
+
redis.call('SADD', prefix .. 'status:pending', id)
|
|
2984
|
+
|
|
2985
|
+
-- Type index
|
|
2986
|
+
redis.call('SADD', prefix .. 'type:' .. jobType, id)
|
|
2987
|
+
|
|
2988
|
+
-- Tag indexes
|
|
2989
|
+
if tagsJson ~= "null" then
|
|
2990
|
+
local tags = cjson.decode(tagsJson)
|
|
2991
|
+
for _, tag in ipairs(tags) do
|
|
2992
|
+
redis.call('SADD', prefix .. 'tag:' .. tag, id)
|
|
2993
|
+
end
|
|
2994
|
+
for _, tag in ipairs(tags) do
|
|
2995
|
+
redis.call('SADD', prefix .. 'job:' .. id .. ':tags', tag)
|
|
2996
|
+
end
|
|
2997
|
+
end
|
|
2998
|
+
|
|
2999
|
+
-- Idempotency mapping
|
|
3000
|
+
if idempotencyKey ~= "null" then
|
|
3001
|
+
redis.call('SET', prefix .. 'idempotency:' .. idempotencyKey, id)
|
|
3002
|
+
end
|
|
3003
|
+
|
|
3004
|
+
-- All-jobs sorted set
|
|
3005
|
+
redis.call('ZADD', prefix .. 'all', nowMs, id)
|
|
3006
|
+
|
|
3007
|
+
-- Queue or delayed
|
|
3008
|
+
if runAt <= nowMs then
|
|
3009
|
+
local score = priority * ${SCORE_RANGE} + (${SCORE_RANGE} - nowMs)
|
|
3010
|
+
redis.call('ZADD', prefix .. 'queue', score, id)
|
|
3011
|
+
else
|
|
3012
|
+
redis.call('ZADD', prefix .. 'delayed', runAt, id)
|
|
3013
|
+
end
|
|
3014
|
+
|
|
3015
|
+
results[i] = id
|
|
3016
|
+
end
|
|
3017
|
+
end
|
|
3018
|
+
|
|
3019
|
+
return results
|
|
3020
|
+
`;
|
|
2366
3021
|
var GET_NEXT_BATCH_SCRIPT = `
|
|
2367
3022
|
local prefix = KEYS[1]
|
|
2368
3023
|
local workerId = ARGV[1]
|
|
2369
3024
|
local batchSize = tonumber(ARGV[2])
|
|
2370
3025
|
local nowMs = tonumber(ARGV[3])
|
|
2371
3026
|
local jobTypeFilter = ARGV[4] -- "null" or JSON array or single string
|
|
3027
|
+
local groupConcurrencyRaw = ARGV[5] -- "null" or positive integer
|
|
3028
|
+
local groupConcurrency = nil
|
|
3029
|
+
if groupConcurrencyRaw ~= "null" then
|
|
3030
|
+
groupConcurrency = tonumber(groupConcurrencyRaw)
|
|
3031
|
+
end
|
|
3032
|
+
local groupActiveKey = prefix .. 'group:active'
|
|
2372
3033
|
|
|
2373
3034
|
-- 1. Move ready delayed jobs into queue
|
|
2374
3035
|
local delayed = redis.call('ZRANGEBYSCORE', prefix .. 'delayed', '-inf', nowMs, 'LIMIT', 0, 200)
|
|
@@ -2469,36 +3130,53 @@ for i = 1, #candidates, 2 do
|
|
|
2469
3130
|
-- Not ready yet: move to delayed
|
|
2470
3131
|
redis.call('ZADD', prefix .. 'delayed', runAt, jobId)
|
|
2471
3132
|
else
|
|
2472
|
-
|
|
2473
|
-
local
|
|
2474
|
-
local
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
3133
|
+
local groupId = redis.call('HGET', jk, 'groupId')
|
|
3134
|
+
local hasGroup = groupId and groupId ~= 'null'
|
|
3135
|
+
local canClaim = true
|
|
3136
|
+
if hasGroup and groupConcurrency then
|
|
3137
|
+
local activeCount = tonumber(redis.call('HGET', groupActiveKey, groupId) or '0')
|
|
3138
|
+
if activeCount >= groupConcurrency then
|
|
3139
|
+
table.insert(putBack, score)
|
|
3140
|
+
table.insert(putBack, jobId)
|
|
3141
|
+
canClaim = false
|
|
3142
|
+
end
|
|
3143
|
+
end
|
|
2478
3144
|
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
'
|
|
2482
|
-
'
|
|
2483
|
-
'
|
|
2484
|
-
'
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
'
|
|
2488
|
-
|
|
3145
|
+
if canClaim then
|
|
3146
|
+
-- Claim this job
|
|
3147
|
+
local attempts = tonumber(redis.call('HGET', jk, 'attempts'))
|
|
3148
|
+
local startedAt = redis.call('HGET', jk, 'startedAt')
|
|
3149
|
+
local lastRetriedAt = redis.call('HGET', jk, 'lastRetriedAt')
|
|
3150
|
+
if startedAt == 'null' then startedAt = nowMs end
|
|
3151
|
+
if attempts > 0 then lastRetriedAt = nowMs end
|
|
3152
|
+
|
|
3153
|
+
redis.call('HMSET', jk,
|
|
3154
|
+
'status', 'processing',
|
|
3155
|
+
'lockedAt', nowMs,
|
|
3156
|
+
'lockedBy', workerId,
|
|
3157
|
+
'attempts', attempts + 1,
|
|
3158
|
+
'updatedAt', nowMs,
|
|
3159
|
+
'pendingReason', 'null',
|
|
3160
|
+
'startedAt', startedAt,
|
|
3161
|
+
'lastRetriedAt', lastRetriedAt
|
|
3162
|
+
)
|
|
2489
3163
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
3164
|
+
-- Update status sets
|
|
3165
|
+
redis.call('SREM', prefix .. 'status:pending', jobId)
|
|
3166
|
+
redis.call('SADD', prefix .. 'status:processing', jobId)
|
|
3167
|
+
if hasGroup and groupConcurrency then
|
|
3168
|
+
redis.call('HINCRBY', groupActiveKey, groupId, 1)
|
|
3169
|
+
end
|
|
2493
3170
|
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
3171
|
+
-- Return job data as flat array
|
|
3172
|
+
local data = redis.call('HGETALL', jk)
|
|
3173
|
+
for _, v in ipairs(data) do
|
|
3174
|
+
table.insert(results, v)
|
|
3175
|
+
end
|
|
3176
|
+
-- Separator
|
|
3177
|
+
table.insert(results, '__JOB_SEP__')
|
|
3178
|
+
jobsClaimed = jobsClaimed + 1
|
|
2498
3179
|
end
|
|
2499
|
-
-- Separator
|
|
2500
|
-
table.insert(results, '__JOB_SEP__')
|
|
2501
|
-
jobsClaimed = jobsClaimed + 1
|
|
2502
3180
|
end
|
|
2503
3181
|
end
|
|
2504
3182
|
end
|
|
@@ -2515,18 +3193,34 @@ var COMPLETE_JOB_SCRIPT = `
|
|
|
2515
3193
|
local prefix = KEYS[1]
|
|
2516
3194
|
local jobId = ARGV[1]
|
|
2517
3195
|
local nowMs = ARGV[2]
|
|
3196
|
+
local outputJson = ARGV[3]
|
|
2518
3197
|
local jk = prefix .. 'job:' .. jobId
|
|
3198
|
+
local groupId = redis.call('HGET', jk, 'groupId')
|
|
2519
3199
|
|
|
2520
|
-
|
|
3200
|
+
local fields = {
|
|
2521
3201
|
'status', 'completed',
|
|
2522
3202
|
'updatedAt', nowMs,
|
|
2523
3203
|
'completedAt', nowMs,
|
|
2524
3204
|
'stepData', 'null',
|
|
2525
3205
|
'waitUntil', 'null',
|
|
2526
3206
|
'waitTokenId', 'null'
|
|
2527
|
-
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
if outputJson ~= '__NONE__' then
|
|
3210
|
+
fields[#fields + 1] = 'output'
|
|
3211
|
+
fields[#fields + 1] = outputJson
|
|
3212
|
+
end
|
|
3213
|
+
|
|
3214
|
+
redis.call('HMSET', jk, unpack(fields))
|
|
2528
3215
|
redis.call('SREM', prefix .. 'status:processing', jobId)
|
|
2529
3216
|
redis.call('SADD', prefix .. 'status:completed', jobId)
|
|
3217
|
+
if groupId and groupId ~= 'null' then
|
|
3218
|
+
local activeKey = prefix .. 'group:active'
|
|
3219
|
+
local remaining = redis.call('HINCRBY', activeKey, groupId, -1)
|
|
3220
|
+
if tonumber(remaining) <= 0 then
|
|
3221
|
+
redis.call('HDEL', activeKey, groupId)
|
|
3222
|
+
end
|
|
3223
|
+
end
|
|
2530
3224
|
|
|
2531
3225
|
return 1
|
|
2532
3226
|
`;
|
|
@@ -2537,15 +3231,43 @@ local errorJson = ARGV[2]
|
|
|
2537
3231
|
local failureReason = ARGV[3]
|
|
2538
3232
|
local nowMs = tonumber(ARGV[4])
|
|
2539
3233
|
local jk = prefix .. 'job:' .. jobId
|
|
3234
|
+
local groupId = redis.call('HGET', jk, 'groupId')
|
|
2540
3235
|
|
|
2541
3236
|
local attempts = tonumber(redis.call('HGET', jk, 'attempts'))
|
|
2542
3237
|
local maxAttempts = tonumber(redis.call('HGET', jk, 'maxAttempts'))
|
|
2543
3238
|
|
|
2544
|
-
--
|
|
3239
|
+
-- Read per-job retry config (may be "null")
|
|
3240
|
+
local rdRaw = redis.call('HGET', jk, 'retryDelay')
|
|
3241
|
+
local rbRaw = redis.call('HGET', jk, 'retryBackoff')
|
|
3242
|
+
local rmRaw = redis.call('HGET', jk, 'retryDelayMax')
|
|
3243
|
+
|
|
2545
3244
|
local nextAttemptAt = 'null'
|
|
2546
3245
|
if attempts < maxAttempts then
|
|
2547
|
-
local
|
|
2548
|
-
|
|
3246
|
+
local allNull = (rdRaw == 'null' or rdRaw == false)
|
|
3247
|
+
and (rbRaw == 'null' or rbRaw == false)
|
|
3248
|
+
and (rmRaw == 'null' or rmRaw == false)
|
|
3249
|
+
if allNull then
|
|
3250
|
+
-- Legacy formula: 2^attempts minutes
|
|
3251
|
+
local delayMs = math.pow(2, attempts) * 60000
|
|
3252
|
+
nextAttemptAt = nowMs + delayMs
|
|
3253
|
+
else
|
|
3254
|
+
local retryDelaySec = 60
|
|
3255
|
+
if rdRaw and rdRaw ~= 'null' then retryDelaySec = tonumber(rdRaw) end
|
|
3256
|
+
local useBackoff = true
|
|
3257
|
+
if rbRaw and rbRaw ~= 'null' then useBackoff = (rbRaw == 'true') end
|
|
3258
|
+
local maxDelaySec = nil
|
|
3259
|
+
if rmRaw and rmRaw ~= 'null' then maxDelaySec = tonumber(rmRaw) end
|
|
3260
|
+
|
|
3261
|
+
local delaySec
|
|
3262
|
+
if useBackoff then
|
|
3263
|
+
delaySec = retryDelaySec * math.pow(2, attempts)
|
|
3264
|
+
if maxDelaySec then delaySec = math.min(delaySec, maxDelaySec) end
|
|
3265
|
+
delaySec = delaySec * (0.5 + 0.5 * math.random())
|
|
3266
|
+
else
|
|
3267
|
+
delaySec = retryDelaySec
|
|
3268
|
+
end
|
|
3269
|
+
nextAttemptAt = nowMs + math.floor(delaySec * 1000)
|
|
3270
|
+
end
|
|
2549
3271
|
end
|
|
2550
3272
|
|
|
2551
3273
|
-- Append to error_history
|
|
@@ -2567,6 +3289,13 @@ redis.call('HMSET', jk,
|
|
|
2567
3289
|
)
|
|
2568
3290
|
redis.call('SREM', prefix .. 'status:processing', jobId)
|
|
2569
3291
|
redis.call('SADD', prefix .. 'status:failed', jobId)
|
|
3292
|
+
if groupId and groupId ~= 'null' then
|
|
3293
|
+
local activeKey = prefix .. 'group:active'
|
|
3294
|
+
local remaining = redis.call('HINCRBY', activeKey, groupId, -1)
|
|
3295
|
+
if tonumber(remaining) <= 0 then
|
|
3296
|
+
redis.call('HDEL', activeKey, groupId)
|
|
3297
|
+
end
|
|
3298
|
+
end
|
|
2570
3299
|
|
|
2571
3300
|
-- Schedule retry if applicable
|
|
2572
3301
|
if nextAttemptAt ~= 'null' then
|
|
@@ -2583,6 +3312,7 @@ local jk = prefix .. 'job:' .. jobId
|
|
|
2583
3312
|
|
|
2584
3313
|
local oldStatus = redis.call('HGET', jk, 'status')
|
|
2585
3314
|
if oldStatus ~= 'failed' and oldStatus ~= 'processing' then return 0 end
|
|
3315
|
+
local groupId = redis.call('HGET', jk, 'groupId')
|
|
2586
3316
|
|
|
2587
3317
|
redis.call('HMSET', jk,
|
|
2588
3318
|
'status', 'pending',
|
|
@@ -2596,6 +3326,13 @@ redis.call('HMSET', jk,
|
|
|
2596
3326
|
-- Remove from old status, add to pending
|
|
2597
3327
|
redis.call('SREM', prefix .. 'status:' .. oldStatus, jobId)
|
|
2598
3328
|
redis.call('SADD', prefix .. 'status:pending', jobId)
|
|
3329
|
+
if oldStatus == 'processing' and groupId and groupId ~= 'null' then
|
|
3330
|
+
local activeKey = prefix .. 'group:active'
|
|
3331
|
+
local remaining = redis.call('HINCRBY', activeKey, groupId, -1)
|
|
3332
|
+
if tonumber(remaining) <= 0 then
|
|
3333
|
+
redis.call('HDEL', activeKey, groupId)
|
|
3334
|
+
end
|
|
3335
|
+
end
|
|
2599
3336
|
|
|
2600
3337
|
-- Remove from retry sorted set if present
|
|
2601
3338
|
redis.call('ZREM', prefix .. 'retry', jobId)
|
|
@@ -2682,6 +3419,14 @@ for _, jobId in ipairs(processing) do
|
|
|
2682
3419
|
)
|
|
2683
3420
|
redis.call('SREM', prefix .. 'status:processing', jobId)
|
|
2684
3421
|
redis.call('SADD', prefix .. 'status:pending', jobId)
|
|
3422
|
+
local groupId = redis.call('HGET', jk, 'groupId')
|
|
3423
|
+
if groupId and groupId ~= 'null' then
|
|
3424
|
+
local activeKey = prefix .. 'group:active'
|
|
3425
|
+
local remaining = redis.call('HINCRBY', activeKey, groupId, -1)
|
|
3426
|
+
if tonumber(remaining) <= 0 then
|
|
3427
|
+
redis.call('HDEL', activeKey, groupId)
|
|
3428
|
+
end
|
|
3429
|
+
end
|
|
2685
3430
|
|
|
2686
3431
|
-- Re-add to queue
|
|
2687
3432
|
local priority = tonumber(redis.call('HGET', jk, 'priority') or '0')
|
|
@@ -2748,6 +3493,7 @@ local jk = prefix .. 'job:' .. jobId
|
|
|
2748
3493
|
|
|
2749
3494
|
local status = redis.call('HGET', jk, 'status')
|
|
2750
3495
|
if status ~= 'processing' then return 0 end
|
|
3496
|
+
local groupId = redis.call('HGET', jk, 'groupId')
|
|
2751
3497
|
|
|
2752
3498
|
redis.call('HMSET', jk,
|
|
2753
3499
|
'status', 'waiting',
|
|
@@ -2760,6 +3506,13 @@ redis.call('HMSET', jk,
|
|
|
2760
3506
|
)
|
|
2761
3507
|
redis.call('SREM', prefix .. 'status:processing', jobId)
|
|
2762
3508
|
redis.call('SADD', prefix .. 'status:waiting', jobId)
|
|
3509
|
+
if groupId and groupId ~= 'null' then
|
|
3510
|
+
local activeKey = prefix .. 'group:active'
|
|
3511
|
+
local remaining = redis.call('HINCRBY', activeKey, groupId, -1)
|
|
3512
|
+
if tonumber(remaining) <= 0 then
|
|
3513
|
+
redis.call('HDEL', activeKey, groupId)
|
|
3514
|
+
end
|
|
3515
|
+
end
|
|
2763
3516
|
|
|
2764
3517
|
-- Add to waiting sorted set if time-based wait
|
|
2765
3518
|
if waitUntilMs ~= 'null' then
|
|
@@ -2957,9 +3710,23 @@ function deserializeJob(h) {
|
|
|
2957
3710
|
progress: numOrNull(h.progress),
|
|
2958
3711
|
waitUntil: dateOrNull(h.waitUntil),
|
|
2959
3712
|
waitTokenId: nullish(h.waitTokenId),
|
|
2960
|
-
stepData: parseStepData(h.stepData)
|
|
3713
|
+
stepData: parseStepData(h.stepData),
|
|
3714
|
+
retryDelay: numOrNull(h.retryDelay),
|
|
3715
|
+
retryBackoff: h.retryBackoff === "true" ? true : h.retryBackoff === "false" ? false : null,
|
|
3716
|
+
retryDelayMax: numOrNull(h.retryDelayMax),
|
|
3717
|
+
groupId: nullish(h.groupId),
|
|
3718
|
+
groupTier: nullish(h.groupTier),
|
|
3719
|
+
output: parseJsonField(h.output)
|
|
2961
3720
|
};
|
|
2962
3721
|
}
|
|
3722
|
+
function parseJsonField(raw) {
|
|
3723
|
+
if (!raw || raw === "null") return null;
|
|
3724
|
+
try {
|
|
3725
|
+
return JSON.parse(raw);
|
|
3726
|
+
} catch {
|
|
3727
|
+
return null;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
2963
3730
|
function parseStepData(raw) {
|
|
2964
3731
|
if (!raw || raw === "null") return void 0;
|
|
2965
3732
|
try {
|
|
@@ -2969,7 +3736,23 @@ function parseStepData(raw) {
|
|
|
2969
3736
|
}
|
|
2970
3737
|
}
|
|
2971
3738
|
var RedisBackend = class {
|
|
2972
|
-
|
|
3739
|
+
/**
|
|
3740
|
+
* Create a RedisBackend.
|
|
3741
|
+
*
|
|
3742
|
+
* @param configOrClient - Either `redisConfig` from the config file (the
|
|
3743
|
+
* library creates a new ioredis client) or an existing ioredis client
|
|
3744
|
+
* instance (bring your own).
|
|
3745
|
+
* @param keyPrefix - Key prefix, only used when `configOrClient` is an
|
|
3746
|
+
* external client. Ignored when `redisConfig` is passed (uses
|
|
3747
|
+
* `redisConfig.keyPrefix` instead). Default: `'dq:'`.
|
|
3748
|
+
*/
|
|
3749
|
+
constructor(configOrClient, keyPrefix) {
|
|
3750
|
+
if (configOrClient && typeof configOrClient.eval === "function") {
|
|
3751
|
+
this.client = configOrClient;
|
|
3752
|
+
this.prefix = keyPrefix ?? "dq:";
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
const redisConfig = configOrClient;
|
|
2973
3756
|
let IORedis;
|
|
2974
3757
|
try {
|
|
2975
3758
|
const _require = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
@@ -3042,8 +3825,17 @@ var RedisBackend = class {
|
|
|
3042
3825
|
timeoutMs = void 0,
|
|
3043
3826
|
forceKillOnTimeout = false,
|
|
3044
3827
|
tags = void 0,
|
|
3045
|
-
idempotencyKey = void 0
|
|
3046
|
-
|
|
3828
|
+
idempotencyKey = void 0,
|
|
3829
|
+
retryDelay = void 0,
|
|
3830
|
+
retryBackoff = void 0,
|
|
3831
|
+
retryDelayMax = void 0,
|
|
3832
|
+
group = void 0
|
|
3833
|
+
}, options) {
|
|
3834
|
+
if (options?.db) {
|
|
3835
|
+
throw new Error(
|
|
3836
|
+
"The db option is not supported with the Redis backend. Transactional job creation is only available with PostgreSQL."
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3047
3839
|
const now = this.nowMs();
|
|
3048
3840
|
const runAtMs = runAt ? runAt.getTime() : 0;
|
|
3049
3841
|
const result = await this.client.eval(
|
|
@@ -3059,7 +3851,12 @@ var RedisBackend = class {
|
|
|
3059
3851
|
forceKillOnTimeout ? "true" : "false",
|
|
3060
3852
|
tags ? JSON.stringify(tags) : "null",
|
|
3061
3853
|
idempotencyKey ?? "null",
|
|
3062
|
-
now
|
|
3854
|
+
now,
|
|
3855
|
+
retryDelay !== void 0 ? retryDelay.toString() : "null",
|
|
3856
|
+
retryBackoff !== void 0 ? retryBackoff.toString() : "null",
|
|
3857
|
+
retryDelayMax !== void 0 ? retryDelayMax.toString() : "null",
|
|
3858
|
+
group?.id ?? "null",
|
|
3859
|
+
group?.tier ?? "null"
|
|
3063
3860
|
);
|
|
3064
3861
|
const jobId = Number(result);
|
|
3065
3862
|
log(
|
|
@@ -3073,6 +3870,60 @@ var RedisBackend = class {
|
|
|
3073
3870
|
});
|
|
3074
3871
|
return jobId;
|
|
3075
3872
|
}
|
|
3873
|
+
/**
|
|
3874
|
+
* Insert multiple jobs atomically via a single Lua script.
|
|
3875
|
+
* Returns IDs in the same order as the input array.
|
|
3876
|
+
*/
|
|
3877
|
+
async addJobs(jobs, options) {
|
|
3878
|
+
if (jobs.length === 0) return [];
|
|
3879
|
+
if (options?.db) {
|
|
3880
|
+
throw new Error(
|
|
3881
|
+
"The db option is not supported with the Redis backend. Transactional job creation is only available with PostgreSQL."
|
|
3882
|
+
);
|
|
3883
|
+
}
|
|
3884
|
+
const now = this.nowMs();
|
|
3885
|
+
const jobsPayload = jobs.map((job) => ({
|
|
3886
|
+
jobType: job.jobType,
|
|
3887
|
+
payload: JSON.stringify(job.payload),
|
|
3888
|
+
maxAttempts: job.maxAttempts ?? 3,
|
|
3889
|
+
priority: job.priority ?? 0,
|
|
3890
|
+
runAtMs: job.runAt ? job.runAt.getTime() : 0,
|
|
3891
|
+
timeoutMs: job.timeoutMs !== void 0 ? job.timeoutMs.toString() : "null",
|
|
3892
|
+
forceKillOnTimeout: job.forceKillOnTimeout ? "true" : "false",
|
|
3893
|
+
tags: job.tags ? JSON.stringify(job.tags) : "null",
|
|
3894
|
+
idempotencyKey: job.idempotencyKey ?? "null",
|
|
3895
|
+
retryDelay: job.retryDelay !== void 0 ? job.retryDelay.toString() : "null",
|
|
3896
|
+
retryBackoff: job.retryBackoff !== void 0 ? job.retryBackoff.toString() : "null",
|
|
3897
|
+
retryDelayMax: job.retryDelayMax !== void 0 ? job.retryDelayMax.toString() : "null",
|
|
3898
|
+
groupId: job.group?.id ?? "null",
|
|
3899
|
+
groupTier: job.group?.tier ?? "null"
|
|
3900
|
+
}));
|
|
3901
|
+
const result = await this.client.eval(
|
|
3902
|
+
ADD_JOBS_SCRIPT,
|
|
3903
|
+
1,
|
|
3904
|
+
this.prefix,
|
|
3905
|
+
JSON.stringify(jobsPayload),
|
|
3906
|
+
now
|
|
3907
|
+
);
|
|
3908
|
+
const ids = result.map(Number);
|
|
3909
|
+
log(`Batch-inserted ${jobs.length} jobs, IDs: [${ids.join(", ")}]`);
|
|
3910
|
+
const existingIdempotencyIds = /* @__PURE__ */ new Set();
|
|
3911
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
3912
|
+
if (jobs[i].idempotencyKey) {
|
|
3913
|
+
if (existingIdempotencyIds.has(ids[i])) {
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
existingIdempotencyIds.add(ids[i]);
|
|
3917
|
+
}
|
|
3918
|
+
await this.recordJobEvent(ids[i], "added" /* Added */, {
|
|
3919
|
+
jobType: jobs[i].jobType,
|
|
3920
|
+
payload: jobs[i].payload,
|
|
3921
|
+
tags: jobs[i].tags,
|
|
3922
|
+
idempotencyKey: jobs[i].idempotencyKey
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3925
|
+
return ids;
|
|
3926
|
+
}
|
|
3076
3927
|
async getJob(id) {
|
|
3077
3928
|
const data = await this.client.hgetall(`${this.prefix}job:${id}`);
|
|
3078
3929
|
if (!data || Object.keys(data).length === 0) {
|
|
@@ -3143,7 +3994,7 @@ var RedisBackend = class {
|
|
|
3143
3994
|
return jobs.slice(offset, offset + limit);
|
|
3144
3995
|
}
|
|
3145
3996
|
// ── Processing lifecycle ──────────────────────────────────────────────
|
|
3146
|
-
async getNextBatch(workerId, batchSize = 10, jobType) {
|
|
3997
|
+
async getNextBatch(workerId, batchSize = 10, jobType, groupConcurrency) {
|
|
3147
3998
|
const now = this.nowMs();
|
|
3148
3999
|
const jobTypeFilter = jobType === void 0 ? "null" : Array.isArray(jobType) ? JSON.stringify(jobType) : jobType;
|
|
3149
4000
|
const result = await this.client.eval(
|
|
@@ -3153,7 +4004,8 @@ var RedisBackend = class {
|
|
|
3153
4004
|
workerId,
|
|
3154
4005
|
batchSize,
|
|
3155
4006
|
now,
|
|
3156
|
-
jobTypeFilter
|
|
4007
|
+
jobTypeFilter,
|
|
4008
|
+
groupConcurrency !== void 0 ? groupConcurrency : "null"
|
|
3157
4009
|
);
|
|
3158
4010
|
if (!result || result.length === 0) {
|
|
3159
4011
|
log("Found 0 jobs to process");
|
|
@@ -3178,9 +4030,17 @@ var RedisBackend = class {
|
|
|
3178
4030
|
}
|
|
3179
4031
|
return jobs;
|
|
3180
4032
|
}
|
|
3181
|
-
async completeJob(jobId) {
|
|
4033
|
+
async completeJob(jobId, output) {
|
|
3182
4034
|
const now = this.nowMs();
|
|
3183
|
-
|
|
4035
|
+
const outputArg = output !== void 0 ? JSON.stringify(output) : "__NONE__";
|
|
4036
|
+
await this.client.eval(
|
|
4037
|
+
COMPLETE_JOB_SCRIPT,
|
|
4038
|
+
1,
|
|
4039
|
+
this.prefix,
|
|
4040
|
+
jobId,
|
|
4041
|
+
now,
|
|
4042
|
+
outputArg
|
|
4043
|
+
);
|
|
3184
4044
|
await this.recordJobEvent(jobId, "completed" /* Completed */);
|
|
3185
4045
|
log(`Completed job ${jobId}`);
|
|
3186
4046
|
}
|
|
@@ -3233,6 +4093,22 @@ var RedisBackend = class {
|
|
|
3233
4093
|
log(`Error updating progress for job ${jobId}: ${error}`);
|
|
3234
4094
|
}
|
|
3235
4095
|
}
|
|
4096
|
+
// ── Output ────────────────────────────────────────────────────────────
|
|
4097
|
+
async updateOutput(jobId, output) {
|
|
4098
|
+
try {
|
|
4099
|
+
const now = this.nowMs();
|
|
4100
|
+
await this.client.hset(
|
|
4101
|
+
`${this.prefix}job:${jobId}`,
|
|
4102
|
+
"output",
|
|
4103
|
+
JSON.stringify(output),
|
|
4104
|
+
"updatedAt",
|
|
4105
|
+
now.toString()
|
|
4106
|
+
);
|
|
4107
|
+
log(`Updated output for job ${jobId}`);
|
|
4108
|
+
} catch (error) {
|
|
4109
|
+
log(`Error updating output for job ${jobId}: ${error}`);
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
3236
4112
|
// ── Job management ────────────────────────────────────────────────────
|
|
3237
4113
|
async retryJob(jobId) {
|
|
3238
4114
|
const now = this.nowMs();
|
|
@@ -3339,6 +4215,27 @@ var RedisBackend = class {
|
|
|
3339
4215
|
}
|
|
3340
4216
|
metadata.tags = updates.tags;
|
|
3341
4217
|
}
|
|
4218
|
+
if (updates.retryDelay !== void 0) {
|
|
4219
|
+
fields.push(
|
|
4220
|
+
"retryDelay",
|
|
4221
|
+
updates.retryDelay !== null ? updates.retryDelay.toString() : "null"
|
|
4222
|
+
);
|
|
4223
|
+
metadata.retryDelay = updates.retryDelay;
|
|
4224
|
+
}
|
|
4225
|
+
if (updates.retryBackoff !== void 0) {
|
|
4226
|
+
fields.push(
|
|
4227
|
+
"retryBackoff",
|
|
4228
|
+
updates.retryBackoff !== null ? updates.retryBackoff.toString() : "null"
|
|
4229
|
+
);
|
|
4230
|
+
metadata.retryBackoff = updates.retryBackoff;
|
|
4231
|
+
}
|
|
4232
|
+
if (updates.retryDelayMax !== void 0) {
|
|
4233
|
+
fields.push(
|
|
4234
|
+
"retryDelayMax",
|
|
4235
|
+
updates.retryDelayMax !== null ? updates.retryDelayMax.toString() : "null"
|
|
4236
|
+
);
|
|
4237
|
+
metadata.retryDelayMax = updates.retryDelayMax;
|
|
4238
|
+
}
|
|
3342
4239
|
if (fields.length === 0) {
|
|
3343
4240
|
log(`No fields to update for job ${jobId}`);
|
|
3344
4241
|
return;
|
|
@@ -3813,7 +4710,13 @@ var RedisBackend = class {
|
|
|
3813
4710
|
"createdAt",
|
|
3814
4711
|
now.toString(),
|
|
3815
4712
|
"updatedAt",
|
|
3816
|
-
now.toString()
|
|
4713
|
+
now.toString(),
|
|
4714
|
+
"retryDelay",
|
|
4715
|
+
input.retryDelay !== null && input.retryDelay !== void 0 ? input.retryDelay.toString() : "null",
|
|
4716
|
+
"retryBackoff",
|
|
4717
|
+
input.retryBackoff !== null && input.retryBackoff !== void 0 ? input.retryBackoff.toString() : "null",
|
|
4718
|
+
"retryDelayMax",
|
|
4719
|
+
input.retryDelayMax !== null && input.retryDelayMax !== void 0 ? input.retryDelayMax.toString() : "null"
|
|
3817
4720
|
];
|
|
3818
4721
|
await this.client.hmset(key, ...fields);
|
|
3819
4722
|
await this.client.set(
|
|
@@ -3967,6 +4870,24 @@ var RedisBackend = class {
|
|
|
3967
4870
|
if (updates.allowOverlap !== void 0) {
|
|
3968
4871
|
fields.push("allowOverlap", updates.allowOverlap ? "true" : "false");
|
|
3969
4872
|
}
|
|
4873
|
+
if (updates.retryDelay !== void 0) {
|
|
4874
|
+
fields.push(
|
|
4875
|
+
"retryDelay",
|
|
4876
|
+
updates.retryDelay !== null ? updates.retryDelay.toString() : "null"
|
|
4877
|
+
);
|
|
4878
|
+
}
|
|
4879
|
+
if (updates.retryBackoff !== void 0) {
|
|
4880
|
+
fields.push(
|
|
4881
|
+
"retryBackoff",
|
|
4882
|
+
updates.retryBackoff !== null ? updates.retryBackoff.toString() : "null"
|
|
4883
|
+
);
|
|
4884
|
+
}
|
|
4885
|
+
if (updates.retryDelayMax !== void 0) {
|
|
4886
|
+
fields.push(
|
|
4887
|
+
"retryDelayMax",
|
|
4888
|
+
updates.retryDelayMax !== null ? updates.retryDelayMax.toString() : "null"
|
|
4889
|
+
);
|
|
4890
|
+
}
|
|
3970
4891
|
if (nextRunAt !== void 0) {
|
|
3971
4892
|
const val = nextRunAt !== null ? nextRunAt.getTime().toString() : "null";
|
|
3972
4893
|
fields.push("nextRunAt", val);
|
|
@@ -4085,7 +5006,10 @@ var RedisBackend = class {
|
|
|
4085
5006
|
lastJobId: numOrNull(h.lastJobId),
|
|
4086
5007
|
nextRunAt: dateOrNull(h.nextRunAt),
|
|
4087
5008
|
createdAt: new Date(Number(h.createdAt)),
|
|
4088
|
-
updatedAt: new Date(Number(h.updatedAt))
|
|
5009
|
+
updatedAt: new Date(Number(h.updatedAt)),
|
|
5010
|
+
retryDelay: numOrNull(h.retryDelay),
|
|
5011
|
+
retryBackoff: h.retryBackoff === "true" ? true : h.retryBackoff === "false" ? false : null,
|
|
5012
|
+
retryDelayMax: numOrNull(h.retryDelayMax)
|
|
4089
5013
|
};
|
|
4090
5014
|
}
|
|
4091
5015
|
// ── Private helpers (filters) ─────────────────────────────────────────
|
|
@@ -4208,14 +5132,37 @@ var initJobQueue = (config) => {
|
|
|
4208
5132
|
let backend;
|
|
4209
5133
|
if (backendType === "postgres") {
|
|
4210
5134
|
const pgConfig = config;
|
|
4211
|
-
|
|
4212
|
-
|
|
5135
|
+
if (pgConfig.pool) {
|
|
5136
|
+
backend = new PostgresBackend(pgConfig.pool);
|
|
5137
|
+
} else if (pgConfig.databaseConfig) {
|
|
5138
|
+
const pool = createPool(pgConfig.databaseConfig);
|
|
5139
|
+
backend = new PostgresBackend(pool);
|
|
5140
|
+
} else {
|
|
5141
|
+
throw new Error(
|
|
5142
|
+
'PostgreSQL backend requires either "databaseConfig" or "pool" to be provided.'
|
|
5143
|
+
);
|
|
5144
|
+
}
|
|
4213
5145
|
} else if (backendType === "redis") {
|
|
4214
|
-
const redisConfig = config
|
|
4215
|
-
|
|
5146
|
+
const redisConfig = config;
|
|
5147
|
+
if (redisConfig.client) {
|
|
5148
|
+
backend = new RedisBackend(
|
|
5149
|
+
redisConfig.client,
|
|
5150
|
+
redisConfig.keyPrefix
|
|
5151
|
+
);
|
|
5152
|
+
} else if (redisConfig.redisConfig) {
|
|
5153
|
+
backend = new RedisBackend(redisConfig.redisConfig);
|
|
5154
|
+
} else {
|
|
5155
|
+
throw new Error(
|
|
5156
|
+
'Redis backend requires either "redisConfig" or "client" to be provided.'
|
|
5157
|
+
);
|
|
5158
|
+
}
|
|
4216
5159
|
} else {
|
|
4217
5160
|
throw new Error(`Unknown backend: ${backendType}`);
|
|
4218
5161
|
}
|
|
5162
|
+
const emitter = new events.EventEmitter();
|
|
5163
|
+
const emit = (event, data) => {
|
|
5164
|
+
emitter.emit(event, data);
|
|
5165
|
+
};
|
|
4219
5166
|
const enqueueDueCronJobsImpl = async () => {
|
|
4220
5167
|
const dueSchedules = await backend.getDueCronSchedules();
|
|
4221
5168
|
let count = 0;
|
|
@@ -4243,7 +5190,10 @@ var initJobQueue = (config) => {
|
|
|
4243
5190
|
priority: schedule.priority,
|
|
4244
5191
|
timeoutMs: schedule.timeoutMs ?? void 0,
|
|
4245
5192
|
forceKillOnTimeout: schedule.forceKillOnTimeout,
|
|
4246
|
-
tags: schedule.tags
|
|
5193
|
+
tags: schedule.tags,
|
|
5194
|
+
retryDelay: schedule.retryDelay ?? void 0,
|
|
5195
|
+
retryBackoff: schedule.retryBackoff ?? void 0,
|
|
5196
|
+
retryDelayMax: schedule.retryDelayMax ?? void 0
|
|
4247
5197
|
});
|
|
4248
5198
|
const nextRunAt = getNextCronOccurrence(
|
|
4249
5199
|
schedule.cronExpression,
|
|
@@ -4262,7 +5212,21 @@ var initJobQueue = (config) => {
|
|
|
4262
5212
|
return {
|
|
4263
5213
|
// Job queue operations
|
|
4264
5214
|
addJob: withLogContext(
|
|
4265
|
-
(job) =>
|
|
5215
|
+
async (job, options) => {
|
|
5216
|
+
const jobId = await backend.addJob(job, options);
|
|
5217
|
+
emit("job:added", { jobId, jobType: job.jobType });
|
|
5218
|
+
return jobId;
|
|
5219
|
+
},
|
|
5220
|
+
config.verbose ?? false
|
|
5221
|
+
),
|
|
5222
|
+
addJobs: withLogContext(
|
|
5223
|
+
async (jobs, options) => {
|
|
5224
|
+
const jobIds = await backend.addJobs(jobs, options);
|
|
5225
|
+
for (let i = 0; i < jobIds.length; i++) {
|
|
5226
|
+
emit("job:added", { jobId: jobIds[i], jobType: jobs[i].jobType });
|
|
5227
|
+
}
|
|
5228
|
+
return jobIds;
|
|
5229
|
+
},
|
|
4266
5230
|
config.verbose ?? false
|
|
4267
5231
|
),
|
|
4268
5232
|
getJob: withLogContext(
|
|
@@ -4281,13 +5245,16 @@ var initJobQueue = (config) => {
|
|
|
4281
5245
|
(filters, limit, offset) => backend.getJobs(filters, limit, offset),
|
|
4282
5246
|
config.verbose ?? false
|
|
4283
5247
|
),
|
|
4284
|
-
retryJob: (jobId) =>
|
|
5248
|
+
retryJob: async (jobId) => {
|
|
5249
|
+
await backend.retryJob(jobId);
|
|
5250
|
+
emit("job:retried", { jobId });
|
|
5251
|
+
},
|
|
4285
5252
|
cleanupOldJobs: (daysToKeep, batchSize) => backend.cleanupOldJobs(daysToKeep, batchSize),
|
|
4286
5253
|
cleanupOldJobEvents: (daysToKeep, batchSize) => backend.cleanupOldJobEvents(daysToKeep, batchSize),
|
|
4287
|
-
cancelJob: withLogContext(
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
),
|
|
5254
|
+
cancelJob: withLogContext(async (jobId) => {
|
|
5255
|
+
await backend.cancelJob(jobId);
|
|
5256
|
+
emit("job:cancelled", { jobId });
|
|
5257
|
+
}, config.verbose ?? false),
|
|
4291
5258
|
editJob: withLogContext(
|
|
4292
5259
|
(jobId, updates) => backend.editJob(jobId, updates),
|
|
4293
5260
|
config.verbose ?? false
|
|
@@ -4312,9 +5279,17 @@ var initJobQueue = (config) => {
|
|
|
4312
5279
|
config.verbose ?? false
|
|
4313
5280
|
),
|
|
4314
5281
|
// Job processing — automatically enqueues due cron jobs before each batch
|
|
4315
|
-
createProcessor: (handlers, options) => createProcessor(
|
|
4316
|
-
|
|
4317
|
-
|
|
5282
|
+
createProcessor: (handlers, options) => createProcessor(
|
|
5283
|
+
backend,
|
|
5284
|
+
handlers,
|
|
5285
|
+
options,
|
|
5286
|
+
async () => {
|
|
5287
|
+
await enqueueDueCronJobsImpl();
|
|
5288
|
+
},
|
|
5289
|
+
emit
|
|
5290
|
+
),
|
|
5291
|
+
// Background supervisor — automated maintenance
|
|
5292
|
+
createSupervisor: (options) => createSupervisor(backend, options, emit),
|
|
4318
5293
|
// Job events
|
|
4319
5294
|
getJobEvents: withLogContext(
|
|
4320
5295
|
(jobId) => backend.getJobEvents(jobId),
|
|
@@ -4361,7 +5336,10 @@ var initJobQueue = (config) => {
|
|
|
4361
5336
|
tags: options.tags,
|
|
4362
5337
|
timezone: options.timezone ?? "UTC",
|
|
4363
5338
|
allowOverlap: options.allowOverlap ?? false,
|
|
4364
|
-
nextRunAt
|
|
5339
|
+
nextRunAt,
|
|
5340
|
+
retryDelay: options.retryDelay ?? null,
|
|
5341
|
+
retryBackoff: options.retryBackoff ?? null,
|
|
5342
|
+
retryDelayMax: options.retryDelayMax ?? null
|
|
4365
5343
|
};
|
|
4366
5344
|
return backend.addCronSchedule(input);
|
|
4367
5345
|
},
|
|
@@ -4413,6 +5391,23 @@ var initJobQueue = (config) => {
|
|
|
4413
5391
|
() => enqueueDueCronJobsImpl(),
|
|
4414
5392
|
config.verbose ?? false
|
|
4415
5393
|
),
|
|
5394
|
+
// Event hooks
|
|
5395
|
+
on: (event, listener) => {
|
|
5396
|
+
emitter.on(event, listener);
|
|
5397
|
+
},
|
|
5398
|
+
once: (event, listener) => {
|
|
5399
|
+
emitter.once(event, listener);
|
|
5400
|
+
},
|
|
5401
|
+
off: (event, listener) => {
|
|
5402
|
+
emitter.off(event, listener);
|
|
5403
|
+
},
|
|
5404
|
+
removeAllListeners: (event) => {
|
|
5405
|
+
if (event) {
|
|
5406
|
+
emitter.removeAllListeners(event);
|
|
5407
|
+
} else {
|
|
5408
|
+
emitter.removeAllListeners();
|
|
5409
|
+
}
|
|
5410
|
+
},
|
|
4416
5411
|
// Advanced access
|
|
4417
5412
|
getPool: () => {
|
|
4418
5413
|
if (!(backend instanceof PostgresBackend)) {
|