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