@workglow/job-queue 0.2.22 → 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/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
- await this.storage.abort(jobId);
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 size = await this.storage.size(JobStatus3.PROCESSING);
611
- const sleepTime = Math.max(100, size * 2);
612
- await sleep(sleepTime);
613
- for (const controller of this.activeJobAbortControllers.values()) {
614
- if (!controller.signal.aborted) {
615
- controller.abort();
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
- await this.limiter.recordJobCompletion();
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
- const error = new AbortSignalJobError("Job Aborted");
867
- await this.failJob(job, error);
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.startCleanupLoop();
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=9EA74A4E777F02B264756E2164756E21
1600
+ //# debugId=306268EEB05F1A3964756E2164756E21