@orion-js/dogs 4.5.1 → 4.6.1

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/index.js CHANGED
@@ -71,6 +71,11 @@ var JobRecordSchema = schemaWithName("JobRecord", {
71
71
  nextRunAt: { type: "date" },
72
72
  lastRunAt: { type: "date", optional: true },
73
73
  lockedUntil: { type: "date", optional: true },
74
+ /**
75
+ * Identifies the execution that currently owns the lock. Mutations from an execution must match
76
+ * this value so a stale execution cannot modify a newer claim.
77
+ */
78
+ lockId: { type: "string", optional: true },
74
79
  tries: { type: "number", optional: true },
75
80
  params: { type: "blackbox", optional: true },
76
81
  /**
@@ -138,6 +143,7 @@ var JobsRepo = class {
138
143
  __publicField(this, "jobs", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
139
144
  }
140
145
  async getJobAndLock(jobNames, lockTime) {
146
+ const executionId = generateId();
141
147
  const lockedUntil = new Date(Date.now() + lockTime);
142
148
  const job = await this.jobs.findOneAndUpdate(
143
149
  {
@@ -151,7 +157,7 @@ var JobsRepo = class {
151
157
  ]
152
158
  },
153
159
  {
154
- $set: { lockedUntil, lastRunAt: /* @__PURE__ */ new Date() },
160
+ $set: { lockId: executionId, lockedUntil, lastRunAt: /* @__PURE__ */ new Date() },
155
161
  $inc: { tries: 1 }
156
162
  },
157
163
  {
@@ -172,7 +178,8 @@ var JobsRepo = class {
172
178
  }
173
179
  return {
174
180
  jobId: job._id,
175
- executionId: generateId(),
181
+ executionId,
182
+ lockId: executionId,
176
183
  name: job.jobName,
177
184
  params: job.params,
178
185
  type: job.type,
@@ -183,8 +190,11 @@ var JobsRepo = class {
183
190
  wasStale
184
191
  };
185
192
  }
186
- async setJobRecordPriority(jobId, priority) {
187
- await this.jobs.updateOne(jobId, { $set: { priority } });
193
+ async setJobRecordPriority(jobId, priority, lockId) {
194
+ const result = await this.jobs.updateOne(lockId ? { _id: jobId, lockId } : jobId, {
195
+ $set: { priority }
196
+ });
197
+ return result.matchedCount > 0;
188
198
  }
189
199
  async scheduleNextRun(options) {
190
200
  const updator = {
@@ -193,22 +203,31 @@ var JobsRepo = class {
193
203
  priority: options.priority,
194
204
  ...options.resetTries ? { tries: 0 } : {}
195
205
  },
196
- $unset: { lockedUntil: "" }
206
+ $unset: { lockId: "", lockedUntil: "" }
197
207
  };
198
- await this.jobs.updateOne(options.jobId, updator);
208
+ const result = await this.jobs.updateOne(
209
+ options.lockId ? { _id: options.jobId, lockId: options.lockId } : options.jobId,
210
+ updator
211
+ );
212
+ return result.matchedCount > 0;
199
213
  }
200
- async deleteEventJob(jobId) {
201
- await this.jobs.deleteOne({ _id: jobId, type: "event" });
214
+ async deleteEventJob(jobId, lockId) {
215
+ const result = await this.jobs.deleteOne({
216
+ _id: jobId,
217
+ type: "event",
218
+ ...lockId ? { lockId } : {}
219
+ });
220
+ return result.deletedCount > 0;
202
221
  }
203
222
  /**
204
223
  * Marks an event job as having reached its maximum tries limit.
205
224
  * The job won't be picked up for execution and MongoDB will delete it after
206
225
  * the configured retention period.
207
226
  */
208
- async markJobAsMaxTriesReached(jobId, retentionMs = DEFAULT_MAX_TRIES_REACHED_RETENTION_MS) {
227
+ async markJobAsMaxTriesReached(jobId, retentionMs = DEFAULT_MAX_TRIES_REACHED_RETENTION_MS, lockId) {
209
228
  const maxTriesReachedAt = /* @__PURE__ */ new Date();
210
- await this.jobs.updateOne(
211
- { _id: jobId, type: "event" },
229
+ const result = await this.jobs.updateOne(
230
+ { _id: jobId, type: "event", ...lockId ? { lockId } : {} },
212
231
  {
213
232
  $set: {
214
233
  status: "maxTriesReached",
@@ -216,11 +235,13 @@ var JobsRepo = class {
216
235
  ...retentionMs === null ? {} : { expiresAt: new Date(maxTriesReachedAt.getTime() + retentionMs) }
217
236
  },
218
237
  $unset: {
238
+ lockId: "",
219
239
  lockedUntil: "",
220
240
  ...retentionMs === null ? { expiresAt: "" } : {}
221
241
  }
222
242
  }
223
243
  );
244
+ return result.matchedCount > 0;
224
245
  }
225
246
  /**
226
247
  * Applies the current retention configuration to existing maxTriesReached
@@ -249,23 +270,22 @@ var JobsRepo = class {
249
270
  }
250
271
  ]);
251
272
  }
252
- async extendLockTime(jobId, extraTime) {
253
- await this.updateLockTime(jobId, extraTime);
273
+ async extendLockTime(jobId, extraTime, lockId) {
274
+ return this.updateLockTime(jobId, extraTime, lockId);
254
275
  }
255
276
  /**
256
277
  * Updates the lock time for a job to the specified duration from now.
257
278
  * Can be used to both extend or shorten the lock time.
258
279
  */
259
- async updateLockTime(jobId, lockDuration) {
280
+ async updateLockTime(jobId, lockDuration, lockId) {
260
281
  const lockedUntil = new Date(Date.now() + lockDuration);
261
- await this.jobs.updateOne(
262
- {
263
- _id: jobId
264
- },
282
+ const result = await this.jobs.updateOne(
283
+ { _id: jobId, ...lockId ? { lockId } : {} },
265
284
  {
266
285
  $set: { lockedUntil }
267
286
  }
268
287
  );
288
+ return result.matchedCount > 0;
269
289
  }
270
290
  async unlockAllJobs() {
271
291
  const result = await this.jobs.updateMany(
@@ -273,7 +293,7 @@ var JobsRepo = class {
273
293
  lockedUntil: { $exists: true }
274
294
  },
275
295
  {
276
- $unset: { lockedUntil: "" }
296
+ $unset: { lockId: "", lockedUntil: "" }
277
297
  }
278
298
  );
279
299
  return result.modifiedCount;
@@ -579,11 +599,15 @@ var Executor = class {
579
599
  getEffectiveLockTime(job, jobToRun) {
580
600
  return job.lockTime ?? jobToRun.lockTime;
581
601
  }
582
- getContext(job, jobToRun, onStale) {
602
+ getContext(job, jobToRun, onStale, isExecutionActive) {
583
603
  var _a;
584
604
  const effectiveLockTime = this.getEffectiveLockTime(job, jobToRun);
585
605
  let staleTimeout = setTimeout(() => onStale(), effectiveLockTime);
586
606
  (_a = staleTimeout.unref) == null ? void 0 : _a.call(staleTimeout);
607
+ const contextLogger = logger3.addMetadata({
608
+ jobName: jobToRun.name,
609
+ jobId: jobToRun.jobId
610
+ });
587
611
  return {
588
612
  definition: job,
589
613
  record: jobToRun,
@@ -591,15 +615,26 @@ var Executor = class {
591
615
  clearStaleTimeout: () => clearTimeout(staleTimeout),
592
616
  extendLockTime: async (extraTime) => {
593
617
  var _a2;
618
+ if (!isExecutionActive()) {
619
+ contextLogger.warn(`Will not extend lock for stale execution "${jobToRun.executionId}"`);
620
+ return;
621
+ }
594
622
  clearTimeout(staleTimeout);
623
+ let didExtend = false;
624
+ try {
625
+ didExtend = await this.jobsRepo.extendLockTime(jobToRun.jobId, extraTime, jobToRun.lockId);
626
+ } catch (error) {
627
+ onStale();
628
+ throw error;
629
+ }
630
+ if (!didExtend) {
631
+ onStale();
632
+ return;
633
+ }
595
634
  staleTimeout = setTimeout(() => onStale(), extraTime);
596
635
  (_a2 = staleTimeout.unref) == null ? void 0 : _a2.call(staleTimeout);
597
- await this.jobsRepo.extendLockTime(jobToRun.jobId, extraTime);
598
636
  },
599
- logger: logger3.addMetadata({
600
- jobName: jobToRun.name,
601
- jobId: jobToRun.jobId
602
- })
637
+ logger: contextLogger
603
638
  };
604
639
  }
605
640
  getJobDefinition(jobToRun, jobs) {
@@ -631,8 +666,16 @@ var Executor = class {
631
666
  jobLogger.warn(
632
667
  `Job "${jobToRun.name}" has exceeded max tries (${jobToRun.tries}). Marking as maxTriesReached.`
633
668
  );
634
- await this.jobsRepo.markJobAsMaxTriesReached(jobToRun.jobId, retentionMs);
635
- if (!onMaxTriesReached) return;
669
+ const didMark = await this.jobsRepo.markJobAsMaxTriesReached(
670
+ jobToRun.jobId,
671
+ retentionMs,
672
+ jobToRun.lockId
673
+ );
674
+ if (!didMark) {
675
+ jobLogger.warn(`Could not mark job "${jobToRun.name}" because its lock is no longer owned`);
676
+ return false;
677
+ }
678
+ if (!onMaxTriesReached) return true;
636
679
  try {
637
680
  await onMaxTriesReached(jobToRun);
638
681
  } catch (callbackError) {
@@ -640,30 +683,33 @@ var Executor = class {
640
683
  error: callbackError
641
684
  });
642
685
  }
686
+ return true;
643
687
  }
644
688
  async onError(error, job, jobToRun, context) {
645
689
  const scheduleRecurrent = async () => {
646
690
  if (job.type === "recurrent") {
647
- await this.jobsRepo.scheduleNextRun({
691
+ return this.jobsRepo.scheduleNextRun({
648
692
  jobId: jobToRun.jobId,
649
693
  nextRunAt: getNextRunDate(job),
650
694
  resetTries: true,
651
- priority: job.priority
695
+ priority: job.priority,
696
+ lockId: jobToRun.lockId
652
697
  });
653
698
  }
699
+ return true;
654
700
  };
655
701
  const handleRetry = async (nextRunAt) => {
656
- await this.jobsRepo.scheduleNextRun({
702
+ return this.jobsRepo.scheduleNextRun({
657
703
  jobId: jobToRun.jobId,
658
704
  nextRunAt,
659
705
  resetTries: false,
660
- priority: job.type === "recurrent" ? job.priority : jobToRun.priority
706
+ priority: job.type === "recurrent" ? job.priority : jobToRun.priority,
707
+ lockId: jobToRun.lockId
661
708
  });
662
709
  };
663
710
  if (!job.onError) {
664
711
  context.logger.error(`Error executing job "${jobToRun.name}"`, { error });
665
- await scheduleRecurrent();
666
- return;
712
+ return scheduleRecurrent();
667
713
  }
668
714
  context.logger.info(`Error executing job "${jobToRun.name}"`, { error });
669
715
  const result = await job.onError(
@@ -672,12 +718,12 @@ var Executor = class {
672
718
  context
673
719
  );
674
720
  if (result.action === "dismiss") {
675
- await scheduleRecurrent();
676
- return;
721
+ return scheduleRecurrent();
677
722
  }
678
723
  if (result.action === "retry") {
679
- await handleRetry(getNextRunDate(result));
724
+ return handleRetry(getNextRunDate(result));
680
725
  }
726
+ return true;
681
727
  }
682
728
  async saveExecution(options) {
683
729
  const { startedAt, status, errorMessage, result, job, jobToRun } = options;
@@ -707,19 +753,21 @@ var Executor = class {
707
753
  async afterExecutionSuccess(job, jobToRun, context) {
708
754
  if (job.type === "recurrent") {
709
755
  context.logger.debug(`Scheduling next run for recurrent job "${jobToRun.name}"`);
710
- await this.jobsRepo.scheduleNextRun({
756
+ return this.jobsRepo.scheduleNextRun({
711
757
  jobId: jobToRun.jobId,
712
758
  nextRunAt: getNextRunDate(job),
713
759
  resetTries: true,
714
- priority: job.priority
760
+ priority: job.priority,
761
+ lockId: jobToRun.lockId
715
762
  });
716
763
  }
717
764
  if (job.type === "event") {
718
765
  context.logger.debug(`Removing event job after success "${jobToRun.name}"`);
719
- await this.jobsRepo.deleteEventJob(jobToRun.jobId);
766
+ return this.jobsRepo.deleteEventJob(jobToRun.jobId, jobToRun.lockId);
720
767
  }
768
+ return false;
721
769
  }
722
- async executeJob(config, jobToRun, respawnWorker) {
770
+ async executeJob(config, jobToRun, onExecutionStale) {
723
771
  const job = this.getJobDefinition(jobToRun, config.jobs);
724
772
  if (!job) return;
725
773
  if (job.type === "event") {
@@ -735,33 +783,64 @@ var Executor = class {
735
783
  }
736
784
  const effectiveLockTime = this.getEffectiveLockTime(job, jobToRun);
737
785
  if (effectiveLockTime !== jobToRun.lockTime) {
738
- await this.jobsRepo.updateLockTime(jobToRun.jobId, effectiveLockTime);
786
+ const didUpdateLock = await this.jobsRepo.updateLockTime(
787
+ jobToRun.jobId,
788
+ effectiveLockTime,
789
+ jobToRun.lockId
790
+ );
791
+ if (!didUpdateLock) {
792
+ logger3.warn(`Will not execute job "${jobToRun.name}" because its lock is no longer owned`);
793
+ return;
794
+ }
739
795
  }
740
796
  const tracer = trace.getTracer("orionjs.dogs", "1.0");
741
797
  await tracer.startActiveSpan(`job.${jobToRun.name}.${jobToRun.executionId}`, async (span) => {
742
798
  try {
743
799
  const startedAt = /* @__PURE__ */ new Date();
744
- const onStale = async () => {
745
- if (job.onStale) {
746
- context.logger.info(`Job "${jobToRun.name}" is stale`);
747
- job.onStale(jobToRun.params, context);
748
- } else {
749
- context.logger.error(`Job "${jobToRun.name}" is stale`);
750
- }
751
- await this.jobsRepo.setJobRecordPriority(jobToRun.jobId, 0);
752
- void respawnWorker();
753
- void this.saveExecution({
754
- startedAt,
755
- status: "stale",
756
- result: null,
757
- errorMessage: null,
758
- job,
759
- jobToRun
760
- }).catch((error) => {
761
- context.logger.error("Error saving stale execution history", { error });
800
+ let executionStatus = "running";
801
+ let staleHandlingPromise;
802
+ let context;
803
+ const markAsStale = () => {
804
+ if (executionStatus === "stale") return staleHandlingPromise;
805
+ executionStatus = "stale";
806
+ context.clearStaleTimeout();
807
+ staleHandlingPromise = (async () => {
808
+ context.logger.warn(`Job "${jobToRun.name}" is stale`);
809
+ if (job.onStale) {
810
+ void Promise.resolve(job.onStale(jobToRun.params, context)).catch((error) => {
811
+ context.logger.error(`Error in onStale callback for job "${jobToRun.name}"`, {
812
+ error
813
+ });
814
+ });
815
+ }
816
+ try {
817
+ await this.jobsRepo.setJobRecordPriority(jobToRun.jobId, 0, jobToRun.lockId);
818
+ } catch (error) {
819
+ context.logger.error(`Error lowering priority for stale job "${jobToRun.name}"`, {
820
+ error
821
+ });
822
+ } finally {
823
+ onExecutionStale();
824
+ }
825
+ await this.saveExecution({
826
+ startedAt,
827
+ status: "stale",
828
+ result: null,
829
+ errorMessage: null,
830
+ job,
831
+ jobToRun
832
+ });
833
+ })().catch((error) => {
834
+ context.logger.error("Error handling stale execution", { error });
762
835
  });
836
+ return staleHandlingPromise;
763
837
  };
764
- const context = this.getContext(job, jobToRun, onStale);
838
+ context = this.getContext(
839
+ job,
840
+ jobToRun,
841
+ () => void markAsStale(),
842
+ () => executionStatus === "running"
843
+ );
765
844
  const extraContext = {
766
845
  controllerType: "job",
767
846
  jobName: jobToRun.name,
@@ -777,6 +856,18 @@ var Executor = class {
777
856
  });
778
857
  const result = await job.resolve(jobToRun.params, context);
779
858
  context.clearStaleTimeout();
859
+ if (executionStatus === "stale") {
860
+ await staleHandlingPromise;
861
+ context.logger.warn(
862
+ `Ignoring late success from stale execution "${jobToRun.executionId}"`
863
+ );
864
+ return;
865
+ }
866
+ const stillOwnsJob = await this.afterExecutionSuccess(job, jobToRun, context);
867
+ if (!stillOwnsJob) {
868
+ await markAsStale();
869
+ return;
870
+ }
780
871
  void this.saveExecution({
781
872
  startedAt,
782
873
  status: "success",
@@ -787,9 +878,21 @@ var Executor = class {
787
878
  }).catch((error) => {
788
879
  context.logger.error("Error saving successful execution history", { error });
789
880
  });
790
- await this.afterExecutionSuccess(job, jobToRun, context);
791
881
  } catch (error) {
792
882
  context.clearStaleTimeout();
883
+ if (executionStatus === "stale") {
884
+ await staleHandlingPromise;
885
+ context.logger.warn(
886
+ `Ignoring late error from stale execution "${jobToRun.executionId}"`,
887
+ { error }
888
+ );
889
+ return;
890
+ }
891
+ const stillOwnsJob = await this.onError(error, job, jobToRun, context);
892
+ if (!stillOwnsJob) {
893
+ await markAsStale();
894
+ return;
895
+ }
793
896
  void this.saveExecution({
794
897
  startedAt,
795
898
  status: "error",
@@ -800,7 +903,6 @@ var Executor = class {
800
903
  }).catch((saveError) => {
801
904
  context.logger.error("Error saving failed execution history", { error: saveError });
802
905
  });
803
- await this.onError(error, job, jobToRun, context);
804
906
  }
805
907
  });
806
908
  } catch (error) {
@@ -860,87 +962,131 @@ var WorkerService = class {
860
962
  }
861
963
  workersInstance.runningJobsByName.set(jobName, currentExecutions - 1);
862
964
  }
863
- async withJobAcquisitionLock(workersInstance, callback) {
864
- const previousLock = workersInstance.jobAcquisitionLock;
865
- let releaseLock;
866
- workersInstance.jobAcquisitionLock = new Promise((resolve) => {
867
- releaseLock = resolve;
868
- });
869
- await previousLock;
870
- try {
871
- return await callback();
872
- } finally {
873
- releaseLock();
965
+ releaseExecutionCapacity(workersInstance, execution, nextStatus) {
966
+ if (execution.status !== "running") return;
967
+ execution.status = nextStatus;
968
+ workersInstance.activeExecutions.delete(execution.job.executionId);
969
+ this.releaseJobExecution(workersInstance, execution.job.name);
970
+ execution.releaseCapacity();
971
+ if (nextStatus === "stale") {
972
+ workersInstance.staleExecutions.set(execution.job.executionId, execution);
874
973
  }
875
974
  }
876
- async getJobAndReserveExecution(config, workersInstance, jobNames) {
877
- return this.withJobAcquisitionLock(workersInstance, async () => {
878
- const availableJobNames = this.getAvailableJobNames(config, workersInstance, jobNames);
879
- if (availableJobNames.length === 0) return;
880
- const jobToRun = await this.jobsRepo.getJobAndLock(availableJobNames, config.defaultLockTime);
881
- if (!jobToRun) return;
882
- this.reserveJobExecution(workersInstance, jobToRun.name);
883
- return jobToRun;
975
+ startExecution(config, workersInstance, jobToRun) {
976
+ let releaseCapacity;
977
+ const capacityPromise = new Promise((resolve) => {
978
+ releaseCapacity = resolve;
979
+ });
980
+ const execution = {
981
+ job: jobToRun,
982
+ promise: Promise.resolve(),
983
+ capacityPromise,
984
+ releaseCapacity,
985
+ status: "running"
986
+ };
987
+ this.reserveJobExecution(workersInstance, jobToRun.name);
988
+ workersInstance.activeExecutions.set(jobToRun.executionId, execution);
989
+ logger4.debug(`Starting job execution "${jobToRun.executionId}"`, jobToRun);
990
+ const executionPromise = this.executor.executeJob(config, jobToRun, () => {
991
+ this.releaseExecutionCapacity(workersInstance, execution, "stale");
992
+ });
993
+ execution.promise = executionPromise;
994
+ void executionPromise.catch((error) => {
995
+ logger4.error(`Unhandled error executing job "${jobToRun.name}"`, { error });
996
+ }).finally(() => {
997
+ if (execution.status === "running") {
998
+ this.releaseExecutionCapacity(workersInstance, execution, "finished");
999
+ } else if (execution.status === "stale") {
1000
+ workersInstance.staleExecutions.delete(jobToRun.executionId);
1001
+ }
884
1002
  });
885
1003
  }
886
- async runWorkerLoop(config, workersInstance, workerInstance, jobNames, executeConfig) {
887
- const jobToRun = await this.getJobAndReserveExecution(config, workersInstance, jobNames);
888
- if (!jobToRun) {
889
- logger4.debug("No job to run");
890
- return false;
891
- }
892
- logger4.debug(`Got job [w${workerInstance.workerIndex}] to run:`, jobToRun);
893
- try {
894
- await this.executor.executeJob(executeConfig, jobToRun, workerInstance.respawn);
895
- } finally {
896
- this.releaseJobExecution(workersInstance, jobToRun.name);
897
- }
898
- return true;
1004
+ async waitForPollOrStop(config, workersInstance) {
1005
+ await Promise.race([sleep(config.pollInterval), workersInstance.stopSignal]);
899
1006
  }
900
- async startWorker(config, workersInstance, workerInstance) {
901
- const names = this.getJobNames(config.jobs);
902
- logger4.debug(
903
- `Running worker loop [w${workerInstance.workerIndex}] for jobs "${names.join(", ")}"...`
1007
+ async waitForCapacityOrStop(workersInstance) {
1008
+ const capacityPromises = Array.from(
1009
+ workersInstance.activeExecutions.values(),
1010
+ (execution) => execution.capacityPromise
904
1011
  );
1012
+ if (capacityPromises.length === 0) return;
1013
+ await Promise.race([...capacityPromises, workersInstance.stopSignal]);
1014
+ }
1015
+ async runSchedulerLoop(config, workersInstance) {
1016
+ const jobNames = this.getJobNames(config.jobs);
905
1017
  const executeConfig = {
906
1018
  jobs: config.jobs,
907
1019
  maxTries: config.maxTries,
908
1020
  maxTriesReachedRetentionMs: config.maxTriesReachedRetentionMs,
909
1021
  onMaxTriesReached: config.onMaxTriesReached
910
1022
  };
911
- while (true) {
912
- if (!workerInstance.running) {
913
- logger4.info(`Got signal to stop. Stopping worker [w${workerInstance.workerIndex}]...`);
914
- return;
915
- }
1023
+ logger4.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
1024
+ while (workersInstance.running) {
1025
+ let didMiss = false;
1026
+ let allJobNamesAtCapacity = false;
916
1027
  try {
917
- const didRun = await this.runWorkerLoop(
918
- config,
919
- workersInstance,
920
- workerInstance,
921
- names,
922
- executeConfig
923
- );
924
- if (!didRun) await sleep(config.pollInterval);
925
- if (didRun) await sleep(config.cooldownPeriod);
1028
+ while (workersInstance.activeExecutions.size < config.workersCount) {
1029
+ const availableJobNames = this.getAvailableJobNames(config, workersInstance, jobNames);
1030
+ if (availableJobNames.length === 0) {
1031
+ allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
1032
+ break;
1033
+ }
1034
+ const jobToRun = await this.jobsRepo.getJobAndLock(
1035
+ availableJobNames,
1036
+ config.defaultLockTime
1037
+ );
1038
+ if (!jobToRun) {
1039
+ logger4.debug("No job to run");
1040
+ didMiss = true;
1041
+ break;
1042
+ }
1043
+ this.startExecution(executeConfig, workersInstance, jobToRun);
1044
+ if (!workersInstance.running) break;
1045
+ }
926
1046
  } catch (error) {
927
- logger4.error("Error in job runner.", { error });
928
- await sleep(config.pollInterval);
1047
+ logger4.error("Error in job scheduler.", { error });
1048
+ didMiss = true;
1049
+ }
1050
+ if (!workersInstance.running) return;
1051
+ const concurrencyIsFull = workersInstance.activeExecutions.size >= config.workersCount && workersInstance.activeExecutions.size > 0;
1052
+ if (concurrencyIsFull || allJobNamesAtCapacity) {
1053
+ await this.waitForCapacityOrStop(workersInstance);
1054
+ } else if (didMiss || workersInstance.activeExecutions.size === 0) {
1055
+ await this.waitForPollOrStop(config, workersInstance);
929
1056
  }
930
1057
  }
931
1058
  }
932
1059
  createWorkersInstanceDefinition(config) {
1060
+ let signalStop;
1061
+ const stopSignal = new Promise((resolve) => {
1062
+ signalStop = resolve;
1063
+ });
933
1064
  const workersInstance = {
934
1065
  running: true,
935
1066
  workersCount: config.workersCount,
936
- workers: [],
1067
+ activeExecutions: /* @__PURE__ */ new Map(),
1068
+ staleExecutions: /* @__PURE__ */ new Map(),
937
1069
  runningJobsByName: /* @__PURE__ */ new Map(),
938
- jobAcquisitionLock: Promise.resolve(),
1070
+ schedulerPromise: Promise.resolve(),
1071
+ stopSignal,
1072
+ signalStop,
1073
+ get runningExecutions() {
1074
+ return this.activeExecutions.size;
1075
+ },
939
1076
  stop: async () => {
940
- logger4.info("Stopping workers...");
941
- workersInstance.running = false;
942
- const stopingPromises = workersInstance.workers.map((worker) => worker.stop());
943
- await Promise.all(stopingPromises);
1077
+ if (workersInstance.stopPromise) return workersInstance.stopPromise;
1078
+ workersInstance.stopPromise = (async () => {
1079
+ logger4.info("Stopping job scheduler...");
1080
+ workersInstance.running = false;
1081
+ workersInstance.signalStop();
1082
+ await workersInstance.schedulerPromise;
1083
+ const activeCapacity = Array.from(
1084
+ workersInstance.activeExecutions.values(),
1085
+ (execution) => execution.capacityPromise
1086
+ );
1087
+ await Promise.all(activeCapacity);
1088
+ })();
1089
+ return workersInstance.stopPromise;
944
1090
  }
945
1091
  };
946
1092
  return workersInstance;
@@ -954,44 +1100,17 @@ var WorkerService = class {
954
1100
  })
955
1101
  );
956
1102
  }
957
- async startANewWorker(config, workersInstance, workerIndex = workersInstance.workers.length) {
958
- if (!workersInstance.running) {
959
- return;
960
- }
961
- const workerInstance = {
962
- running: true,
963
- workerIndex,
964
- stop: async () => {
965
- logger4.info(`Stopping worker [w${workerIndex}]...`);
966
- workerInstance.running = false;
967
- await workerInstance.promise;
968
- },
969
- respawn: async () => {
970
- logger4.info(`Respawning worker [w${workerIndex}]...`);
971
- workerInstance.running = false;
972
- await this.startANewWorker(config, workersInstance, workerIndex);
973
- }
974
- };
975
- const workerPromise = this.startWorker(config, workersInstance, workerInstance);
976
- workerInstance.promise = workerPromise;
977
- workersInstance.workers[workerIndex] = workerInstance;
978
- }
979
- async runWorkers(config, workersInstance) {
1103
+ async runScheduler(config, workersInstance) {
980
1104
  logger4.debug("Will ensure maxTriesReached retention and TTL index");
981
- const ensureRetentionPromise = this.jobsRepo.ensureMaxTriesReachedRetention(config.maxTriesReachedRetentionMs).catch((error) => logger4.error("Error ensuring maxTriesReached retention", { error }));
1105
+ void this.jobsRepo.ensureMaxTriesReachedRetention(config.maxTriesReachedRetentionMs).catch((error) => logger4.error("Error ensuring maxTriesReached retention", { error }));
982
1106
  logger4.debug("Will ensure records for recurrent jobs");
983
1107
  await this.ensureRecords(config);
984
- const workersCount = config.workersCount;
985
- const workerWord = workersCount === 1 ? "worker" : "workers";
986
- logger4.info(`Starting ${workersCount} ${workerWord}`);
987
- for (let workerIndex = 0; workerIndex < workersCount; workerIndex++) {
988
- this.startANewWorker(config, workersInstance, workerIndex);
989
- }
990
- await ensureRetentionPromise;
1108
+ logger4.info(`Starting job scheduler with concurrency ${config.workersCount}`);
1109
+ await this.runSchedulerLoop(config, workersInstance);
991
1110
  }
992
1111
  /**
993
- * Starts the job workers with the provided configuration.
994
- * @param userConfig - Configuration for the workers. Required field: jobs
1112
+ * Starts one job scheduler with the provided concurrency configuration.
1113
+ * @param userConfig - Configuration for the scheduler. Required field: jobs
995
1114
  */
996
1115
  startWorkers(userConfig) {
997
1116
  const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
@@ -999,7 +1118,6 @@ var WorkerService = class {
999
1118
  throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
1000
1119
  }
1001
1120
  const config = {
1002
- cooldownPeriod: 100,
1003
1121
  pollInterval: 3e3,
1004
1122
  workersCount: 4,
1005
1123
  defaultLockTime: 30 * 1e3,
@@ -1008,8 +1126,11 @@ var WorkerService = class {
1008
1126
  };
1009
1127
  setNameToJobs(config.jobs);
1010
1128
  const workersInstance = this.createWorkersInstanceDefinition(config);
1011
- logger4.debug("Starting workers", config);
1012
- this.runWorkers(config, workersInstance);
1129
+ logger4.debug("Starting job scheduler", config);
1130
+ workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
1131
+ workersInstance.running = false;
1132
+ logger4.error("Job scheduler stopped after an unexpected error.", { error });
1133
+ });
1013
1134
  return workersInstance;
1014
1135
  }
1015
1136
  };