@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.cjs +277 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +277 -156
- package/dist/index.js.map +1 -1
- package/dist/repos/JobsRepo.d.ts +7 -6
- package/dist/services/Executor.d.ts +5 -5
- package/dist/services/WorkerService.d.ts +30 -13
- package/dist/types/JobRecord.d.ts +8 -0
- package/dist/types/StartConfig.d.ts +4 -3
- package/dist/types/Worker.d.ts +6 -11
- package/package.json +1 -1
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
|
|
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(
|
|
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(
|
|
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({
|
|
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
|
-
|
|
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:
|
|
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(
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
676
|
-
return;
|
|
721
|
+
return scheduleRecurrent();
|
|
677
722
|
}
|
|
678
723
|
if (result.action === "retry") {
|
|
679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
766
|
+
return this.jobsRepo.deleteEventJob(jobToRun.jobId, jobToRun.lockId);
|
|
720
767
|
}
|
|
768
|
+
return false;
|
|
721
769
|
}
|
|
722
|
-
async executeJob(config, jobToRun,
|
|
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(
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
workersInstance.
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
|
887
|
-
|
|
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
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
918
|
-
config,
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
|
928
|
-
|
|
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
|
-
|
|
1067
|
+
activeExecutions: /* @__PURE__ */ new Map(),
|
|
1068
|
+
staleExecutions: /* @__PURE__ */ new Map(),
|
|
937
1069
|
runningJobsByName: /* @__PURE__ */ new Map(),
|
|
938
|
-
|
|
1070
|
+
schedulerPromise: Promise.resolve(),
|
|
1071
|
+
stopSignal,
|
|
1072
|
+
signalStop,
|
|
1073
|
+
get runningExecutions() {
|
|
1074
|
+
return this.activeExecutions.size;
|
|
1075
|
+
},
|
|
939
1076
|
stop: async () => {
|
|
940
|
-
|
|
941
|
-
workersInstance.
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
985
|
-
|
|
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
|
|
994
|
-
* @param userConfig - Configuration for the
|
|
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
|
|
1012
|
-
this.
|
|
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
|
};
|