@workglow/job-queue 0.2.23 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +131 -25
- package/dist/browser.js.map +5 -5
- package/dist/bun.js +131 -25
- package/dist/bun.js.map +5 -5
- package/dist/job/JobQueueClient.d.ts +24 -3
- package/dist/job/JobQueueClient.d.ts.map +1 -1
- package/dist/job/JobQueueServer.d.ts +25 -1
- package/dist/job/JobQueueServer.d.ts.map +1 -1
- package/dist/job/JobQueueWorker.d.ts +90 -7
- package/dist/job/JobQueueWorker.d.ts.map +1 -1
- package/dist/node.js +131 -25
- package/dist/node.js.map +5 -5
- package/package.json +5 -5
package/dist/browser.js
CHANGED
|
@@ -303,6 +303,7 @@ class JobQueueClient {
|
|
|
303
303
|
status: JobStatus2.PENDING
|
|
304
304
|
};
|
|
305
305
|
const id = await this.storage.add(job);
|
|
306
|
+
this.server?.handleJobAdded(id);
|
|
306
307
|
return this.createJobHandle(id);
|
|
307
308
|
}
|
|
308
309
|
async submitBatch(inputs, options) {
|
|
@@ -342,29 +343,52 @@ class JobQueueClient {
|
|
|
342
343
|
async waitFor(jobId) {
|
|
343
344
|
if (!jobId)
|
|
344
345
|
throw new JobNotFoundError("Cannot wait for undefined job");
|
|
346
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
347
|
+
promise.catch(() => {});
|
|
348
|
+
const promises = this.activeJobPromises.get(jobId) || [];
|
|
349
|
+
promises.push({ resolve, reject });
|
|
350
|
+
this.activeJobPromises.set(jobId, promises);
|
|
345
351
|
const job = await this.getJob(jobId);
|
|
346
|
-
if (!job)
|
|
352
|
+
if (!job) {
|
|
353
|
+
this.removePromise(jobId, resolve, reject);
|
|
347
354
|
throw new JobNotFoundError(`Job ${jobId} not found`);
|
|
355
|
+
}
|
|
348
356
|
if (job.status === JobStatus2.COMPLETED) {
|
|
357
|
+
this.removePromise(jobId, resolve, reject);
|
|
349
358
|
return job.output;
|
|
350
359
|
}
|
|
351
360
|
if (job.status === JobStatus2.DISABLED) {
|
|
361
|
+
this.removePromise(jobId, resolve, reject);
|
|
352
362
|
throw new JobDisabledError(`Job ${jobId} was disabled`);
|
|
353
363
|
}
|
|
354
364
|
if (job.status === JobStatus2.FAILED) {
|
|
365
|
+
this.removePromise(jobId, resolve, reject);
|
|
355
366
|
throw this.buildErrorFromJob(job);
|
|
356
367
|
}
|
|
357
|
-
const { promise, resolve, reject } = Promise.withResolvers();
|
|
358
|
-
promise.catch(() => {});
|
|
359
|
-
const promises = this.activeJobPromises.get(jobId) || [];
|
|
360
|
-
promises.push({ resolve, reject });
|
|
361
|
-
this.activeJobPromises.set(jobId, promises);
|
|
362
368
|
return promise;
|
|
363
369
|
}
|
|
370
|
+
removePromise(jobId, resolve, reject) {
|
|
371
|
+
const list = this.activeJobPromises.get(jobId);
|
|
372
|
+
if (!list)
|
|
373
|
+
return;
|
|
374
|
+
const idx = list.findIndex((p) => p.resolve === resolve && p.reject === reject);
|
|
375
|
+
if (idx !== -1)
|
|
376
|
+
list.splice(idx, 1);
|
|
377
|
+
if (list.length === 0)
|
|
378
|
+
this.activeJobPromises.delete(jobId);
|
|
379
|
+
}
|
|
364
380
|
async abort(jobId) {
|
|
365
381
|
if (!jobId)
|
|
366
382
|
throw new JobNotFoundError("Cannot abort undefined job");
|
|
367
|
-
|
|
383
|
+
const firedLocally = this.server?.abortJob(jobId) ?? false;
|
|
384
|
+
if (!firedLocally) {
|
|
385
|
+
try {
|
|
386
|
+
await this.storage.abort(jobId);
|
|
387
|
+
} finally {
|
|
388
|
+
this.events.emit("job_aborting", this.queueName, jobId);
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
368
392
|
this.events.emit("job_aborting", this.queueName, jobId);
|
|
369
393
|
}
|
|
370
394
|
async abortJobRun(jobRunId) {
|
|
@@ -531,7 +555,7 @@ class JobQueueClient {
|
|
|
531
555
|
|
|
532
556
|
// src/job/JobQueueServer.ts
|
|
533
557
|
import { JobStatus as JobStatus4 } from "@workglow/storage";
|
|
534
|
-
import { EventEmitter as EventEmitter3 } from "@workglow/util";
|
|
558
|
+
import { EventEmitter as EventEmitter3, getLogger as getLogger2 } from "@workglow/util";
|
|
535
559
|
|
|
536
560
|
// src/limiter/NullLimiter.ts
|
|
537
561
|
import { createServiceToken } from "@workglow/util";
|
|
@@ -567,10 +591,14 @@ class JobQueueWorker {
|
|
|
567
591
|
jobClass;
|
|
568
592
|
limiter;
|
|
569
593
|
pollIntervalMs;
|
|
594
|
+
stopTimeoutMs;
|
|
570
595
|
events = new EventEmitter2;
|
|
571
596
|
running = false;
|
|
597
|
+
inFlight = new Map;
|
|
572
598
|
wakeResolve = null;
|
|
573
599
|
wakeTimer = null;
|
|
600
|
+
wakePending = false;
|
|
601
|
+
loopPromise = null;
|
|
574
602
|
activeJobAbortControllers = new Map;
|
|
575
603
|
processingTimes = new Map;
|
|
576
604
|
constructor(jobClass, options) {
|
|
@@ -580,6 +608,7 @@ class JobQueueWorker {
|
|
|
580
608
|
this.jobClass = jobClass;
|
|
581
609
|
this.limiter = options.limiter ?? new NullLimiter;
|
|
582
610
|
this.pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
611
|
+
this.stopTimeoutMs = options.stopTimeoutMs ?? 30000;
|
|
583
612
|
}
|
|
584
613
|
async start() {
|
|
585
614
|
if (this.running) {
|
|
@@ -587,17 +616,27 @@ class JobQueueWorker {
|
|
|
587
616
|
}
|
|
588
617
|
this.running = true;
|
|
589
618
|
this.events.emit("worker_start");
|
|
590
|
-
this.processJobs();
|
|
619
|
+
this.loopPromise = this.processJobs();
|
|
591
620
|
return this;
|
|
592
621
|
}
|
|
622
|
+
requestAbort(jobId) {
|
|
623
|
+
const controller = this.activeJobAbortControllers.get(jobId);
|
|
624
|
+
if (controller && !controller.signal.aborted) {
|
|
625
|
+
controller.abort();
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
593
630
|
notify() {
|
|
631
|
+
this.wakePending = true;
|
|
594
632
|
if (this.wakeResolve) {
|
|
595
633
|
if (this.wakeTimer) {
|
|
596
634
|
clearTimeout(this.wakeTimer);
|
|
597
635
|
this.wakeTimer = null;
|
|
598
636
|
}
|
|
599
|
-
this.wakeResolve
|
|
637
|
+
const resolve = this.wakeResolve;
|
|
600
638
|
this.wakeResolve = null;
|
|
639
|
+
resolve();
|
|
601
640
|
}
|
|
602
641
|
}
|
|
603
642
|
async stop() {
|
|
@@ -606,15 +645,24 @@ class JobQueueWorker {
|
|
|
606
645
|
}
|
|
607
646
|
this.running = false;
|
|
608
647
|
this.notify();
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
648
|
+
const loopPromise = this.loopPromise;
|
|
649
|
+
this.loopPromise = null;
|
|
650
|
+
if (loopPromise) {
|
|
651
|
+
await loopPromise;
|
|
652
|
+
}
|
|
653
|
+
if (this.stopTimeoutMs > 0 && this.inFlight.size > 0) {
|
|
654
|
+
const drain = Promise.allSettled([...this.inFlight.values()]);
|
|
655
|
+
await Promise.race([drain, sleep(this.stopTimeoutMs)]);
|
|
656
|
+
}
|
|
657
|
+
if (this.inFlight.size > 0) {
|
|
658
|
+
for (const controller of this.activeJobAbortControllers.values()) {
|
|
659
|
+
if (!controller.signal.aborted) {
|
|
660
|
+
controller.abort();
|
|
661
|
+
}
|
|
615
662
|
}
|
|
663
|
+
const abortDrain = Promise.allSettled([...this.inFlight.values()]);
|
|
664
|
+
await Promise.race([abortDrain, sleep(1000)]);
|
|
616
665
|
}
|
|
617
|
-
await sleep(sleepTime);
|
|
618
666
|
this.events.emit("worker_stop");
|
|
619
667
|
return this;
|
|
620
668
|
}
|
|
@@ -662,6 +710,10 @@ class JobQueueWorker {
|
|
|
662
710
|
if (canProceed) {
|
|
663
711
|
const job = await this.next();
|
|
664
712
|
if (job) {
|
|
713
|
+
if (!this.running) {
|
|
714
|
+
await this.releaseClaimedJob(job);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
665
717
|
this.processSingleJob(job);
|
|
666
718
|
continue;
|
|
667
719
|
}
|
|
@@ -690,18 +742,27 @@ class JobQueueWorker {
|
|
|
690
742
|
return this.pollIntervalMs;
|
|
691
743
|
}
|
|
692
744
|
waitForWakeOrTimeout(timeoutMs) {
|
|
745
|
+
if (this.wakePending) {
|
|
746
|
+
this.wakePending = false;
|
|
747
|
+
return Promise.resolve();
|
|
748
|
+
}
|
|
693
749
|
return new Promise((resolve) => {
|
|
694
750
|
this.wakeTimer = setTimeout(() => {
|
|
695
751
|
this.wakeTimer = null;
|
|
696
752
|
this.wakeResolve = null;
|
|
753
|
+
this.wakePending = false;
|
|
697
754
|
resolve();
|
|
698
755
|
}, timeoutMs);
|
|
699
756
|
this.wakeResolve = () => {
|
|
757
|
+
this.wakePending = false;
|
|
700
758
|
resolve();
|
|
701
759
|
};
|
|
702
760
|
});
|
|
703
761
|
}
|
|
704
762
|
async checkForAbortingJobs() {
|
|
763
|
+
if (this.activeJobAbortControllers.size === 0) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
705
766
|
const abortingJobs = await this.storage.peek(JobStatus3.ABORTING);
|
|
706
767
|
for (const jobData of abortingJobs) {
|
|
707
768
|
const controller = this.activeJobAbortControllers.get(jobData.id);
|
|
@@ -714,6 +775,8 @@ class JobQueueWorker {
|
|
|
714
775
|
if (!job || !job.id) {
|
|
715
776
|
throw new JobNotFoundError("Invalid job provided for processing");
|
|
716
777
|
}
|
|
778
|
+
const { promise: inFlightPromise, resolve: resolveInFlight } = Promise.withResolvers();
|
|
779
|
+
this.inFlight.set(job.id, inFlightPromise);
|
|
717
780
|
const startTime = Date.now();
|
|
718
781
|
const telemetry = getTelemetryProvider();
|
|
719
782
|
const span = telemetry.isEnabled ? telemetry.startSpan("workglow.job.process", {
|
|
@@ -764,7 +827,12 @@ class JobQueueWorker {
|
|
|
764
827
|
span?.setAttributes({ "workglow.job.error": spanErrorMessage });
|
|
765
828
|
} finally {
|
|
766
829
|
span?.end();
|
|
767
|
-
|
|
830
|
+
try {
|
|
831
|
+
await this.limiter.recordJobCompletion();
|
|
832
|
+
} finally {
|
|
833
|
+
this.inFlight.delete(job.id);
|
|
834
|
+
resolveInFlight();
|
|
835
|
+
}
|
|
768
836
|
}
|
|
769
837
|
}
|
|
770
838
|
async executeJob(job, signal) {
|
|
@@ -777,7 +845,6 @@ class JobQueueWorker {
|
|
|
777
845
|
}
|
|
778
846
|
async updateProgress(jobId, progress, message = "", details = null) {
|
|
779
847
|
progress = Math.max(0, Math.min(100, progress));
|
|
780
|
-
await this.storage.saveProgress(jobId, progress, message, details);
|
|
781
848
|
this.events.emit("job_progress", jobId, progress, message, details);
|
|
782
849
|
}
|
|
783
850
|
async completeJob(job, output) {
|
|
@@ -830,6 +897,13 @@ class JobQueueWorker {
|
|
|
830
897
|
this.cleanupJob(job.id);
|
|
831
898
|
}
|
|
832
899
|
}
|
|
900
|
+
async releaseClaimedJob(job) {
|
|
901
|
+
try {
|
|
902
|
+
await this.storage.release(job.id);
|
|
903
|
+
} catch (err) {
|
|
904
|
+
getLogger().error("releaseClaimedJob errored:", { error: err });
|
|
905
|
+
}
|
|
906
|
+
}
|
|
833
907
|
async rescheduleJob(job, retryDate) {
|
|
834
908
|
try {
|
|
835
909
|
job.status = JobStatus3.PENDING;
|
|
@@ -848,6 +922,9 @@ class JobQueueWorker {
|
|
|
848
922
|
createAbortController(jobId) {
|
|
849
923
|
if (!jobId)
|
|
850
924
|
throw new JobNotFoundError("Cannot create abort controller for undefined job");
|
|
925
|
+
if (!this.inFlight.has(jobId)) {
|
|
926
|
+
throw new Error(`createAbortController invariant violated: jobId ${String(jobId)} is not in inFlight. ` + `Abort controllers must only be created from within processSingleJob.`);
|
|
927
|
+
}
|
|
851
928
|
if (this.activeJobAbortControllers.has(jobId)) {
|
|
852
929
|
return this.activeJobAbortControllers.get(jobId);
|
|
853
930
|
}
|
|
@@ -857,13 +934,18 @@ class JobQueueWorker {
|
|
|
857
934
|
return abortController;
|
|
858
935
|
}
|
|
859
936
|
async handleAbort(jobId) {
|
|
937
|
+
if (this.inFlight.has(jobId)) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
860
940
|
const job = await this.getJob(jobId);
|
|
861
941
|
if (!job) {
|
|
862
942
|
getLogger().error("handleAbort: job not found", { jobId });
|
|
863
943
|
return;
|
|
864
944
|
}
|
|
865
|
-
|
|
866
|
-
|
|
945
|
+
if (job.status === JobStatus3.COMPLETED || job.status === JobStatus3.FAILED || job.status === JobStatus3.DISABLED) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
await this.failJob(job, new AbortSignalJobError("Job Aborted"));
|
|
867
949
|
}
|
|
868
950
|
async getJob(id) {
|
|
869
951
|
const job = await this.storage.get(id);
|
|
@@ -920,6 +1002,7 @@ class JobQueueServer {
|
|
|
920
1002
|
deleteAfterFailureMs;
|
|
921
1003
|
deleteAfterDisabledMs;
|
|
922
1004
|
cleanupIntervalMs;
|
|
1005
|
+
stopTimeoutMs;
|
|
923
1006
|
events = new EventEmitter3;
|
|
924
1007
|
workers = [];
|
|
925
1008
|
clients = new Set;
|
|
@@ -946,6 +1029,7 @@ class JobQueueServer {
|
|
|
946
1029
|
this.deleteAfterFailureMs = options.deleteAfterFailureMs;
|
|
947
1030
|
this.deleteAfterDisabledMs = options.deleteAfterDisabledMs;
|
|
948
1031
|
this.cleanupIntervalMs = options.cleanupIntervalMs ?? 1e4;
|
|
1032
|
+
this.stopTimeoutMs = options.stopTimeoutMs;
|
|
949
1033
|
this.initializeWorkers();
|
|
950
1034
|
}
|
|
951
1035
|
async start() {
|
|
@@ -961,11 +1045,21 @@ class JobQueueServer {
|
|
|
961
1045
|
this.notifyWorkers();
|
|
962
1046
|
}
|
|
963
1047
|
});
|
|
964
|
-
} catch {
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
getLogger2().debug("subscribeToChanges unsupported on this storage", {
|
|
1050
|
+
queueName: this.queueName,
|
|
1051
|
+
error: err
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
965
1054
|
await Promise.all(this.workers.map((worker) => worker.start()));
|
|
966
|
-
this.
|
|
1055
|
+
if (this.hasRetentionTtls()) {
|
|
1056
|
+
this.startCleanupLoop();
|
|
1057
|
+
}
|
|
967
1058
|
return this;
|
|
968
1059
|
}
|
|
1060
|
+
hasRetentionTtls() {
|
|
1061
|
+
return this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0 || this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0 || this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0;
|
|
1062
|
+
}
|
|
969
1063
|
async stop() {
|
|
970
1064
|
if (!this.running) {
|
|
971
1065
|
return this;
|
|
@@ -1024,6 +1118,17 @@ class JobQueueServer {
|
|
|
1024
1118
|
worker.notify();
|
|
1025
1119
|
}
|
|
1026
1120
|
}
|
|
1121
|
+
handleJobAdded(_jobId) {
|
|
1122
|
+
this.notifyWorkers();
|
|
1123
|
+
}
|
|
1124
|
+
abortJob(jobId) {
|
|
1125
|
+
for (const worker of this.workers) {
|
|
1126
|
+
if (worker.requestAbort(jobId)) {
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1027
1132
|
on(event, listener) {
|
|
1028
1133
|
this.events.on(event, listener);
|
|
1029
1134
|
}
|
|
@@ -1041,7 +1146,8 @@ class JobQueueServer {
|
|
|
1041
1146
|
storage: this.storage,
|
|
1042
1147
|
queueName: this.queueName,
|
|
1043
1148
|
limiter: this.limiter,
|
|
1044
|
-
pollIntervalMs: this.pollIntervalMs
|
|
1149
|
+
pollIntervalMs: this.pollIntervalMs,
|
|
1150
|
+
stopTimeoutMs: this.stopTimeoutMs
|
|
1045
1151
|
});
|
|
1046
1152
|
worker.on("job_start", (jobId) => {
|
|
1047
1153
|
this.stats = { ...this.stats, totalJobs: this.stats.totalJobs + 1 };
|
|
@@ -1490,4 +1596,4 @@ export {
|
|
|
1490
1596
|
AbortSignalJobError
|
|
1491
1597
|
};
|
|
1492
1598
|
|
|
1493
|
-
//# debugId=
|
|
1599
|
+
//# debugId=DEA35BFCE014423364756E2164756E21
|