@nicnocquee/dataqueue 1.34.0 → 1.35.0-beta.20260224075710
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 +23 -11
- package/ai/rules/advanced.md +77 -1
- package/ai/rules/basic.md +72 -3
- package/ai/rules/react-dashboard.md +5 -1
- package/ai/skills/dataqueue-advanced/SKILL.md +159 -0
- package/ai/skills/dataqueue-core/SKILL.md +107 -3
- package/ai/skills/dataqueue-react/SKILL.md +19 -7
- package/dist/index.cjs +937 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +358 -11
- package/dist/index.d.ts +358 -11
- package/dist/index.js +937 -108
- 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/package.json +1 -1
- package/src/backend.ts +36 -3
- package/src/backends/postgres.ts +344 -42
- package/src/backends/redis-scripts.ts +173 -8
- package/src/backends/redis.test.ts +668 -0
- package/src/backends/redis.ts +244 -15
- package/src/db-util.ts +1 -1
- package/src/index.test.ts +811 -12
- package/src/index.ts +106 -14
- package/src/processor.ts +133 -49
- package/src/queue.test.ts +477 -0
- package/src/queue.ts +20 -3
- package/src/supervisor.test.ts +340 -0
- package/src/supervisor.ts +177 -0
- package/src/types.ts +318 -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,32 @@ 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, onError, emit) {
|
|
494
535
|
const jobs = await backend.getNextBatch(
|
|
495
536
|
workerId,
|
|
496
537
|
batchSize,
|
|
497
538
|
jobType
|
|
498
539
|
);
|
|
540
|
+
if (emit) {
|
|
541
|
+
for (const job of jobs) {
|
|
542
|
+
emit("job:processing", { jobId: job.id, jobType: job.jobType });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
499
545
|
if (!concurrency || concurrency >= jobs.length) {
|
|
500
546
|
await Promise.all(
|
|
501
|
-
jobs.map(
|
|
547
|
+
jobs.map(
|
|
548
|
+
(job) => processJobWithHandlers(backend, job, jobHandlers, emit)
|
|
549
|
+
)
|
|
502
550
|
);
|
|
503
551
|
return jobs.length;
|
|
504
552
|
}
|
|
@@ -511,7 +559,7 @@ async function processBatchWithHandlers(backend, workerId, batchSize, jobType, j
|
|
|
511
559
|
while (running < concurrency && idx < jobs.length) {
|
|
512
560
|
const job = jobs[idx++];
|
|
513
561
|
running++;
|
|
514
|
-
processJobWithHandlers(backend, job, jobHandlers).then(() => {
|
|
562
|
+
processJobWithHandlers(backend, job, jobHandlers, emit).then(() => {
|
|
515
563
|
running--;
|
|
516
564
|
finished++;
|
|
517
565
|
next();
|
|
@@ -528,7 +576,7 @@ async function processBatchWithHandlers(backend, workerId, batchSize, jobType, j
|
|
|
528
576
|
next();
|
|
529
577
|
});
|
|
530
578
|
}
|
|
531
|
-
var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
579
|
+
var createProcessor = (backend, handlers, options = {}, onBeforeBatch, emit) => {
|
|
532
580
|
const {
|
|
533
581
|
workerId = `worker-${Math.random().toString(36).substring(2, 9)}`,
|
|
534
582
|
batchSize = 10,
|
|
@@ -548,11 +596,11 @@ var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
|
548
596
|
await onBeforeBatch();
|
|
549
597
|
} catch (hookError) {
|
|
550
598
|
log(`onBeforeBatch hook error: ${hookError}`);
|
|
599
|
+
const err = hookError instanceof Error ? hookError : new Error(String(hookError));
|
|
551
600
|
if (onError) {
|
|
552
|
-
onError(
|
|
553
|
-
hookError instanceof Error ? hookError : new Error(String(hookError))
|
|
554
|
-
);
|
|
601
|
+
onError(err);
|
|
555
602
|
}
|
|
603
|
+
emit?.("error", err);
|
|
556
604
|
}
|
|
557
605
|
}
|
|
558
606
|
log(
|
|
@@ -566,11 +614,14 @@ var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
|
566
614
|
jobType,
|
|
567
615
|
handlers,
|
|
568
616
|
concurrency,
|
|
569
|
-
onError
|
|
617
|
+
onError,
|
|
618
|
+
emit
|
|
570
619
|
);
|
|
571
620
|
return processed;
|
|
572
621
|
} catch (error) {
|
|
573
|
-
|
|
622
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
623
|
+
onError(err);
|
|
624
|
+
emit?.("error", err);
|
|
574
625
|
}
|
|
575
626
|
return 0;
|
|
576
627
|
};
|
|
@@ -649,6 +700,138 @@ var createProcessor = (backend, handlers, options = {}, onBeforeBatch) => {
|
|
|
649
700
|
isRunning: () => running
|
|
650
701
|
};
|
|
651
702
|
};
|
|
703
|
+
|
|
704
|
+
// src/supervisor.ts
|
|
705
|
+
var createSupervisor = (backend, options = {}, emit) => {
|
|
706
|
+
const {
|
|
707
|
+
intervalMs = 6e4,
|
|
708
|
+
stuckJobsTimeoutMinutes = 10,
|
|
709
|
+
cleanupJobsDaysToKeep = 30,
|
|
710
|
+
cleanupEventsDaysToKeep = 30,
|
|
711
|
+
cleanupBatchSize = 1e3,
|
|
712
|
+
reclaimStuckJobs = true,
|
|
713
|
+
expireTimedOutTokens = true,
|
|
714
|
+
onError = (error) => console.error("Supervisor maintenance error:", error),
|
|
715
|
+
verbose = false
|
|
716
|
+
} = options;
|
|
717
|
+
let running = false;
|
|
718
|
+
let timeoutId = null;
|
|
719
|
+
let currentRunPromise = null;
|
|
720
|
+
setLogContext(verbose);
|
|
721
|
+
const runOnce = async () => {
|
|
722
|
+
setLogContext(verbose);
|
|
723
|
+
const result = {
|
|
724
|
+
reclaimedJobs: 0,
|
|
725
|
+
cleanedUpJobs: 0,
|
|
726
|
+
cleanedUpEvents: 0,
|
|
727
|
+
expiredTokens: 0
|
|
728
|
+
};
|
|
729
|
+
if (reclaimStuckJobs) {
|
|
730
|
+
try {
|
|
731
|
+
result.reclaimedJobs = await backend.reclaimStuckJobs(
|
|
732
|
+
stuckJobsTimeoutMinutes
|
|
733
|
+
);
|
|
734
|
+
if (result.reclaimedJobs > 0) {
|
|
735
|
+
log(`Supervisor: reclaimed ${result.reclaimedJobs} stuck jobs`);
|
|
736
|
+
}
|
|
737
|
+
} catch (e) {
|
|
738
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
739
|
+
onError(err);
|
|
740
|
+
emit?.("error", err);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (cleanupJobsDaysToKeep > 0) {
|
|
744
|
+
try {
|
|
745
|
+
result.cleanedUpJobs = await backend.cleanupOldJobs(
|
|
746
|
+
cleanupJobsDaysToKeep,
|
|
747
|
+
cleanupBatchSize
|
|
748
|
+
);
|
|
749
|
+
if (result.cleanedUpJobs > 0) {
|
|
750
|
+
log(`Supervisor: cleaned up ${result.cleanedUpJobs} old jobs`);
|
|
751
|
+
}
|
|
752
|
+
} catch (e) {
|
|
753
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
754
|
+
onError(err);
|
|
755
|
+
emit?.("error", err);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (cleanupEventsDaysToKeep > 0) {
|
|
759
|
+
try {
|
|
760
|
+
result.cleanedUpEvents = await backend.cleanupOldJobEvents(
|
|
761
|
+
cleanupEventsDaysToKeep,
|
|
762
|
+
cleanupBatchSize
|
|
763
|
+
);
|
|
764
|
+
if (result.cleanedUpEvents > 0) {
|
|
765
|
+
log(
|
|
766
|
+
`Supervisor: cleaned up ${result.cleanedUpEvents} old job events`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
} catch (e) {
|
|
770
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
771
|
+
onError(err);
|
|
772
|
+
emit?.("error", err);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (expireTimedOutTokens) {
|
|
776
|
+
try {
|
|
777
|
+
result.expiredTokens = await backend.expireTimedOutWaitpoints();
|
|
778
|
+
if (result.expiredTokens > 0) {
|
|
779
|
+
log(`Supervisor: expired ${result.expiredTokens} timed-out tokens`);
|
|
780
|
+
}
|
|
781
|
+
} catch (e) {
|
|
782
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
783
|
+
onError(err);
|
|
784
|
+
emit?.("error", err);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return result;
|
|
788
|
+
};
|
|
789
|
+
return {
|
|
790
|
+
start: async () => {
|
|
791
|
+
return runOnce();
|
|
792
|
+
},
|
|
793
|
+
startInBackground: () => {
|
|
794
|
+
if (running) return;
|
|
795
|
+
log("Supervisor: starting background maintenance loop");
|
|
796
|
+
running = true;
|
|
797
|
+
const loop = async () => {
|
|
798
|
+
if (!running) return;
|
|
799
|
+
currentRunPromise = runOnce();
|
|
800
|
+
await currentRunPromise;
|
|
801
|
+
currentRunPromise = null;
|
|
802
|
+
if (running) {
|
|
803
|
+
timeoutId = setTimeout(loop, intervalMs);
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
loop();
|
|
807
|
+
},
|
|
808
|
+
stop: () => {
|
|
809
|
+
running = false;
|
|
810
|
+
if (timeoutId !== null) {
|
|
811
|
+
clearTimeout(timeoutId);
|
|
812
|
+
timeoutId = null;
|
|
813
|
+
}
|
|
814
|
+
log("Supervisor: stopped");
|
|
815
|
+
},
|
|
816
|
+
stopAndDrain: async (timeoutMs = 3e4) => {
|
|
817
|
+
running = false;
|
|
818
|
+
if (timeoutId !== null) {
|
|
819
|
+
clearTimeout(timeoutId);
|
|
820
|
+
timeoutId = null;
|
|
821
|
+
}
|
|
822
|
+
if (currentRunPromise) {
|
|
823
|
+
log("Supervisor: draining current maintenance run\u2026");
|
|
824
|
+
await Promise.race([
|
|
825
|
+
currentRunPromise,
|
|
826
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
827
|
+
]);
|
|
828
|
+
currentRunPromise = null;
|
|
829
|
+
}
|
|
830
|
+
log("Supervisor: drained and stopped");
|
|
831
|
+
},
|
|
832
|
+
isRunning: () => running
|
|
833
|
+
};
|
|
834
|
+
};
|
|
652
835
|
function loadPemOrFile(value) {
|
|
653
836
|
if (!value) return void 0;
|
|
654
837
|
if (value.startsWith("file://")) {
|
|
@@ -800,6 +983,14 @@ var PostgresBackend = class {
|
|
|
800
983
|
}
|
|
801
984
|
}
|
|
802
985
|
// ── Job CRUD ──────────────────────────────────────────────────────────
|
|
986
|
+
/**
|
|
987
|
+
* Add a job and return its numeric ID.
|
|
988
|
+
*
|
|
989
|
+
* @param job - Job configuration.
|
|
990
|
+
* @param options - Optional. Pass `{ db }` to run the INSERT on an external
|
|
991
|
+
* client (e.g., inside a transaction) so the job is part of the caller's
|
|
992
|
+
* transaction. The event INSERT also uses the same client.
|
|
993
|
+
*/
|
|
803
994
|
async addJob({
|
|
804
995
|
jobType,
|
|
805
996
|
payload,
|
|
@@ -809,17 +1000,21 @@ var PostgresBackend = class {
|
|
|
809
1000
|
timeoutMs = void 0,
|
|
810
1001
|
forceKillOnTimeout = false,
|
|
811
1002
|
tags = void 0,
|
|
812
|
-
idempotencyKey = void 0
|
|
813
|
-
|
|
814
|
-
|
|
1003
|
+
idempotencyKey = void 0,
|
|
1004
|
+
retryDelay = void 0,
|
|
1005
|
+
retryBackoff = void 0,
|
|
1006
|
+
retryDelayMax = void 0
|
|
1007
|
+
}, options) {
|
|
1008
|
+
const externalClient = options?.db;
|
|
1009
|
+
const client = externalClient ?? await this.pool.connect();
|
|
815
1010
|
try {
|
|
816
1011
|
let result;
|
|
817
1012
|
const onConflict = idempotencyKey ? `ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING` : "";
|
|
818
1013
|
if (runAt) {
|
|
819
1014
|
result = await client.query(
|
|
820
1015
|
`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)
|
|
1016
|
+
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max)
|
|
1017
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
823
1018
|
${onConflict}
|
|
824
1019
|
RETURNING id`,
|
|
825
1020
|
[
|
|
@@ -831,14 +1026,17 @@ var PostgresBackend = class {
|
|
|
831
1026
|
timeoutMs ?? null,
|
|
832
1027
|
forceKillOnTimeout ?? false,
|
|
833
1028
|
tags ?? null,
|
|
834
|
-
idempotencyKey ?? null
|
|
1029
|
+
idempotencyKey ?? null,
|
|
1030
|
+
retryDelay ?? null,
|
|
1031
|
+
retryBackoff ?? null,
|
|
1032
|
+
retryDelayMax ?? null
|
|
835
1033
|
]
|
|
836
1034
|
);
|
|
837
1035
|
} else {
|
|
838
1036
|
result = await client.query(
|
|
839
1037
|
`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)
|
|
1038
|
+
(job_type, payload, max_attempts, priority, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max)
|
|
1039
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
842
1040
|
${onConflict}
|
|
843
1041
|
RETURNING id`,
|
|
844
1042
|
[
|
|
@@ -849,7 +1047,10 @@ var PostgresBackend = class {
|
|
|
849
1047
|
timeoutMs ?? null,
|
|
850
1048
|
forceKillOnTimeout ?? false,
|
|
851
1049
|
tags ?? null,
|
|
852
|
-
idempotencyKey ?? null
|
|
1050
|
+
idempotencyKey ?? null,
|
|
1051
|
+
retryDelay ?? null,
|
|
1052
|
+
retryBackoff ?? null,
|
|
1053
|
+
retryDelayMax ?? null
|
|
853
1054
|
]
|
|
854
1055
|
);
|
|
855
1056
|
}
|
|
@@ -872,25 +1073,188 @@ var PostgresBackend = class {
|
|
|
872
1073
|
log(
|
|
873
1074
|
`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
1075
|
);
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1076
|
+
if (externalClient) {
|
|
1077
|
+
try {
|
|
1078
|
+
await client.query(
|
|
1079
|
+
`INSERT INTO job_events (job_id, event_type, metadata) VALUES ($1, $2, $3)`,
|
|
1080
|
+
[
|
|
1081
|
+
jobId,
|
|
1082
|
+
"added" /* Added */,
|
|
1083
|
+
JSON.stringify({ jobType, payload, tags, idempotencyKey })
|
|
1084
|
+
]
|
|
1085
|
+
);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
log(`Error recording job event for job ${jobId}: ${error}`);
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
await this.recordJobEvent(jobId, "added" /* Added */, {
|
|
1091
|
+
jobType,
|
|
1092
|
+
payload,
|
|
1093
|
+
tags,
|
|
1094
|
+
idempotencyKey
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
881
1097
|
return jobId;
|
|
882
1098
|
} catch (error) {
|
|
883
1099
|
log(`Error adding job: ${error}`);
|
|
884
1100
|
throw error;
|
|
885
1101
|
} finally {
|
|
886
|
-
client.release();
|
|
1102
|
+
if (!externalClient) client.release();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Insert multiple jobs in a single database round-trip.
|
|
1107
|
+
*
|
|
1108
|
+
* Uses a multi-row INSERT with ON CONFLICT handling for idempotency keys.
|
|
1109
|
+
* Returns IDs in the same order as the input array.
|
|
1110
|
+
*/
|
|
1111
|
+
async addJobs(jobs, options) {
|
|
1112
|
+
if (jobs.length === 0) return [];
|
|
1113
|
+
const externalClient = options?.db;
|
|
1114
|
+
const client = externalClient ?? await this.pool.connect();
|
|
1115
|
+
try {
|
|
1116
|
+
const COLS_PER_JOB = 12;
|
|
1117
|
+
const valueClauses = [];
|
|
1118
|
+
const params = [];
|
|
1119
|
+
const hasAnyIdempotencyKey = jobs.some((j) => j.idempotencyKey);
|
|
1120
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
1121
|
+
const {
|
|
1122
|
+
jobType,
|
|
1123
|
+
payload,
|
|
1124
|
+
maxAttempts = 3,
|
|
1125
|
+
priority = 0,
|
|
1126
|
+
runAt = null,
|
|
1127
|
+
timeoutMs = void 0,
|
|
1128
|
+
forceKillOnTimeout = false,
|
|
1129
|
+
tags = void 0,
|
|
1130
|
+
idempotencyKey = void 0,
|
|
1131
|
+
retryDelay = void 0,
|
|
1132
|
+
retryBackoff = void 0,
|
|
1133
|
+
retryDelayMax = void 0
|
|
1134
|
+
} = jobs[i];
|
|
1135
|
+
const base = i * COLS_PER_JOB;
|
|
1136
|
+
valueClauses.push(
|
|
1137
|
+
`($${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})`
|
|
1138
|
+
);
|
|
1139
|
+
params.push(
|
|
1140
|
+
jobType,
|
|
1141
|
+
payload,
|
|
1142
|
+
maxAttempts,
|
|
1143
|
+
priority,
|
|
1144
|
+
runAt,
|
|
1145
|
+
timeoutMs ?? null,
|
|
1146
|
+
forceKillOnTimeout ?? false,
|
|
1147
|
+
tags ?? null,
|
|
1148
|
+
idempotencyKey ?? null,
|
|
1149
|
+
retryDelay ?? null,
|
|
1150
|
+
retryBackoff ?? null,
|
|
1151
|
+
retryDelayMax ?? null
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const onConflict = hasAnyIdempotencyKey ? `ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING` : "";
|
|
1155
|
+
const result = await client.query(
|
|
1156
|
+
`INSERT INTO job_queue
|
|
1157
|
+
(job_type, payload, max_attempts, priority, run_at, timeout_ms, force_kill_on_timeout, tags, idempotency_key, retry_delay, retry_backoff, retry_delay_max)
|
|
1158
|
+
VALUES ${valueClauses.join(", ")}
|
|
1159
|
+
${onConflict}
|
|
1160
|
+
RETURNING id, idempotency_key`,
|
|
1161
|
+
params
|
|
1162
|
+
);
|
|
1163
|
+
const returnedKeyToId = /* @__PURE__ */ new Map();
|
|
1164
|
+
const returnedNullKeyIds = [];
|
|
1165
|
+
for (const row of result.rows) {
|
|
1166
|
+
if (row.idempotency_key != null) {
|
|
1167
|
+
returnedKeyToId.set(row.idempotency_key, row.id);
|
|
1168
|
+
} else {
|
|
1169
|
+
returnedNullKeyIds.push(row.id);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const missingKeys = [];
|
|
1173
|
+
for (const job of jobs) {
|
|
1174
|
+
if (job.idempotencyKey && !returnedKeyToId.has(job.idempotencyKey)) {
|
|
1175
|
+
missingKeys.push(job.idempotencyKey);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (missingKeys.length > 0) {
|
|
1179
|
+
const existing = await client.query(
|
|
1180
|
+
`SELECT id, idempotency_key FROM job_queue WHERE idempotency_key = ANY($1)`,
|
|
1181
|
+
[missingKeys]
|
|
1182
|
+
);
|
|
1183
|
+
for (const row of existing.rows) {
|
|
1184
|
+
returnedKeyToId.set(row.idempotency_key, row.id);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
let nullKeyIdx = 0;
|
|
1188
|
+
const ids = [];
|
|
1189
|
+
for (const job of jobs) {
|
|
1190
|
+
if (job.idempotencyKey) {
|
|
1191
|
+
const id = returnedKeyToId.get(job.idempotencyKey);
|
|
1192
|
+
if (id === void 0) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`Failed to resolve job ID for idempotency key "${job.idempotencyKey}"`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
ids.push(id);
|
|
1198
|
+
} else {
|
|
1199
|
+
ids.push(returnedNullKeyIds[nullKeyIdx++]);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
log(`Batch-inserted ${jobs.length} jobs, IDs: [${ids.join(", ")}]`);
|
|
1203
|
+
const newJobEvents = [];
|
|
1204
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
1205
|
+
const job = jobs[i];
|
|
1206
|
+
const wasInserted = !job.idempotencyKey || !missingKeys.includes(job.idempotencyKey);
|
|
1207
|
+
if (wasInserted) {
|
|
1208
|
+
newJobEvents.push({
|
|
1209
|
+
jobId: ids[i],
|
|
1210
|
+
eventType: "added" /* Added */,
|
|
1211
|
+
metadata: {
|
|
1212
|
+
jobType: job.jobType,
|
|
1213
|
+
payload: job.payload,
|
|
1214
|
+
tags: job.tags,
|
|
1215
|
+
idempotencyKey: job.idempotencyKey
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (newJobEvents.length > 0) {
|
|
1221
|
+
if (externalClient) {
|
|
1222
|
+
const evtValues = [];
|
|
1223
|
+
const evtParams = [];
|
|
1224
|
+
let evtIdx = 1;
|
|
1225
|
+
for (const evt of newJobEvents) {
|
|
1226
|
+
evtValues.push(`($${evtIdx++}, $${evtIdx++}, $${evtIdx++})`);
|
|
1227
|
+
evtParams.push(
|
|
1228
|
+
evt.jobId,
|
|
1229
|
+
evt.eventType,
|
|
1230
|
+
evt.metadata ? JSON.stringify(evt.metadata) : null
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
await client.query(
|
|
1235
|
+
`INSERT INTO job_events (job_id, event_type, metadata) VALUES ${evtValues.join(", ")}`,
|
|
1236
|
+
evtParams
|
|
1237
|
+
);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
log(`Error recording batch job events: ${error}`);
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
await this.recordJobEventsBatch(newJobEvents);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return ids;
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
log(`Error batch-inserting jobs: ${error}`);
|
|
1248
|
+
throw error;
|
|
1249
|
+
} finally {
|
|
1250
|
+
if (!externalClient) client.release();
|
|
887
1251
|
}
|
|
888
1252
|
}
|
|
889
1253
|
async getJob(id) {
|
|
890
1254
|
const client = await this.pool.connect();
|
|
891
1255
|
try {
|
|
892
1256
|
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`,
|
|
1257
|
+
`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", output FROM job_queue WHERE id = $1`,
|
|
894
1258
|
[id]
|
|
895
1259
|
);
|
|
896
1260
|
if (result.rows.length === 0) {
|
|
@@ -917,7 +1281,7 @@ var PostgresBackend = class {
|
|
|
917
1281
|
const client = await this.pool.connect();
|
|
918
1282
|
try {
|
|
919
1283
|
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`,
|
|
1284
|
+
`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", output FROM job_queue WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
|
|
921
1285
|
[status, limit, offset]
|
|
922
1286
|
);
|
|
923
1287
|
log(`Found ${result.rows.length} jobs by status ${status}`);
|
|
@@ -939,7 +1303,7 @@ var PostgresBackend = class {
|
|
|
939
1303
|
const client = await this.pool.connect();
|
|
940
1304
|
try {
|
|
941
1305
|
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`,
|
|
1306
|
+
`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", output FROM job_queue ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
943
1307
|
[limit, offset]
|
|
944
1308
|
);
|
|
945
1309
|
log(`Found ${result.rows.length} jobs (all)`);
|
|
@@ -959,7 +1323,7 @@ var PostgresBackend = class {
|
|
|
959
1323
|
async getJobs(filters, limit = 100, offset = 0) {
|
|
960
1324
|
const client = await this.pool.connect();
|
|
961
1325
|
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`;
|
|
1326
|
+
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", output FROM job_queue`;
|
|
963
1327
|
const params = [];
|
|
964
1328
|
const where = [];
|
|
965
1329
|
let paramIdx = 1;
|
|
@@ -1060,7 +1424,7 @@ var PostgresBackend = class {
|
|
|
1060
1424
|
async getJobsByTags(tags, mode = "all", limit = 100, offset = 0) {
|
|
1061
1425
|
const client = await this.pool.connect();
|
|
1062
1426
|
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
|
|
1427
|
+
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", output
|
|
1064
1428
|
FROM job_queue`;
|
|
1065
1429
|
let params = [];
|
|
1066
1430
|
switch (mode) {
|
|
@@ -1154,7 +1518,7 @@ var PostgresBackend = class {
|
|
|
1154
1518
|
LIMIT $2
|
|
1155
1519
|
FOR UPDATE SKIP LOCKED
|
|
1156
1520
|
)
|
|
1157
|
-
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
|
|
1521
|
+
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", output
|
|
1158
1522
|
`,
|
|
1159
1523
|
params
|
|
1160
1524
|
);
|
|
@@ -1182,17 +1546,19 @@ var PostgresBackend = class {
|
|
|
1182
1546
|
client.release();
|
|
1183
1547
|
}
|
|
1184
1548
|
}
|
|
1185
|
-
async completeJob(jobId) {
|
|
1549
|
+
async completeJob(jobId, output) {
|
|
1186
1550
|
const client = await this.pool.connect();
|
|
1187
1551
|
try {
|
|
1552
|
+
const outputJson = output !== void 0 ? JSON.stringify(output) : null;
|
|
1188
1553
|
const result = await client.query(
|
|
1189
1554
|
`
|
|
1190
1555
|
UPDATE job_queue
|
|
1191
1556
|
SET status = 'completed', updated_at = NOW(), completed_at = NOW(),
|
|
1192
|
-
step_data = NULL, wait_until = NULL, wait_token_id = NULL
|
|
1557
|
+
step_data = NULL, wait_until = NULL, wait_token_id = NULL,
|
|
1558
|
+
output = COALESCE($2::jsonb, output)
|
|
1193
1559
|
WHERE id = $1 AND status = 'processing'
|
|
1194
1560
|
`,
|
|
1195
|
-
[jobId]
|
|
1561
|
+
[jobId, outputJson]
|
|
1196
1562
|
);
|
|
1197
1563
|
if (result.rowCount === 0) {
|
|
1198
1564
|
log(
|
|
@@ -1216,9 +1582,17 @@ var PostgresBackend = class {
|
|
|
1216
1582
|
UPDATE job_queue
|
|
1217
1583
|
SET status = 'failed',
|
|
1218
1584
|
updated_at = NOW(),
|
|
1219
|
-
next_attempt_at = CASE
|
|
1220
|
-
WHEN attempts
|
|
1221
|
-
|
|
1585
|
+
next_attempt_at = CASE
|
|
1586
|
+
WHEN attempts >= max_attempts THEN NULL
|
|
1587
|
+
WHEN retry_delay IS NULL AND retry_backoff IS NULL AND retry_delay_max IS NULL
|
|
1588
|
+
THEN NOW() + (POWER(2, attempts) * INTERVAL '1 minute')
|
|
1589
|
+
WHEN COALESCE(retry_backoff, true) = true
|
|
1590
|
+
THEN NOW() + (LEAST(
|
|
1591
|
+
COALESCE(retry_delay_max, 2147483647),
|
|
1592
|
+
COALESCE(retry_delay, 60) * POWER(2, attempts)
|
|
1593
|
+
) * (0.5 + 0.5 * random()) * INTERVAL '1 second')
|
|
1594
|
+
ELSE
|
|
1595
|
+
NOW() + (COALESCE(retry_delay, 60) * INTERVAL '1 second')
|
|
1222
1596
|
END,
|
|
1223
1597
|
error_history = COALESCE(error_history, '[]'::jsonb) || $2::jsonb,
|
|
1224
1598
|
failure_reason = $3,
|
|
@@ -1287,6 +1661,21 @@ var PostgresBackend = class {
|
|
|
1287
1661
|
client.release();
|
|
1288
1662
|
}
|
|
1289
1663
|
}
|
|
1664
|
+
// ── Output ────────────────────────────────────────────────────────────
|
|
1665
|
+
async updateOutput(jobId, output) {
|
|
1666
|
+
const client = await this.pool.connect();
|
|
1667
|
+
try {
|
|
1668
|
+
await client.query(
|
|
1669
|
+
`UPDATE job_queue SET output = $2::jsonb, updated_at = NOW() WHERE id = $1`,
|
|
1670
|
+
[jobId, JSON.stringify(output)]
|
|
1671
|
+
);
|
|
1672
|
+
log(`Updated output for job ${jobId}`);
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
log(`Error updating output for job ${jobId}: ${error}`);
|
|
1675
|
+
} finally {
|
|
1676
|
+
client.release();
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1290
1679
|
// ── Job management ────────────────────────────────────────────────────
|
|
1291
1680
|
async retryJob(jobId) {
|
|
1292
1681
|
const client = await this.pool.connect();
|
|
@@ -1456,6 +1845,18 @@ var PostgresBackend = class {
|
|
|
1456
1845
|
updateFields.push(`tags = $${paramIdx++}`);
|
|
1457
1846
|
params.push(updates.tags ?? null);
|
|
1458
1847
|
}
|
|
1848
|
+
if (updates.retryDelay !== void 0) {
|
|
1849
|
+
updateFields.push(`retry_delay = $${paramIdx++}`);
|
|
1850
|
+
params.push(updates.retryDelay ?? null);
|
|
1851
|
+
}
|
|
1852
|
+
if (updates.retryBackoff !== void 0) {
|
|
1853
|
+
updateFields.push(`retry_backoff = $${paramIdx++}`);
|
|
1854
|
+
params.push(updates.retryBackoff ?? null);
|
|
1855
|
+
}
|
|
1856
|
+
if (updates.retryDelayMax !== void 0) {
|
|
1857
|
+
updateFields.push(`retry_delay_max = $${paramIdx++}`);
|
|
1858
|
+
params.push(updates.retryDelayMax ?? null);
|
|
1859
|
+
}
|
|
1459
1860
|
if (updateFields.length === 0) {
|
|
1460
1861
|
log(`No fields to update for job ${jobId}`);
|
|
1461
1862
|
return;
|
|
@@ -1477,6 +1878,12 @@ var PostgresBackend = class {
|
|
|
1477
1878
|
if (updates.timeoutMs !== void 0)
|
|
1478
1879
|
metadata.timeoutMs = updates.timeoutMs;
|
|
1479
1880
|
if (updates.tags !== void 0) metadata.tags = updates.tags;
|
|
1881
|
+
if (updates.retryDelay !== void 0)
|
|
1882
|
+
metadata.retryDelay = updates.retryDelay;
|
|
1883
|
+
if (updates.retryBackoff !== void 0)
|
|
1884
|
+
metadata.retryBackoff = updates.retryBackoff;
|
|
1885
|
+
if (updates.retryDelayMax !== void 0)
|
|
1886
|
+
metadata.retryDelayMax = updates.retryDelayMax;
|
|
1480
1887
|
await this.recordJobEvent(jobId, "edited" /* Edited */, metadata);
|
|
1481
1888
|
log(`Edited job ${jobId}: ${JSON.stringify(metadata)}`);
|
|
1482
1889
|
} catch (error) {
|
|
@@ -1520,6 +1927,18 @@ var PostgresBackend = class {
|
|
|
1520
1927
|
updateFields.push(`tags = $${paramIdx++}`);
|
|
1521
1928
|
params.push(updates.tags ?? null);
|
|
1522
1929
|
}
|
|
1930
|
+
if (updates.retryDelay !== void 0) {
|
|
1931
|
+
updateFields.push(`retry_delay = $${paramIdx++}`);
|
|
1932
|
+
params.push(updates.retryDelay ?? null);
|
|
1933
|
+
}
|
|
1934
|
+
if (updates.retryBackoff !== void 0) {
|
|
1935
|
+
updateFields.push(`retry_backoff = $${paramIdx++}`);
|
|
1936
|
+
params.push(updates.retryBackoff ?? null);
|
|
1937
|
+
}
|
|
1938
|
+
if (updates.retryDelayMax !== void 0) {
|
|
1939
|
+
updateFields.push(`retry_delay_max = $${paramIdx++}`);
|
|
1940
|
+
params.push(updates.retryDelayMax ?? null);
|
|
1941
|
+
}
|
|
1523
1942
|
if (updateFields.length === 0) {
|
|
1524
1943
|
log(`No fields to update for batch edit`);
|
|
1525
1944
|
return 0;
|
|
@@ -1761,8 +2180,8 @@ var PostgresBackend = class {
|
|
|
1761
2180
|
`INSERT INTO cron_schedules
|
|
1762
2181
|
(schedule_name, cron_expression, job_type, payload, max_attempts,
|
|
1763
2182
|
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)
|
|
2183
|
+
allow_overlap, next_run_at, retry_delay, retry_backoff, retry_delay_max)
|
|
2184
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
1766
2185
|
RETURNING id`,
|
|
1767
2186
|
[
|
|
1768
2187
|
input.scheduleName,
|
|
@@ -1776,7 +2195,10 @@ var PostgresBackend = class {
|
|
|
1776
2195
|
input.tags ?? null,
|
|
1777
2196
|
input.timezone,
|
|
1778
2197
|
input.allowOverlap,
|
|
1779
|
-
input.nextRunAt
|
|
2198
|
+
input.nextRunAt,
|
|
2199
|
+
input.retryDelay,
|
|
2200
|
+
input.retryBackoff,
|
|
2201
|
+
input.retryDelayMax
|
|
1780
2202
|
]
|
|
1781
2203
|
);
|
|
1782
2204
|
const id = result.rows[0].id;
|
|
@@ -1806,7 +2228,9 @@ var PostgresBackend = class {
|
|
|
1806
2228
|
timezone, allow_overlap AS "allowOverlap", status,
|
|
1807
2229
|
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1808
2230
|
next_run_at AS "nextRunAt",
|
|
1809
|
-
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
2231
|
+
created_at AS "createdAt", updated_at AS "updatedAt",
|
|
2232
|
+
retry_delay AS "retryDelay", retry_backoff AS "retryBackoff",
|
|
2233
|
+
retry_delay_max AS "retryDelayMax"
|
|
1810
2234
|
FROM cron_schedules WHERE id = $1`,
|
|
1811
2235
|
[id]
|
|
1812
2236
|
);
|
|
@@ -1831,7 +2255,9 @@ var PostgresBackend = class {
|
|
|
1831
2255
|
timezone, allow_overlap AS "allowOverlap", status,
|
|
1832
2256
|
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1833
2257
|
next_run_at AS "nextRunAt",
|
|
1834
|
-
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
2258
|
+
created_at AS "createdAt", updated_at AS "updatedAt",
|
|
2259
|
+
retry_delay AS "retryDelay", retry_backoff AS "retryBackoff",
|
|
2260
|
+
retry_delay_max AS "retryDelayMax"
|
|
1835
2261
|
FROM cron_schedules WHERE schedule_name = $1`,
|
|
1836
2262
|
[name]
|
|
1837
2263
|
);
|
|
@@ -1855,7 +2281,9 @@ var PostgresBackend = class {
|
|
|
1855
2281
|
timezone, allow_overlap AS "allowOverlap", status,
|
|
1856
2282
|
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1857
2283
|
next_run_at AS "nextRunAt",
|
|
1858
|
-
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
2284
|
+
created_at AS "createdAt", updated_at AS "updatedAt",
|
|
2285
|
+
retry_delay AS "retryDelay", retry_backoff AS "retryBackoff",
|
|
2286
|
+
retry_delay_max AS "retryDelayMax"
|
|
1859
2287
|
FROM cron_schedules`;
|
|
1860
2288
|
const params = [];
|
|
1861
2289
|
if (status) {
|
|
@@ -1960,6 +2388,18 @@ var PostgresBackend = class {
|
|
|
1960
2388
|
updateFields.push(`allow_overlap = $${paramIdx++}`);
|
|
1961
2389
|
params.push(updates.allowOverlap);
|
|
1962
2390
|
}
|
|
2391
|
+
if (updates.retryDelay !== void 0) {
|
|
2392
|
+
updateFields.push(`retry_delay = $${paramIdx++}`);
|
|
2393
|
+
params.push(updates.retryDelay);
|
|
2394
|
+
}
|
|
2395
|
+
if (updates.retryBackoff !== void 0) {
|
|
2396
|
+
updateFields.push(`retry_backoff = $${paramIdx++}`);
|
|
2397
|
+
params.push(updates.retryBackoff);
|
|
2398
|
+
}
|
|
2399
|
+
if (updates.retryDelayMax !== void 0) {
|
|
2400
|
+
updateFields.push(`retry_delay_max = $${paramIdx++}`);
|
|
2401
|
+
params.push(updates.retryDelayMax);
|
|
2402
|
+
}
|
|
1963
2403
|
if (nextRunAt !== void 0) {
|
|
1964
2404
|
updateFields.push(`next_run_at = $${paramIdx++}`);
|
|
1965
2405
|
params.push(nextRunAt);
|
|
@@ -1995,7 +2435,9 @@ var PostgresBackend = class {
|
|
|
1995
2435
|
timezone, allow_overlap AS "allowOverlap", status,
|
|
1996
2436
|
last_enqueued_at AS "lastEnqueuedAt", last_job_id AS "lastJobId",
|
|
1997
2437
|
next_run_at AS "nextRunAt",
|
|
1998
|
-
created_at AS "createdAt", updated_at AS "updatedAt"
|
|
2438
|
+
created_at AS "createdAt", updated_at AS "updatedAt",
|
|
2439
|
+
retry_delay AS "retryDelay", retry_backoff AS "retryBackoff",
|
|
2440
|
+
retry_delay_max AS "retryDelayMax"
|
|
1999
2441
|
FROM cron_schedules
|
|
2000
2442
|
WHERE status = 'active'
|
|
2001
2443
|
AND next_run_at IS NOT NULL
|
|
@@ -2279,6 +2721,9 @@ local forceKillOnTimeout = ARGV[7]
|
|
|
2279
2721
|
local tagsJson = ARGV[8] -- "null" or JSON array string
|
|
2280
2722
|
local idempotencyKey = ARGV[9] -- "null" string if not set
|
|
2281
2723
|
local nowMs = tonumber(ARGV[10])
|
|
2724
|
+
local retryDelay = ARGV[11] -- "null" or seconds string
|
|
2725
|
+
local retryBackoff = ARGV[12] -- "null" or "true"/"false"
|
|
2726
|
+
local retryDelayMax = ARGV[13] -- "null" or seconds string
|
|
2282
2727
|
|
|
2283
2728
|
-- Idempotency check
|
|
2284
2729
|
if idempotencyKey ~= "null" then
|
|
@@ -2322,7 +2767,10 @@ redis.call('HMSET', jobKey,
|
|
|
2322
2767
|
'idempotencyKey', idempotencyKey,
|
|
2323
2768
|
'waitUntil', 'null',
|
|
2324
2769
|
'waitTokenId', 'null',
|
|
2325
|
-
'stepData', 'null'
|
|
2770
|
+
'stepData', 'null',
|
|
2771
|
+
'retryDelay', retryDelay,
|
|
2772
|
+
'retryBackoff', retryBackoff,
|
|
2773
|
+
'retryDelayMax', retryDelayMax
|
|
2326
2774
|
)
|
|
2327
2775
|
|
|
2328
2776
|
-- Status index
|
|
@@ -2363,6 +2811,118 @@ end
|
|
|
2363
2811
|
|
|
2364
2812
|
return id
|
|
2365
2813
|
`;
|
|
2814
|
+
var ADD_JOBS_SCRIPT = `
|
|
2815
|
+
local prefix = KEYS[1]
|
|
2816
|
+
local jobsJson = ARGV[1]
|
|
2817
|
+
local nowMs = tonumber(ARGV[2])
|
|
2818
|
+
|
|
2819
|
+
local jobs = cjson.decode(jobsJson)
|
|
2820
|
+
local results = {}
|
|
2821
|
+
|
|
2822
|
+
for i, job in ipairs(jobs) do
|
|
2823
|
+
local jobType = job.jobType
|
|
2824
|
+
local payloadJson = job.payload
|
|
2825
|
+
local maxAttempts = tonumber(job.maxAttempts)
|
|
2826
|
+
local priority = tonumber(job.priority)
|
|
2827
|
+
local runAtMs = tostring(job.runAtMs)
|
|
2828
|
+
local timeoutMs = tostring(job.timeoutMs)
|
|
2829
|
+
local forceKillOnTimeout = tostring(job.forceKillOnTimeout)
|
|
2830
|
+
local tagsJson = tostring(job.tags)
|
|
2831
|
+
local idempotencyKey = tostring(job.idempotencyKey)
|
|
2832
|
+
local retryDelay = tostring(job.retryDelay)
|
|
2833
|
+
local retryBackoff = tostring(job.retryBackoff)
|
|
2834
|
+
local retryDelayMax = tostring(job.retryDelayMax)
|
|
2835
|
+
|
|
2836
|
+
-- Idempotency check
|
|
2837
|
+
local skip = false
|
|
2838
|
+
if idempotencyKey ~= "null" then
|
|
2839
|
+
local existing = redis.call('GET', prefix .. 'idempotency:' .. idempotencyKey)
|
|
2840
|
+
if existing then
|
|
2841
|
+
results[i] = tonumber(existing)
|
|
2842
|
+
skip = true
|
|
2843
|
+
end
|
|
2844
|
+
end
|
|
2845
|
+
|
|
2846
|
+
if not skip then
|
|
2847
|
+
-- Generate ID
|
|
2848
|
+
local id = redis.call('INCR', prefix .. 'id_seq')
|
|
2849
|
+
local jobKey = prefix .. 'job:' .. id
|
|
2850
|
+
local runAt = runAtMs ~= "0" and tonumber(runAtMs) or nowMs
|
|
2851
|
+
|
|
2852
|
+
-- Store the job hash
|
|
2853
|
+
redis.call('HMSET', jobKey,
|
|
2854
|
+
'id', id,
|
|
2855
|
+
'jobType', jobType,
|
|
2856
|
+
'payload', payloadJson,
|
|
2857
|
+
'status', 'pending',
|
|
2858
|
+
'maxAttempts', maxAttempts,
|
|
2859
|
+
'attempts', 0,
|
|
2860
|
+
'priority', priority,
|
|
2861
|
+
'runAt', runAt,
|
|
2862
|
+
'timeoutMs', timeoutMs,
|
|
2863
|
+
'forceKillOnTimeout', forceKillOnTimeout,
|
|
2864
|
+
'createdAt', nowMs,
|
|
2865
|
+
'updatedAt', nowMs,
|
|
2866
|
+
'lockedAt', 'null',
|
|
2867
|
+
'lockedBy', 'null',
|
|
2868
|
+
'nextAttemptAt', 'null',
|
|
2869
|
+
'pendingReason', 'null',
|
|
2870
|
+
'errorHistory', '[]',
|
|
2871
|
+
'failureReason', 'null',
|
|
2872
|
+
'completedAt', 'null',
|
|
2873
|
+
'startedAt', 'null',
|
|
2874
|
+
'lastRetriedAt', 'null',
|
|
2875
|
+
'lastFailedAt', 'null',
|
|
2876
|
+
'lastCancelledAt', 'null',
|
|
2877
|
+
'tags', tagsJson,
|
|
2878
|
+
'idempotencyKey', idempotencyKey,
|
|
2879
|
+
'waitUntil', 'null',
|
|
2880
|
+
'waitTokenId', 'null',
|
|
2881
|
+
'stepData', 'null',
|
|
2882
|
+
'retryDelay', retryDelay,
|
|
2883
|
+
'retryBackoff', retryBackoff,
|
|
2884
|
+
'retryDelayMax', retryDelayMax
|
|
2885
|
+
)
|
|
2886
|
+
|
|
2887
|
+
-- Status index
|
|
2888
|
+
redis.call('SADD', prefix .. 'status:pending', id)
|
|
2889
|
+
|
|
2890
|
+
-- Type index
|
|
2891
|
+
redis.call('SADD', prefix .. 'type:' .. jobType, id)
|
|
2892
|
+
|
|
2893
|
+
-- Tag indexes
|
|
2894
|
+
if tagsJson ~= "null" then
|
|
2895
|
+
local tags = cjson.decode(tagsJson)
|
|
2896
|
+
for _, tag in ipairs(tags) do
|
|
2897
|
+
redis.call('SADD', prefix .. 'tag:' .. tag, id)
|
|
2898
|
+
end
|
|
2899
|
+
for _, tag in ipairs(tags) do
|
|
2900
|
+
redis.call('SADD', prefix .. 'job:' .. id .. ':tags', tag)
|
|
2901
|
+
end
|
|
2902
|
+
end
|
|
2903
|
+
|
|
2904
|
+
-- Idempotency mapping
|
|
2905
|
+
if idempotencyKey ~= "null" then
|
|
2906
|
+
redis.call('SET', prefix .. 'idempotency:' .. idempotencyKey, id)
|
|
2907
|
+
end
|
|
2908
|
+
|
|
2909
|
+
-- All-jobs sorted set
|
|
2910
|
+
redis.call('ZADD', prefix .. 'all', nowMs, id)
|
|
2911
|
+
|
|
2912
|
+
-- Queue or delayed
|
|
2913
|
+
if runAt <= nowMs then
|
|
2914
|
+
local score = priority * ${SCORE_RANGE} + (${SCORE_RANGE} - nowMs)
|
|
2915
|
+
redis.call('ZADD', prefix .. 'queue', score, id)
|
|
2916
|
+
else
|
|
2917
|
+
redis.call('ZADD', prefix .. 'delayed', runAt, id)
|
|
2918
|
+
end
|
|
2919
|
+
|
|
2920
|
+
results[i] = id
|
|
2921
|
+
end
|
|
2922
|
+
end
|
|
2923
|
+
|
|
2924
|
+
return results
|
|
2925
|
+
`;
|
|
2366
2926
|
var GET_NEXT_BATCH_SCRIPT = `
|
|
2367
2927
|
local prefix = KEYS[1]
|
|
2368
2928
|
local workerId = ARGV[1]
|
|
@@ -2515,16 +3075,24 @@ var COMPLETE_JOB_SCRIPT = `
|
|
|
2515
3075
|
local prefix = KEYS[1]
|
|
2516
3076
|
local jobId = ARGV[1]
|
|
2517
3077
|
local nowMs = ARGV[2]
|
|
3078
|
+
local outputJson = ARGV[3]
|
|
2518
3079
|
local jk = prefix .. 'job:' .. jobId
|
|
2519
3080
|
|
|
2520
|
-
|
|
3081
|
+
local fields = {
|
|
2521
3082
|
'status', 'completed',
|
|
2522
3083
|
'updatedAt', nowMs,
|
|
2523
3084
|
'completedAt', nowMs,
|
|
2524
3085
|
'stepData', 'null',
|
|
2525
3086
|
'waitUntil', 'null',
|
|
2526
3087
|
'waitTokenId', 'null'
|
|
2527
|
-
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
if outputJson ~= '__NONE__' then
|
|
3091
|
+
fields[#fields + 1] = 'output'
|
|
3092
|
+
fields[#fields + 1] = outputJson
|
|
3093
|
+
end
|
|
3094
|
+
|
|
3095
|
+
redis.call('HMSET', jk, unpack(fields))
|
|
2528
3096
|
redis.call('SREM', prefix .. 'status:processing', jobId)
|
|
2529
3097
|
redis.call('SADD', prefix .. 'status:completed', jobId)
|
|
2530
3098
|
|
|
@@ -2541,11 +3109,38 @@ local jk = prefix .. 'job:' .. jobId
|
|
|
2541
3109
|
local attempts = tonumber(redis.call('HGET', jk, 'attempts'))
|
|
2542
3110
|
local maxAttempts = tonumber(redis.call('HGET', jk, 'maxAttempts'))
|
|
2543
3111
|
|
|
2544
|
-
--
|
|
3112
|
+
-- Read per-job retry config (may be "null")
|
|
3113
|
+
local rdRaw = redis.call('HGET', jk, 'retryDelay')
|
|
3114
|
+
local rbRaw = redis.call('HGET', jk, 'retryBackoff')
|
|
3115
|
+
local rmRaw = redis.call('HGET', jk, 'retryDelayMax')
|
|
3116
|
+
|
|
2545
3117
|
local nextAttemptAt = 'null'
|
|
2546
3118
|
if attempts < maxAttempts then
|
|
2547
|
-
local
|
|
2548
|
-
|
|
3119
|
+
local allNull = (rdRaw == 'null' or rdRaw == false)
|
|
3120
|
+
and (rbRaw == 'null' or rbRaw == false)
|
|
3121
|
+
and (rmRaw == 'null' or rmRaw == false)
|
|
3122
|
+
if allNull then
|
|
3123
|
+
-- Legacy formula: 2^attempts minutes
|
|
3124
|
+
local delayMs = math.pow(2, attempts) * 60000
|
|
3125
|
+
nextAttemptAt = nowMs + delayMs
|
|
3126
|
+
else
|
|
3127
|
+
local retryDelaySec = 60
|
|
3128
|
+
if rdRaw and rdRaw ~= 'null' then retryDelaySec = tonumber(rdRaw) end
|
|
3129
|
+
local useBackoff = true
|
|
3130
|
+
if rbRaw and rbRaw ~= 'null' then useBackoff = (rbRaw == 'true') end
|
|
3131
|
+
local maxDelaySec = nil
|
|
3132
|
+
if rmRaw and rmRaw ~= 'null' then maxDelaySec = tonumber(rmRaw) end
|
|
3133
|
+
|
|
3134
|
+
local delaySec
|
|
3135
|
+
if useBackoff then
|
|
3136
|
+
delaySec = retryDelaySec * math.pow(2, attempts)
|
|
3137
|
+
if maxDelaySec then delaySec = math.min(delaySec, maxDelaySec) end
|
|
3138
|
+
delaySec = delaySec * (0.5 + 0.5 * math.random())
|
|
3139
|
+
else
|
|
3140
|
+
delaySec = retryDelaySec
|
|
3141
|
+
end
|
|
3142
|
+
nextAttemptAt = nowMs + math.floor(delaySec * 1000)
|
|
3143
|
+
end
|
|
2549
3144
|
end
|
|
2550
3145
|
|
|
2551
3146
|
-- Append to error_history
|
|
@@ -2957,9 +3552,21 @@ function deserializeJob(h) {
|
|
|
2957
3552
|
progress: numOrNull(h.progress),
|
|
2958
3553
|
waitUntil: dateOrNull(h.waitUntil),
|
|
2959
3554
|
waitTokenId: nullish(h.waitTokenId),
|
|
2960
|
-
stepData: parseStepData(h.stepData)
|
|
3555
|
+
stepData: parseStepData(h.stepData),
|
|
3556
|
+
retryDelay: numOrNull(h.retryDelay),
|
|
3557
|
+
retryBackoff: h.retryBackoff === "true" ? true : h.retryBackoff === "false" ? false : null,
|
|
3558
|
+
retryDelayMax: numOrNull(h.retryDelayMax),
|
|
3559
|
+
output: parseJsonField(h.output)
|
|
2961
3560
|
};
|
|
2962
3561
|
}
|
|
3562
|
+
function parseJsonField(raw) {
|
|
3563
|
+
if (!raw || raw === "null") return null;
|
|
3564
|
+
try {
|
|
3565
|
+
return JSON.parse(raw);
|
|
3566
|
+
} catch {
|
|
3567
|
+
return null;
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
2963
3570
|
function parseStepData(raw) {
|
|
2964
3571
|
if (!raw || raw === "null") return void 0;
|
|
2965
3572
|
try {
|
|
@@ -2969,7 +3576,23 @@ function parseStepData(raw) {
|
|
|
2969
3576
|
}
|
|
2970
3577
|
}
|
|
2971
3578
|
var RedisBackend = class {
|
|
2972
|
-
|
|
3579
|
+
/**
|
|
3580
|
+
* Create a RedisBackend.
|
|
3581
|
+
*
|
|
3582
|
+
* @param configOrClient - Either `redisConfig` from the config file (the
|
|
3583
|
+
* library creates a new ioredis client) or an existing ioredis client
|
|
3584
|
+
* instance (bring your own).
|
|
3585
|
+
* @param keyPrefix - Key prefix, only used when `configOrClient` is an
|
|
3586
|
+
* external client. Ignored when `redisConfig` is passed (uses
|
|
3587
|
+
* `redisConfig.keyPrefix` instead). Default: `'dq:'`.
|
|
3588
|
+
*/
|
|
3589
|
+
constructor(configOrClient, keyPrefix) {
|
|
3590
|
+
if (configOrClient && typeof configOrClient.eval === "function") {
|
|
3591
|
+
this.client = configOrClient;
|
|
3592
|
+
this.prefix = keyPrefix ?? "dq:";
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
const redisConfig = configOrClient;
|
|
2973
3596
|
let IORedis;
|
|
2974
3597
|
try {
|
|
2975
3598
|
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 +3665,16 @@ var RedisBackend = class {
|
|
|
3042
3665
|
timeoutMs = void 0,
|
|
3043
3666
|
forceKillOnTimeout = false,
|
|
3044
3667
|
tags = void 0,
|
|
3045
|
-
idempotencyKey = void 0
|
|
3046
|
-
|
|
3668
|
+
idempotencyKey = void 0,
|
|
3669
|
+
retryDelay = void 0,
|
|
3670
|
+
retryBackoff = void 0,
|
|
3671
|
+
retryDelayMax = void 0
|
|
3672
|
+
}, options) {
|
|
3673
|
+
if (options?.db) {
|
|
3674
|
+
throw new Error(
|
|
3675
|
+
"The db option is not supported with the Redis backend. Transactional job creation is only available with PostgreSQL."
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3047
3678
|
const now = this.nowMs();
|
|
3048
3679
|
const runAtMs = runAt ? runAt.getTime() : 0;
|
|
3049
3680
|
const result = await this.client.eval(
|
|
@@ -3059,7 +3690,10 @@ var RedisBackend = class {
|
|
|
3059
3690
|
forceKillOnTimeout ? "true" : "false",
|
|
3060
3691
|
tags ? JSON.stringify(tags) : "null",
|
|
3061
3692
|
idempotencyKey ?? "null",
|
|
3062
|
-
now
|
|
3693
|
+
now,
|
|
3694
|
+
retryDelay !== void 0 ? retryDelay.toString() : "null",
|
|
3695
|
+
retryBackoff !== void 0 ? retryBackoff.toString() : "null",
|
|
3696
|
+
retryDelayMax !== void 0 ? retryDelayMax.toString() : "null"
|
|
3063
3697
|
);
|
|
3064
3698
|
const jobId = Number(result);
|
|
3065
3699
|
log(
|
|
@@ -3073,6 +3707,58 @@ var RedisBackend = class {
|
|
|
3073
3707
|
});
|
|
3074
3708
|
return jobId;
|
|
3075
3709
|
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Insert multiple jobs atomically via a single Lua script.
|
|
3712
|
+
* Returns IDs in the same order as the input array.
|
|
3713
|
+
*/
|
|
3714
|
+
async addJobs(jobs, options) {
|
|
3715
|
+
if (jobs.length === 0) return [];
|
|
3716
|
+
if (options?.db) {
|
|
3717
|
+
throw new Error(
|
|
3718
|
+
"The db option is not supported with the Redis backend. Transactional job creation is only available with PostgreSQL."
|
|
3719
|
+
);
|
|
3720
|
+
}
|
|
3721
|
+
const now = this.nowMs();
|
|
3722
|
+
const jobsPayload = jobs.map((job) => ({
|
|
3723
|
+
jobType: job.jobType,
|
|
3724
|
+
payload: JSON.stringify(job.payload),
|
|
3725
|
+
maxAttempts: job.maxAttempts ?? 3,
|
|
3726
|
+
priority: job.priority ?? 0,
|
|
3727
|
+
runAtMs: job.runAt ? job.runAt.getTime() : 0,
|
|
3728
|
+
timeoutMs: job.timeoutMs !== void 0 ? job.timeoutMs.toString() : "null",
|
|
3729
|
+
forceKillOnTimeout: job.forceKillOnTimeout ? "true" : "false",
|
|
3730
|
+
tags: job.tags ? JSON.stringify(job.tags) : "null",
|
|
3731
|
+
idempotencyKey: job.idempotencyKey ?? "null",
|
|
3732
|
+
retryDelay: job.retryDelay !== void 0 ? job.retryDelay.toString() : "null",
|
|
3733
|
+
retryBackoff: job.retryBackoff !== void 0 ? job.retryBackoff.toString() : "null",
|
|
3734
|
+
retryDelayMax: job.retryDelayMax !== void 0 ? job.retryDelayMax.toString() : "null"
|
|
3735
|
+
}));
|
|
3736
|
+
const result = await this.client.eval(
|
|
3737
|
+
ADD_JOBS_SCRIPT,
|
|
3738
|
+
1,
|
|
3739
|
+
this.prefix,
|
|
3740
|
+
JSON.stringify(jobsPayload),
|
|
3741
|
+
now
|
|
3742
|
+
);
|
|
3743
|
+
const ids = result.map(Number);
|
|
3744
|
+
log(`Batch-inserted ${jobs.length} jobs, IDs: [${ids.join(", ")}]`);
|
|
3745
|
+
const existingIdempotencyIds = /* @__PURE__ */ new Set();
|
|
3746
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
3747
|
+
if (jobs[i].idempotencyKey) {
|
|
3748
|
+
if (existingIdempotencyIds.has(ids[i])) {
|
|
3749
|
+
continue;
|
|
3750
|
+
}
|
|
3751
|
+
existingIdempotencyIds.add(ids[i]);
|
|
3752
|
+
}
|
|
3753
|
+
await this.recordJobEvent(ids[i], "added" /* Added */, {
|
|
3754
|
+
jobType: jobs[i].jobType,
|
|
3755
|
+
payload: jobs[i].payload,
|
|
3756
|
+
tags: jobs[i].tags,
|
|
3757
|
+
idempotencyKey: jobs[i].idempotencyKey
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
return ids;
|
|
3761
|
+
}
|
|
3076
3762
|
async getJob(id) {
|
|
3077
3763
|
const data = await this.client.hgetall(`${this.prefix}job:${id}`);
|
|
3078
3764
|
if (!data || Object.keys(data).length === 0) {
|
|
@@ -3178,9 +3864,17 @@ var RedisBackend = class {
|
|
|
3178
3864
|
}
|
|
3179
3865
|
return jobs;
|
|
3180
3866
|
}
|
|
3181
|
-
async completeJob(jobId) {
|
|
3867
|
+
async completeJob(jobId, output) {
|
|
3182
3868
|
const now = this.nowMs();
|
|
3183
|
-
|
|
3869
|
+
const outputArg = output !== void 0 ? JSON.stringify(output) : "__NONE__";
|
|
3870
|
+
await this.client.eval(
|
|
3871
|
+
COMPLETE_JOB_SCRIPT,
|
|
3872
|
+
1,
|
|
3873
|
+
this.prefix,
|
|
3874
|
+
jobId,
|
|
3875
|
+
now,
|
|
3876
|
+
outputArg
|
|
3877
|
+
);
|
|
3184
3878
|
await this.recordJobEvent(jobId, "completed" /* Completed */);
|
|
3185
3879
|
log(`Completed job ${jobId}`);
|
|
3186
3880
|
}
|
|
@@ -3233,6 +3927,22 @@ var RedisBackend = class {
|
|
|
3233
3927
|
log(`Error updating progress for job ${jobId}: ${error}`);
|
|
3234
3928
|
}
|
|
3235
3929
|
}
|
|
3930
|
+
// ── Output ────────────────────────────────────────────────────────────
|
|
3931
|
+
async updateOutput(jobId, output) {
|
|
3932
|
+
try {
|
|
3933
|
+
const now = this.nowMs();
|
|
3934
|
+
await this.client.hset(
|
|
3935
|
+
`${this.prefix}job:${jobId}`,
|
|
3936
|
+
"output",
|
|
3937
|
+
JSON.stringify(output),
|
|
3938
|
+
"updatedAt",
|
|
3939
|
+
now.toString()
|
|
3940
|
+
);
|
|
3941
|
+
log(`Updated output for job ${jobId}`);
|
|
3942
|
+
} catch (error) {
|
|
3943
|
+
log(`Error updating output for job ${jobId}: ${error}`);
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3236
3946
|
// ── Job management ────────────────────────────────────────────────────
|
|
3237
3947
|
async retryJob(jobId) {
|
|
3238
3948
|
const now = this.nowMs();
|
|
@@ -3339,6 +4049,27 @@ var RedisBackend = class {
|
|
|
3339
4049
|
}
|
|
3340
4050
|
metadata.tags = updates.tags;
|
|
3341
4051
|
}
|
|
4052
|
+
if (updates.retryDelay !== void 0) {
|
|
4053
|
+
fields.push(
|
|
4054
|
+
"retryDelay",
|
|
4055
|
+
updates.retryDelay !== null ? updates.retryDelay.toString() : "null"
|
|
4056
|
+
);
|
|
4057
|
+
metadata.retryDelay = updates.retryDelay;
|
|
4058
|
+
}
|
|
4059
|
+
if (updates.retryBackoff !== void 0) {
|
|
4060
|
+
fields.push(
|
|
4061
|
+
"retryBackoff",
|
|
4062
|
+
updates.retryBackoff !== null ? updates.retryBackoff.toString() : "null"
|
|
4063
|
+
);
|
|
4064
|
+
metadata.retryBackoff = updates.retryBackoff;
|
|
4065
|
+
}
|
|
4066
|
+
if (updates.retryDelayMax !== void 0) {
|
|
4067
|
+
fields.push(
|
|
4068
|
+
"retryDelayMax",
|
|
4069
|
+
updates.retryDelayMax !== null ? updates.retryDelayMax.toString() : "null"
|
|
4070
|
+
);
|
|
4071
|
+
metadata.retryDelayMax = updates.retryDelayMax;
|
|
4072
|
+
}
|
|
3342
4073
|
if (fields.length === 0) {
|
|
3343
4074
|
log(`No fields to update for job ${jobId}`);
|
|
3344
4075
|
return;
|
|
@@ -3813,7 +4544,13 @@ var RedisBackend = class {
|
|
|
3813
4544
|
"createdAt",
|
|
3814
4545
|
now.toString(),
|
|
3815
4546
|
"updatedAt",
|
|
3816
|
-
now.toString()
|
|
4547
|
+
now.toString(),
|
|
4548
|
+
"retryDelay",
|
|
4549
|
+
input.retryDelay !== null && input.retryDelay !== void 0 ? input.retryDelay.toString() : "null",
|
|
4550
|
+
"retryBackoff",
|
|
4551
|
+
input.retryBackoff !== null && input.retryBackoff !== void 0 ? input.retryBackoff.toString() : "null",
|
|
4552
|
+
"retryDelayMax",
|
|
4553
|
+
input.retryDelayMax !== null && input.retryDelayMax !== void 0 ? input.retryDelayMax.toString() : "null"
|
|
3817
4554
|
];
|
|
3818
4555
|
await this.client.hmset(key, ...fields);
|
|
3819
4556
|
await this.client.set(
|
|
@@ -3967,6 +4704,24 @@ var RedisBackend = class {
|
|
|
3967
4704
|
if (updates.allowOverlap !== void 0) {
|
|
3968
4705
|
fields.push("allowOverlap", updates.allowOverlap ? "true" : "false");
|
|
3969
4706
|
}
|
|
4707
|
+
if (updates.retryDelay !== void 0) {
|
|
4708
|
+
fields.push(
|
|
4709
|
+
"retryDelay",
|
|
4710
|
+
updates.retryDelay !== null ? updates.retryDelay.toString() : "null"
|
|
4711
|
+
);
|
|
4712
|
+
}
|
|
4713
|
+
if (updates.retryBackoff !== void 0) {
|
|
4714
|
+
fields.push(
|
|
4715
|
+
"retryBackoff",
|
|
4716
|
+
updates.retryBackoff !== null ? updates.retryBackoff.toString() : "null"
|
|
4717
|
+
);
|
|
4718
|
+
}
|
|
4719
|
+
if (updates.retryDelayMax !== void 0) {
|
|
4720
|
+
fields.push(
|
|
4721
|
+
"retryDelayMax",
|
|
4722
|
+
updates.retryDelayMax !== null ? updates.retryDelayMax.toString() : "null"
|
|
4723
|
+
);
|
|
4724
|
+
}
|
|
3970
4725
|
if (nextRunAt !== void 0) {
|
|
3971
4726
|
const val = nextRunAt !== null ? nextRunAt.getTime().toString() : "null";
|
|
3972
4727
|
fields.push("nextRunAt", val);
|
|
@@ -4085,7 +4840,10 @@ var RedisBackend = class {
|
|
|
4085
4840
|
lastJobId: numOrNull(h.lastJobId),
|
|
4086
4841
|
nextRunAt: dateOrNull(h.nextRunAt),
|
|
4087
4842
|
createdAt: new Date(Number(h.createdAt)),
|
|
4088
|
-
updatedAt: new Date(Number(h.updatedAt))
|
|
4843
|
+
updatedAt: new Date(Number(h.updatedAt)),
|
|
4844
|
+
retryDelay: numOrNull(h.retryDelay),
|
|
4845
|
+
retryBackoff: h.retryBackoff === "true" ? true : h.retryBackoff === "false" ? false : null,
|
|
4846
|
+
retryDelayMax: numOrNull(h.retryDelayMax)
|
|
4089
4847
|
};
|
|
4090
4848
|
}
|
|
4091
4849
|
// ── Private helpers (filters) ─────────────────────────────────────────
|
|
@@ -4208,14 +4966,37 @@ var initJobQueue = (config) => {
|
|
|
4208
4966
|
let backend;
|
|
4209
4967
|
if (backendType === "postgres") {
|
|
4210
4968
|
const pgConfig = config;
|
|
4211
|
-
|
|
4212
|
-
|
|
4969
|
+
if (pgConfig.pool) {
|
|
4970
|
+
backend = new PostgresBackend(pgConfig.pool);
|
|
4971
|
+
} else if (pgConfig.databaseConfig) {
|
|
4972
|
+
const pool = createPool(pgConfig.databaseConfig);
|
|
4973
|
+
backend = new PostgresBackend(pool);
|
|
4974
|
+
} else {
|
|
4975
|
+
throw new Error(
|
|
4976
|
+
'PostgreSQL backend requires either "databaseConfig" or "pool" to be provided.'
|
|
4977
|
+
);
|
|
4978
|
+
}
|
|
4213
4979
|
} else if (backendType === "redis") {
|
|
4214
|
-
const redisConfig = config
|
|
4215
|
-
|
|
4980
|
+
const redisConfig = config;
|
|
4981
|
+
if (redisConfig.client) {
|
|
4982
|
+
backend = new RedisBackend(
|
|
4983
|
+
redisConfig.client,
|
|
4984
|
+
redisConfig.keyPrefix
|
|
4985
|
+
);
|
|
4986
|
+
} else if (redisConfig.redisConfig) {
|
|
4987
|
+
backend = new RedisBackend(redisConfig.redisConfig);
|
|
4988
|
+
} else {
|
|
4989
|
+
throw new Error(
|
|
4990
|
+
'Redis backend requires either "redisConfig" or "client" to be provided.'
|
|
4991
|
+
);
|
|
4992
|
+
}
|
|
4216
4993
|
} else {
|
|
4217
4994
|
throw new Error(`Unknown backend: ${backendType}`);
|
|
4218
4995
|
}
|
|
4996
|
+
const emitter = new events.EventEmitter();
|
|
4997
|
+
const emit = (event, data) => {
|
|
4998
|
+
emitter.emit(event, data);
|
|
4999
|
+
};
|
|
4219
5000
|
const enqueueDueCronJobsImpl = async () => {
|
|
4220
5001
|
const dueSchedules = await backend.getDueCronSchedules();
|
|
4221
5002
|
let count = 0;
|
|
@@ -4243,7 +5024,10 @@ var initJobQueue = (config) => {
|
|
|
4243
5024
|
priority: schedule.priority,
|
|
4244
5025
|
timeoutMs: schedule.timeoutMs ?? void 0,
|
|
4245
5026
|
forceKillOnTimeout: schedule.forceKillOnTimeout,
|
|
4246
|
-
tags: schedule.tags
|
|
5027
|
+
tags: schedule.tags,
|
|
5028
|
+
retryDelay: schedule.retryDelay ?? void 0,
|
|
5029
|
+
retryBackoff: schedule.retryBackoff ?? void 0,
|
|
5030
|
+
retryDelayMax: schedule.retryDelayMax ?? void 0
|
|
4247
5031
|
});
|
|
4248
5032
|
const nextRunAt = getNextCronOccurrence(
|
|
4249
5033
|
schedule.cronExpression,
|
|
@@ -4262,7 +5046,21 @@ var initJobQueue = (config) => {
|
|
|
4262
5046
|
return {
|
|
4263
5047
|
// Job queue operations
|
|
4264
5048
|
addJob: withLogContext(
|
|
4265
|
-
(job) =>
|
|
5049
|
+
async (job, options) => {
|
|
5050
|
+
const jobId = await backend.addJob(job, options);
|
|
5051
|
+
emit("job:added", { jobId, jobType: job.jobType });
|
|
5052
|
+
return jobId;
|
|
5053
|
+
},
|
|
5054
|
+
config.verbose ?? false
|
|
5055
|
+
),
|
|
5056
|
+
addJobs: withLogContext(
|
|
5057
|
+
async (jobs, options) => {
|
|
5058
|
+
const jobIds = await backend.addJobs(jobs, options);
|
|
5059
|
+
for (let i = 0; i < jobIds.length; i++) {
|
|
5060
|
+
emit("job:added", { jobId: jobIds[i], jobType: jobs[i].jobType });
|
|
5061
|
+
}
|
|
5062
|
+
return jobIds;
|
|
5063
|
+
},
|
|
4266
5064
|
config.verbose ?? false
|
|
4267
5065
|
),
|
|
4268
5066
|
getJob: withLogContext(
|
|
@@ -4281,13 +5079,16 @@ var initJobQueue = (config) => {
|
|
|
4281
5079
|
(filters, limit, offset) => backend.getJobs(filters, limit, offset),
|
|
4282
5080
|
config.verbose ?? false
|
|
4283
5081
|
),
|
|
4284
|
-
retryJob: (jobId) =>
|
|
5082
|
+
retryJob: async (jobId) => {
|
|
5083
|
+
await backend.retryJob(jobId);
|
|
5084
|
+
emit("job:retried", { jobId });
|
|
5085
|
+
},
|
|
4285
5086
|
cleanupOldJobs: (daysToKeep, batchSize) => backend.cleanupOldJobs(daysToKeep, batchSize),
|
|
4286
5087
|
cleanupOldJobEvents: (daysToKeep, batchSize) => backend.cleanupOldJobEvents(daysToKeep, batchSize),
|
|
4287
|
-
cancelJob: withLogContext(
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
),
|
|
5088
|
+
cancelJob: withLogContext(async (jobId) => {
|
|
5089
|
+
await backend.cancelJob(jobId);
|
|
5090
|
+
emit("job:cancelled", { jobId });
|
|
5091
|
+
}, config.verbose ?? false),
|
|
4291
5092
|
editJob: withLogContext(
|
|
4292
5093
|
(jobId, updates) => backend.editJob(jobId, updates),
|
|
4293
5094
|
config.verbose ?? false
|
|
@@ -4312,9 +5113,17 @@ var initJobQueue = (config) => {
|
|
|
4312
5113
|
config.verbose ?? false
|
|
4313
5114
|
),
|
|
4314
5115
|
// Job processing — automatically enqueues due cron jobs before each batch
|
|
4315
|
-
createProcessor: (handlers, options) => createProcessor(
|
|
4316
|
-
|
|
4317
|
-
|
|
5116
|
+
createProcessor: (handlers, options) => createProcessor(
|
|
5117
|
+
backend,
|
|
5118
|
+
handlers,
|
|
5119
|
+
options,
|
|
5120
|
+
async () => {
|
|
5121
|
+
await enqueueDueCronJobsImpl();
|
|
5122
|
+
},
|
|
5123
|
+
emit
|
|
5124
|
+
),
|
|
5125
|
+
// Background supervisor — automated maintenance
|
|
5126
|
+
createSupervisor: (options) => createSupervisor(backend, options, emit),
|
|
4318
5127
|
// Job events
|
|
4319
5128
|
getJobEvents: withLogContext(
|
|
4320
5129
|
(jobId) => backend.getJobEvents(jobId),
|
|
@@ -4361,7 +5170,10 @@ var initJobQueue = (config) => {
|
|
|
4361
5170
|
tags: options.tags,
|
|
4362
5171
|
timezone: options.timezone ?? "UTC",
|
|
4363
5172
|
allowOverlap: options.allowOverlap ?? false,
|
|
4364
|
-
nextRunAt
|
|
5173
|
+
nextRunAt,
|
|
5174
|
+
retryDelay: options.retryDelay ?? null,
|
|
5175
|
+
retryBackoff: options.retryBackoff ?? null,
|
|
5176
|
+
retryDelayMax: options.retryDelayMax ?? null
|
|
4365
5177
|
};
|
|
4366
5178
|
return backend.addCronSchedule(input);
|
|
4367
5179
|
},
|
|
@@ -4413,6 +5225,23 @@ var initJobQueue = (config) => {
|
|
|
4413
5225
|
() => enqueueDueCronJobsImpl(),
|
|
4414
5226
|
config.verbose ?? false
|
|
4415
5227
|
),
|
|
5228
|
+
// Event hooks
|
|
5229
|
+
on: (event, listener) => {
|
|
5230
|
+
emitter.on(event, listener);
|
|
5231
|
+
},
|
|
5232
|
+
once: (event, listener) => {
|
|
5233
|
+
emitter.once(event, listener);
|
|
5234
|
+
},
|
|
5235
|
+
off: (event, listener) => {
|
|
5236
|
+
emitter.off(event, listener);
|
|
5237
|
+
},
|
|
5238
|
+
removeAllListeners: (event) => {
|
|
5239
|
+
if (event) {
|
|
5240
|
+
emitter.removeAllListeners(event);
|
|
5241
|
+
} else {
|
|
5242
|
+
emitter.removeAllListeners();
|
|
5243
|
+
}
|
|
5244
|
+
},
|
|
4416
5245
|
// Advanced access
|
|
4417
5246
|
getPool: () => {
|
|
4418
5247
|
if (!(backend instanceof PostgresBackend)) {
|