@orion-js/dogs 4.5.0 → 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 +352 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +351 -161
- package/dist/index.js.map +1 -1
- package/dist/repos/JobsRepo.d.ts +15 -7
- package/dist/services/Executor.d.ts +6 -5
- package/dist/services/WorkerService.d.ts +30 -13
- package/dist/types/JobRecord.d.ts +23 -0
- package/dist/types/StartConfig.d.ts +12 -4
- package/dist/types/Worker.d.ts +6 -11
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -74,6 +74,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
|
|
|
74
74
|
// src/index.ts
|
|
75
75
|
var index_exports = {};
|
|
76
76
|
__export(index_exports, {
|
|
77
|
+
DEFAULT_MAX_TRIES_REACHED_RETENTION_MS: () => DEFAULT_MAX_TRIES_REACHED_RETENTION_MS,
|
|
77
78
|
EventJob: () => EventJob,
|
|
78
79
|
HistoryRecordSchema: () => HistoryRecordSchema,
|
|
79
80
|
JobRecordSchema: () => JobRecordSchema,
|
|
@@ -114,15 +115,32 @@ var JobRecordSchema = (0, import_schema.schemaWithName)("JobRecord", {
|
|
|
114
115
|
nextRunAt: { type: "date" },
|
|
115
116
|
lastRunAt: { type: "date", optional: true },
|
|
116
117
|
lockedUntil: { type: "date", optional: true },
|
|
118
|
+
/**
|
|
119
|
+
* Identifies the execution that currently owns the lock. Mutations from an execution must match
|
|
120
|
+
* this value so a stale execution cannot modify a newer claim.
|
|
121
|
+
*/
|
|
122
|
+
lockId: { type: "string", optional: true },
|
|
117
123
|
tries: { type: "number", optional: true },
|
|
118
124
|
params: { type: "blackbox", optional: true },
|
|
119
125
|
/**
|
|
120
126
|
* Status of the job. Optional for backwards compatibility with existing records.
|
|
121
127
|
* Records without this field are treated as 'pending'.
|
|
122
128
|
*/
|
|
123
|
-
status: { type: JobStatusEnum, optional: true }
|
|
129
|
+
status: { type: JobStatusEnum, optional: true },
|
|
130
|
+
/**
|
|
131
|
+
* Date when the job reached its maximum tries. Used as the stable anchor when
|
|
132
|
+
* maxTriesReachedRetentionMs changes between worker restarts.
|
|
133
|
+
*/
|
|
134
|
+
maxTriesReachedAt: { type: "date", optional: true },
|
|
135
|
+
/**
|
|
136
|
+
* Date when MongoDB should remove a terminal event job through the TTL index.
|
|
137
|
+
*/
|
|
138
|
+
expiresAt: { type: "date", optional: true }
|
|
124
139
|
});
|
|
125
140
|
|
|
141
|
+
// src/types/StartConfig.ts
|
|
142
|
+
var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
|
|
143
|
+
|
|
126
144
|
// src/repos/JobsRepo.ts
|
|
127
145
|
var _jobs_dec, _JobsRepo_decorators, _init;
|
|
128
146
|
_JobsRepo_decorators = [(0, import_mongodb.Repository)()], _jobs_dec = [(0, import_mongodb.MongoCollection)({
|
|
@@ -154,6 +172,13 @@ _JobsRepo_decorators = [(0, import_mongodb.Repository)()], _jobs_dec = [(0, impo
|
|
|
154
172
|
unique: true,
|
|
155
173
|
sparse: true
|
|
156
174
|
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
keys: {
|
|
178
|
+
expiresAt: 1
|
|
179
|
+
},
|
|
180
|
+
name: "jobs_dogs_records_expires_at_ttl",
|
|
181
|
+
expireAfterSeconds: 0
|
|
157
182
|
}
|
|
158
183
|
]
|
|
159
184
|
})];
|
|
@@ -162,6 +187,7 @@ var JobsRepo = class {
|
|
|
162
187
|
__publicField(this, "jobs", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
|
|
163
188
|
}
|
|
164
189
|
async getJobAndLock(jobNames, lockTime) {
|
|
190
|
+
const executionId = (0, import_helpers.generateId)();
|
|
165
191
|
const lockedUntil = new Date(Date.now() + lockTime);
|
|
166
192
|
const job = await this.jobs.findOneAndUpdate(
|
|
167
193
|
{
|
|
@@ -175,7 +201,7 @@ var JobsRepo = class {
|
|
|
175
201
|
]
|
|
176
202
|
},
|
|
177
203
|
{
|
|
178
|
-
$set: { lockedUntil, lastRunAt: /* @__PURE__ */ new Date() },
|
|
204
|
+
$set: { lockId: executionId, lockedUntil, lastRunAt: /* @__PURE__ */ new Date() },
|
|
179
205
|
$inc: { tries: 1 }
|
|
180
206
|
},
|
|
181
207
|
{
|
|
@@ -196,7 +222,8 @@ var JobsRepo = class {
|
|
|
196
222
|
}
|
|
197
223
|
return {
|
|
198
224
|
jobId: job._id,
|
|
199
|
-
executionId
|
|
225
|
+
executionId,
|
|
226
|
+
lockId: executionId,
|
|
200
227
|
name: job.jobName,
|
|
201
228
|
params: job.params,
|
|
202
229
|
type: job.type,
|
|
@@ -207,8 +234,11 @@ var JobsRepo = class {
|
|
|
207
234
|
wasStale
|
|
208
235
|
};
|
|
209
236
|
}
|
|
210
|
-
async setJobRecordPriority(jobId, priority) {
|
|
211
|
-
await this.jobs.updateOne(
|
|
237
|
+
async setJobRecordPriority(jobId, priority, lockId) {
|
|
238
|
+
const result = await this.jobs.updateOne(lockId ? { _id: jobId, lockId } : jobId, {
|
|
239
|
+
$set: { priority }
|
|
240
|
+
});
|
|
241
|
+
return result.matchedCount > 0;
|
|
212
242
|
}
|
|
213
243
|
async scheduleNextRun(options) {
|
|
214
244
|
const updator = {
|
|
@@ -217,43 +247,89 @@ var JobsRepo = class {
|
|
|
217
247
|
priority: options.priority,
|
|
218
248
|
...options.resetTries ? { tries: 0 } : {}
|
|
219
249
|
},
|
|
220
|
-
$unset: { lockedUntil: "" }
|
|
250
|
+
$unset: { lockId: "", lockedUntil: "" }
|
|
221
251
|
};
|
|
222
|
-
await this.jobs.updateOne(
|
|
252
|
+
const result = await this.jobs.updateOne(
|
|
253
|
+
options.lockId ? { _id: options.jobId, lockId: options.lockId } : options.jobId,
|
|
254
|
+
updator
|
|
255
|
+
);
|
|
256
|
+
return result.matchedCount > 0;
|
|
223
257
|
}
|
|
224
|
-
async deleteEventJob(jobId) {
|
|
225
|
-
await this.jobs.deleteOne({
|
|
258
|
+
async deleteEventJob(jobId, lockId) {
|
|
259
|
+
const result = await this.jobs.deleteOne({
|
|
260
|
+
_id: jobId,
|
|
261
|
+
type: "event",
|
|
262
|
+
...lockId ? { lockId } : {}
|
|
263
|
+
});
|
|
264
|
+
return result.deletedCount > 0;
|
|
226
265
|
}
|
|
227
266
|
/**
|
|
228
267
|
* Marks an event job as having reached its maximum tries limit.
|
|
229
|
-
* The job
|
|
268
|
+
* The job won't be picked up for execution and MongoDB will delete it after
|
|
269
|
+
* the configured retention period.
|
|
230
270
|
*/
|
|
231
|
-
async markJobAsMaxTriesReached(jobId) {
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
async markJobAsMaxTriesReached(jobId, retentionMs = DEFAULT_MAX_TRIES_REACHED_RETENTION_MS, lockId) {
|
|
272
|
+
const maxTriesReachedAt = /* @__PURE__ */ new Date();
|
|
273
|
+
const result = await this.jobs.updateOne(
|
|
274
|
+
{ _id: jobId, type: "event", ...lockId ? { lockId } : {} },
|
|
234
275
|
{
|
|
235
|
-
$set: {
|
|
236
|
-
|
|
276
|
+
$set: {
|
|
277
|
+
status: "maxTriesReached",
|
|
278
|
+
maxTriesReachedAt,
|
|
279
|
+
...retentionMs === null ? {} : { expiresAt: new Date(maxTriesReachedAt.getTime() + retentionMs) }
|
|
280
|
+
},
|
|
281
|
+
$unset: {
|
|
282
|
+
lockId: "",
|
|
283
|
+
lockedUntil: "",
|
|
284
|
+
...retentionMs === null ? { expiresAt: "" } : {}
|
|
285
|
+
}
|
|
237
286
|
}
|
|
238
287
|
);
|
|
288
|
+
return result.matchedCount > 0;
|
|
239
289
|
}
|
|
240
|
-
|
|
241
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Applies the current retention configuration to existing maxTriesReached
|
|
292
|
+
* event jobs. The collection definition ensures the TTL index. Legacy records
|
|
293
|
+
* use lastRunAt as their retention anchor when available.
|
|
294
|
+
*/
|
|
295
|
+
async ensureMaxTriesReachedRetention(retentionMs) {
|
|
296
|
+
const selector = {
|
|
297
|
+
type: "event",
|
|
298
|
+
status: "maxTriesReached"
|
|
299
|
+
};
|
|
300
|
+
if (retentionMs === null) {
|
|
301
|
+
await this.jobs.updateMany(selector, { $unset: { expiresAt: "" } });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const now = /* @__PURE__ */ new Date();
|
|
305
|
+
const retentionAnchor = {
|
|
306
|
+
$ifNull: ["$maxTriesReachedAt", { $ifNull: ["$lastRunAt", now] }]
|
|
307
|
+
};
|
|
308
|
+
await this.jobs.rawCollection.updateMany(selector, [
|
|
309
|
+
{
|
|
310
|
+
$set: {
|
|
311
|
+
maxTriesReachedAt: retentionAnchor,
|
|
312
|
+
expiresAt: { $add: [retentionAnchor, retentionMs] }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
]);
|
|
316
|
+
}
|
|
317
|
+
async extendLockTime(jobId, extraTime, lockId) {
|
|
318
|
+
return this.updateLockTime(jobId, extraTime, lockId);
|
|
242
319
|
}
|
|
243
320
|
/**
|
|
244
321
|
* Updates the lock time for a job to the specified duration from now.
|
|
245
322
|
* Can be used to both extend or shorten the lock time.
|
|
246
323
|
*/
|
|
247
|
-
async updateLockTime(jobId, lockDuration) {
|
|
324
|
+
async updateLockTime(jobId, lockDuration, lockId) {
|
|
248
325
|
const lockedUntil = new Date(Date.now() + lockDuration);
|
|
249
|
-
await this.jobs.updateOne(
|
|
250
|
-
{
|
|
251
|
-
_id: jobId
|
|
252
|
-
},
|
|
326
|
+
const result = await this.jobs.updateOne(
|
|
327
|
+
{ _id: jobId, ...lockId ? { lockId } : {} },
|
|
253
328
|
{
|
|
254
329
|
$set: { lockedUntil }
|
|
255
330
|
}
|
|
256
331
|
);
|
|
332
|
+
return result.matchedCount > 0;
|
|
257
333
|
}
|
|
258
334
|
async unlockAllJobs() {
|
|
259
335
|
const result = await this.jobs.updateMany(
|
|
@@ -261,7 +337,7 @@ var JobsRepo = class {
|
|
|
261
337
|
lockedUntil: { $exists: true }
|
|
262
338
|
},
|
|
263
339
|
{
|
|
264
|
-
$unset: { lockedUntil: "" }
|
|
340
|
+
$unset: { lockId: "", lockedUntil: "" }
|
|
265
341
|
}
|
|
266
342
|
);
|
|
267
343
|
return result.modifiedCount;
|
|
@@ -567,11 +643,15 @@ var Executor = class {
|
|
|
567
643
|
getEffectiveLockTime(job, jobToRun) {
|
|
568
644
|
return job.lockTime ?? jobToRun.lockTime;
|
|
569
645
|
}
|
|
570
|
-
getContext(job, jobToRun, onStale) {
|
|
646
|
+
getContext(job, jobToRun, onStale, isExecutionActive) {
|
|
571
647
|
var _a;
|
|
572
648
|
const effectiveLockTime = this.getEffectiveLockTime(job, jobToRun);
|
|
573
649
|
let staleTimeout = setTimeout(() => onStale(), effectiveLockTime);
|
|
574
650
|
(_a = staleTimeout.unref) == null ? void 0 : _a.call(staleTimeout);
|
|
651
|
+
const contextLogger = import_logger3.logger.addMetadata({
|
|
652
|
+
jobName: jobToRun.name,
|
|
653
|
+
jobId: jobToRun.jobId
|
|
654
|
+
});
|
|
575
655
|
return {
|
|
576
656
|
definition: job,
|
|
577
657
|
record: jobToRun,
|
|
@@ -579,15 +659,26 @@ var Executor = class {
|
|
|
579
659
|
clearStaleTimeout: () => clearTimeout(staleTimeout),
|
|
580
660
|
extendLockTime: async (extraTime) => {
|
|
581
661
|
var _a2;
|
|
662
|
+
if (!isExecutionActive()) {
|
|
663
|
+
contextLogger.warn(`Will not extend lock for stale execution "${jobToRun.executionId}"`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
582
666
|
clearTimeout(staleTimeout);
|
|
667
|
+
let didExtend = false;
|
|
668
|
+
try {
|
|
669
|
+
didExtend = await this.jobsRepo.extendLockTime(jobToRun.jobId, extraTime, jobToRun.lockId);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
onStale();
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
if (!didExtend) {
|
|
675
|
+
onStale();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
583
678
|
staleTimeout = setTimeout(() => onStale(), extraTime);
|
|
584
679
|
(_a2 = staleTimeout.unref) == null ? void 0 : _a2.call(staleTimeout);
|
|
585
|
-
await this.jobsRepo.extendLockTime(jobToRun.jobId, extraTime);
|
|
586
680
|
},
|
|
587
|
-
logger:
|
|
588
|
-
jobName: jobToRun.name,
|
|
589
|
-
jobId: jobToRun.jobId
|
|
590
|
-
})
|
|
681
|
+
logger: contextLogger
|
|
591
682
|
};
|
|
592
683
|
}
|
|
593
684
|
getJobDefinition(jobToRun, jobs) {
|
|
@@ -611,7 +702,7 @@ var Executor = class {
|
|
|
611
702
|
* Handles when a job has reached its maximum retry attempts.
|
|
612
703
|
* Marks the job in the database and invokes the onMaxTriesReached callback when provided.
|
|
613
704
|
*/
|
|
614
|
-
async handleMaxTriesReached(jobToRun, onMaxTriesReached) {
|
|
705
|
+
async handleMaxTriesReached(jobToRun, retentionMs, onMaxTriesReached) {
|
|
615
706
|
const jobLogger = import_logger3.logger.addMetadata({
|
|
616
707
|
jobName: jobToRun.name,
|
|
617
708
|
jobId: jobToRun.jobId
|
|
@@ -619,8 +710,16 @@ var Executor = class {
|
|
|
619
710
|
jobLogger.warn(
|
|
620
711
|
`Job "${jobToRun.name}" has exceeded max tries (${jobToRun.tries}). Marking as maxTriesReached.`
|
|
621
712
|
);
|
|
622
|
-
await this.jobsRepo.markJobAsMaxTriesReached(
|
|
623
|
-
|
|
713
|
+
const didMark = await this.jobsRepo.markJobAsMaxTriesReached(
|
|
714
|
+
jobToRun.jobId,
|
|
715
|
+
retentionMs,
|
|
716
|
+
jobToRun.lockId
|
|
717
|
+
);
|
|
718
|
+
if (!didMark) {
|
|
719
|
+
jobLogger.warn(`Could not mark job "${jobToRun.name}" because its lock is no longer owned`);
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
if (!onMaxTriesReached) return true;
|
|
624
723
|
try {
|
|
625
724
|
await onMaxTriesReached(jobToRun);
|
|
626
725
|
} catch (callbackError) {
|
|
@@ -628,30 +727,33 @@ var Executor = class {
|
|
|
628
727
|
error: callbackError
|
|
629
728
|
});
|
|
630
729
|
}
|
|
730
|
+
return true;
|
|
631
731
|
}
|
|
632
732
|
async onError(error, job, jobToRun, context) {
|
|
633
733
|
const scheduleRecurrent = async () => {
|
|
634
734
|
if (job.type === "recurrent") {
|
|
635
|
-
|
|
735
|
+
return this.jobsRepo.scheduleNextRun({
|
|
636
736
|
jobId: jobToRun.jobId,
|
|
637
737
|
nextRunAt: getNextRunDate(job),
|
|
638
738
|
resetTries: true,
|
|
639
|
-
priority: job.priority
|
|
739
|
+
priority: job.priority,
|
|
740
|
+
lockId: jobToRun.lockId
|
|
640
741
|
});
|
|
641
742
|
}
|
|
743
|
+
return true;
|
|
642
744
|
};
|
|
643
745
|
const handleRetry = async (nextRunAt) => {
|
|
644
|
-
|
|
746
|
+
return this.jobsRepo.scheduleNextRun({
|
|
645
747
|
jobId: jobToRun.jobId,
|
|
646
748
|
nextRunAt,
|
|
647
749
|
resetTries: false,
|
|
648
|
-
priority: job.type === "recurrent" ? job.priority : jobToRun.priority
|
|
750
|
+
priority: job.type === "recurrent" ? job.priority : jobToRun.priority,
|
|
751
|
+
lockId: jobToRun.lockId
|
|
649
752
|
});
|
|
650
753
|
};
|
|
651
754
|
if (!job.onError) {
|
|
652
755
|
context.logger.error(`Error executing job "${jobToRun.name}"`, { error });
|
|
653
|
-
|
|
654
|
-
return;
|
|
756
|
+
return scheduleRecurrent();
|
|
655
757
|
}
|
|
656
758
|
context.logger.info(`Error executing job "${jobToRun.name}"`, { error });
|
|
657
759
|
const result = await job.onError(
|
|
@@ -660,12 +762,12 @@ var Executor = class {
|
|
|
660
762
|
context
|
|
661
763
|
);
|
|
662
764
|
if (result.action === "dismiss") {
|
|
663
|
-
|
|
664
|
-
return;
|
|
765
|
+
return scheduleRecurrent();
|
|
665
766
|
}
|
|
666
767
|
if (result.action === "retry") {
|
|
667
|
-
|
|
768
|
+
return handleRetry(getNextRunDate(result));
|
|
668
769
|
}
|
|
770
|
+
return true;
|
|
669
771
|
}
|
|
670
772
|
async saveExecution(options) {
|
|
671
773
|
const { startedAt, status, errorMessage, result, job, jobToRun } = options;
|
|
@@ -695,57 +797,94 @@ var Executor = class {
|
|
|
695
797
|
async afterExecutionSuccess(job, jobToRun, context) {
|
|
696
798
|
if (job.type === "recurrent") {
|
|
697
799
|
context.logger.debug(`Scheduling next run for recurrent job "${jobToRun.name}"`);
|
|
698
|
-
|
|
800
|
+
return this.jobsRepo.scheduleNextRun({
|
|
699
801
|
jobId: jobToRun.jobId,
|
|
700
802
|
nextRunAt: getNextRunDate(job),
|
|
701
803
|
resetTries: true,
|
|
702
|
-
priority: job.priority
|
|
804
|
+
priority: job.priority,
|
|
805
|
+
lockId: jobToRun.lockId
|
|
703
806
|
});
|
|
704
807
|
}
|
|
705
808
|
if (job.type === "event") {
|
|
706
809
|
context.logger.debug(`Removing event job after success "${jobToRun.name}"`);
|
|
707
|
-
|
|
810
|
+
return this.jobsRepo.deleteEventJob(jobToRun.jobId, jobToRun.lockId);
|
|
708
811
|
}
|
|
812
|
+
return false;
|
|
709
813
|
}
|
|
710
|
-
async executeJob(config, jobToRun,
|
|
814
|
+
async executeJob(config, jobToRun, onExecutionStale) {
|
|
711
815
|
const job = this.getJobDefinition(jobToRun, config.jobs);
|
|
712
816
|
if (!job) return;
|
|
713
817
|
if (job.type === "event") {
|
|
714
818
|
const effectiveMaxTries = this.getEffectiveMaxTries(job, config.maxTries);
|
|
715
819
|
if (typeof effectiveMaxTries === "number" && jobToRun.tries > effectiveMaxTries) {
|
|
716
|
-
await this.handleMaxTriesReached(
|
|
820
|
+
await this.handleMaxTriesReached(
|
|
821
|
+
jobToRun,
|
|
822
|
+
config.maxTriesReachedRetentionMs,
|
|
823
|
+
config.onMaxTriesReached
|
|
824
|
+
);
|
|
717
825
|
return;
|
|
718
826
|
}
|
|
719
827
|
}
|
|
720
828
|
const effectiveLockTime = this.getEffectiveLockTime(job, jobToRun);
|
|
721
829
|
if (effectiveLockTime !== jobToRun.lockTime) {
|
|
722
|
-
await this.jobsRepo.updateLockTime(
|
|
830
|
+
const didUpdateLock = await this.jobsRepo.updateLockTime(
|
|
831
|
+
jobToRun.jobId,
|
|
832
|
+
effectiveLockTime,
|
|
833
|
+
jobToRun.lockId
|
|
834
|
+
);
|
|
835
|
+
if (!didUpdateLock) {
|
|
836
|
+
import_logger3.logger.warn(`Will not execute job "${jobToRun.name}" because its lock is no longer owned`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
723
839
|
}
|
|
724
840
|
const tracer = import_api.trace.getTracer("orionjs.dogs", "1.0");
|
|
725
841
|
await tracer.startActiveSpan(`job.${jobToRun.name}.${jobToRun.executionId}`, async (span) => {
|
|
726
842
|
try {
|
|
727
843
|
const startedAt = /* @__PURE__ */ new Date();
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
844
|
+
let executionStatus = "running";
|
|
845
|
+
let staleHandlingPromise;
|
|
846
|
+
let context;
|
|
847
|
+
const markAsStale = () => {
|
|
848
|
+
if (executionStatus === "stale") return staleHandlingPromise;
|
|
849
|
+
executionStatus = "stale";
|
|
850
|
+
context.clearStaleTimeout();
|
|
851
|
+
staleHandlingPromise = (async () => {
|
|
852
|
+
context.logger.warn(`Job "${jobToRun.name}" is stale`);
|
|
853
|
+
if (job.onStale) {
|
|
854
|
+
void Promise.resolve(job.onStale(jobToRun.params, context)).catch((error) => {
|
|
855
|
+
context.logger.error(`Error in onStale callback for job "${jobToRun.name}"`, {
|
|
856
|
+
error
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
await this.jobsRepo.setJobRecordPriority(jobToRun.jobId, 0, jobToRun.lockId);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
context.logger.error(`Error lowering priority for stale job "${jobToRun.name}"`, {
|
|
864
|
+
error
|
|
865
|
+
});
|
|
866
|
+
} finally {
|
|
867
|
+
onExecutionStale();
|
|
868
|
+
}
|
|
869
|
+
await this.saveExecution({
|
|
870
|
+
startedAt,
|
|
871
|
+
status: "stale",
|
|
872
|
+
result: null,
|
|
873
|
+
errorMessage: null,
|
|
874
|
+
job,
|
|
875
|
+
jobToRun
|
|
876
|
+
});
|
|
877
|
+
})().catch((error) => {
|
|
878
|
+
context.logger.error("Error handling stale execution", { error });
|
|
746
879
|
});
|
|
880
|
+
return staleHandlingPromise;
|
|
747
881
|
};
|
|
748
|
-
|
|
882
|
+
context = this.getContext(
|
|
883
|
+
job,
|
|
884
|
+
jobToRun,
|
|
885
|
+
() => void markAsStale(),
|
|
886
|
+
() => executionStatus === "running"
|
|
887
|
+
);
|
|
749
888
|
const extraContext = {
|
|
750
889
|
controllerType: "job",
|
|
751
890
|
jobName: jobToRun.name,
|
|
@@ -761,6 +900,18 @@ var Executor = class {
|
|
|
761
900
|
});
|
|
762
901
|
const result = await job.resolve(jobToRun.params, context);
|
|
763
902
|
context.clearStaleTimeout();
|
|
903
|
+
if (executionStatus === "stale") {
|
|
904
|
+
await staleHandlingPromise;
|
|
905
|
+
context.logger.warn(
|
|
906
|
+
`Ignoring late success from stale execution "${jobToRun.executionId}"`
|
|
907
|
+
);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const stillOwnsJob = await this.afterExecutionSuccess(job, jobToRun, context);
|
|
911
|
+
if (!stillOwnsJob) {
|
|
912
|
+
await markAsStale();
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
764
915
|
void this.saveExecution({
|
|
765
916
|
startedAt,
|
|
766
917
|
status: "success",
|
|
@@ -771,9 +922,21 @@ var Executor = class {
|
|
|
771
922
|
}).catch((error) => {
|
|
772
923
|
context.logger.error("Error saving successful execution history", { error });
|
|
773
924
|
});
|
|
774
|
-
await this.afterExecutionSuccess(job, jobToRun, context);
|
|
775
925
|
} catch (error) {
|
|
776
926
|
context.clearStaleTimeout();
|
|
927
|
+
if (executionStatus === "stale") {
|
|
928
|
+
await staleHandlingPromise;
|
|
929
|
+
context.logger.warn(
|
|
930
|
+
`Ignoring late error from stale execution "${jobToRun.executionId}"`,
|
|
931
|
+
{ error }
|
|
932
|
+
);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const stillOwnsJob = await this.onError(error, job, jobToRun, context);
|
|
936
|
+
if (!stillOwnsJob) {
|
|
937
|
+
await markAsStale();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
777
940
|
void this.saveExecution({
|
|
778
941
|
startedAt,
|
|
779
942
|
status: "error",
|
|
@@ -784,7 +947,6 @@ var Executor = class {
|
|
|
784
947
|
}).catch((saveError) => {
|
|
785
948
|
context.logger.error("Error saving failed execution history", { error: saveError });
|
|
786
949
|
});
|
|
787
|
-
await this.onError(error, job, jobToRun, context);
|
|
788
950
|
}
|
|
789
951
|
});
|
|
790
952
|
} catch (error) {
|
|
@@ -844,86 +1006,131 @@ var WorkerService = class {
|
|
|
844
1006
|
}
|
|
845
1007
|
workersInstance.runningJobsByName.set(jobName, currentExecutions - 1);
|
|
846
1008
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
workersInstance.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
return await callback();
|
|
856
|
-
} finally {
|
|
857
|
-
releaseLock();
|
|
1009
|
+
releaseExecutionCapacity(workersInstance, execution, nextStatus) {
|
|
1010
|
+
if (execution.status !== "running") return;
|
|
1011
|
+
execution.status = nextStatus;
|
|
1012
|
+
workersInstance.activeExecutions.delete(execution.job.executionId);
|
|
1013
|
+
this.releaseJobExecution(workersInstance, execution.job.name);
|
|
1014
|
+
execution.releaseCapacity();
|
|
1015
|
+
if (nextStatus === "stale") {
|
|
1016
|
+
workersInstance.staleExecutions.set(execution.job.executionId, execution);
|
|
858
1017
|
}
|
|
859
1018
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1019
|
+
startExecution(config, workersInstance, jobToRun) {
|
|
1020
|
+
let releaseCapacity;
|
|
1021
|
+
const capacityPromise = new Promise((resolve) => {
|
|
1022
|
+
releaseCapacity = resolve;
|
|
1023
|
+
});
|
|
1024
|
+
const execution = {
|
|
1025
|
+
job: jobToRun,
|
|
1026
|
+
promise: Promise.resolve(),
|
|
1027
|
+
capacityPromise,
|
|
1028
|
+
releaseCapacity,
|
|
1029
|
+
status: "running"
|
|
1030
|
+
};
|
|
1031
|
+
this.reserveJobExecution(workersInstance, jobToRun.name);
|
|
1032
|
+
workersInstance.activeExecutions.set(jobToRun.executionId, execution);
|
|
1033
|
+
import_logger4.logger.debug(`Starting job execution "${jobToRun.executionId}"`, jobToRun);
|
|
1034
|
+
const executionPromise = this.executor.executeJob(config, jobToRun, () => {
|
|
1035
|
+
this.releaseExecutionCapacity(workersInstance, execution, "stale");
|
|
1036
|
+
});
|
|
1037
|
+
execution.promise = executionPromise;
|
|
1038
|
+
void executionPromise.catch((error) => {
|
|
1039
|
+
import_logger4.logger.error(`Unhandled error executing job "${jobToRun.name}"`, { error });
|
|
1040
|
+
}).finally(() => {
|
|
1041
|
+
if (execution.status === "running") {
|
|
1042
|
+
this.releaseExecutionCapacity(workersInstance, execution, "finished");
|
|
1043
|
+
} else if (execution.status === "stale") {
|
|
1044
|
+
workersInstance.staleExecutions.delete(jobToRun.executionId);
|
|
1045
|
+
}
|
|
868
1046
|
});
|
|
869
1047
|
}
|
|
870
|
-
async
|
|
871
|
-
|
|
872
|
-
if (!jobToRun) {
|
|
873
|
-
import_logger4.logger.debug("No job to run");
|
|
874
|
-
return false;
|
|
875
|
-
}
|
|
876
|
-
import_logger4.logger.debug(`Got job [w${workerInstance.workerIndex}] to run:`, jobToRun);
|
|
877
|
-
try {
|
|
878
|
-
await this.executor.executeJob(executeConfig, jobToRun, workerInstance.respawn);
|
|
879
|
-
} finally {
|
|
880
|
-
this.releaseJobExecution(workersInstance, jobToRun.name);
|
|
881
|
-
}
|
|
882
|
-
return true;
|
|
1048
|
+
async waitForPollOrStop(config, workersInstance) {
|
|
1049
|
+
await Promise.race([(0, import_helpers2.sleep)(config.pollInterval), workersInstance.stopSignal]);
|
|
883
1050
|
}
|
|
884
|
-
async
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
1051
|
+
async waitForCapacityOrStop(workersInstance) {
|
|
1052
|
+
const capacityPromises = Array.from(
|
|
1053
|
+
workersInstance.activeExecutions.values(),
|
|
1054
|
+
(execution) => execution.capacityPromise
|
|
888
1055
|
);
|
|
1056
|
+
if (capacityPromises.length === 0) return;
|
|
1057
|
+
await Promise.race([...capacityPromises, workersInstance.stopSignal]);
|
|
1058
|
+
}
|
|
1059
|
+
async runSchedulerLoop(config, workersInstance) {
|
|
1060
|
+
const jobNames = this.getJobNames(config.jobs);
|
|
889
1061
|
const executeConfig = {
|
|
890
1062
|
jobs: config.jobs,
|
|
891
1063
|
maxTries: config.maxTries,
|
|
1064
|
+
maxTriesReachedRetentionMs: config.maxTriesReachedRetentionMs,
|
|
892
1065
|
onMaxTriesReached: config.onMaxTriesReached
|
|
893
1066
|
};
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
1067
|
+
import_logger4.logger.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
|
|
1068
|
+
while (workersInstance.running) {
|
|
1069
|
+
let didMiss = false;
|
|
1070
|
+
let allJobNamesAtCapacity = false;
|
|
899
1071
|
try {
|
|
900
|
-
|
|
901
|
-
config,
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1072
|
+
while (workersInstance.activeExecutions.size < config.workersCount) {
|
|
1073
|
+
const availableJobNames = this.getAvailableJobNames(config, workersInstance, jobNames);
|
|
1074
|
+
if (availableJobNames.length === 0) {
|
|
1075
|
+
allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
const jobToRun = await this.jobsRepo.getJobAndLock(
|
|
1079
|
+
availableJobNames,
|
|
1080
|
+
config.defaultLockTime
|
|
1081
|
+
);
|
|
1082
|
+
if (!jobToRun) {
|
|
1083
|
+
import_logger4.logger.debug("No job to run");
|
|
1084
|
+
didMiss = true;
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
this.startExecution(executeConfig, workersInstance, jobToRun);
|
|
1088
|
+
if (!workersInstance.running) break;
|
|
1089
|
+
}
|
|
909
1090
|
} catch (error) {
|
|
910
|
-
import_logger4.logger.error("Error in job
|
|
911
|
-
|
|
1091
|
+
import_logger4.logger.error("Error in job scheduler.", { error });
|
|
1092
|
+
didMiss = true;
|
|
1093
|
+
}
|
|
1094
|
+
if (!workersInstance.running) return;
|
|
1095
|
+
const concurrencyIsFull = workersInstance.activeExecutions.size >= config.workersCount && workersInstance.activeExecutions.size > 0;
|
|
1096
|
+
if (concurrencyIsFull || allJobNamesAtCapacity) {
|
|
1097
|
+
await this.waitForCapacityOrStop(workersInstance);
|
|
1098
|
+
} else if (didMiss || workersInstance.activeExecutions.size === 0) {
|
|
1099
|
+
await this.waitForPollOrStop(config, workersInstance);
|
|
912
1100
|
}
|
|
913
1101
|
}
|
|
914
1102
|
}
|
|
915
1103
|
createWorkersInstanceDefinition(config) {
|
|
1104
|
+
let signalStop;
|
|
1105
|
+
const stopSignal = new Promise((resolve) => {
|
|
1106
|
+
signalStop = resolve;
|
|
1107
|
+
});
|
|
916
1108
|
const workersInstance = {
|
|
917
1109
|
running: true,
|
|
918
1110
|
workersCount: config.workersCount,
|
|
919
|
-
|
|
1111
|
+
activeExecutions: /* @__PURE__ */ new Map(),
|
|
1112
|
+
staleExecutions: /* @__PURE__ */ new Map(),
|
|
920
1113
|
runningJobsByName: /* @__PURE__ */ new Map(),
|
|
921
|
-
|
|
1114
|
+
schedulerPromise: Promise.resolve(),
|
|
1115
|
+
stopSignal,
|
|
1116
|
+
signalStop,
|
|
1117
|
+
get runningExecutions() {
|
|
1118
|
+
return this.activeExecutions.size;
|
|
1119
|
+
},
|
|
922
1120
|
stop: async () => {
|
|
923
|
-
|
|
924
|
-
workersInstance.
|
|
925
|
-
|
|
926
|
-
|
|
1121
|
+
if (workersInstance.stopPromise) return workersInstance.stopPromise;
|
|
1122
|
+
workersInstance.stopPromise = (async () => {
|
|
1123
|
+
import_logger4.logger.info("Stopping job scheduler...");
|
|
1124
|
+
workersInstance.running = false;
|
|
1125
|
+
workersInstance.signalStop();
|
|
1126
|
+
await workersInstance.schedulerPromise;
|
|
1127
|
+
const activeCapacity = Array.from(
|
|
1128
|
+
workersInstance.activeExecutions.values(),
|
|
1129
|
+
(execution) => execution.capacityPromise
|
|
1130
|
+
);
|
|
1131
|
+
await Promise.all(activeCapacity);
|
|
1132
|
+
})();
|
|
1133
|
+
return workersInstance.stopPromise;
|
|
927
1134
|
}
|
|
928
1135
|
};
|
|
929
1136
|
return workersInstance;
|
|
@@ -937,54 +1144,37 @@ var WorkerService = class {
|
|
|
937
1144
|
})
|
|
938
1145
|
);
|
|
939
1146
|
}
|
|
940
|
-
async
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
}
|
|
944
|
-
const workerInstance = {
|
|
945
|
-
running: true,
|
|
946
|
-
workerIndex,
|
|
947
|
-
stop: async () => {
|
|
948
|
-
import_logger4.logger.info(`Stopping worker [w${workerIndex}]...`);
|
|
949
|
-
workerInstance.running = false;
|
|
950
|
-
await workerInstance.promise;
|
|
951
|
-
},
|
|
952
|
-
respawn: async () => {
|
|
953
|
-
import_logger4.logger.info(`Respawning worker [w${workerIndex}]...`);
|
|
954
|
-
workerInstance.running = false;
|
|
955
|
-
await this.startANewWorker(config, workersInstance, workerIndex);
|
|
956
|
-
}
|
|
957
|
-
};
|
|
958
|
-
const workerPromise = this.startWorker(config, workersInstance, workerInstance);
|
|
959
|
-
workerInstance.promise = workerPromise;
|
|
960
|
-
workersInstance.workers[workerIndex] = workerInstance;
|
|
961
|
-
}
|
|
962
|
-
async runWorkers(config, workersInstance) {
|
|
1147
|
+
async runScheduler(config, workersInstance) {
|
|
1148
|
+
import_logger4.logger.debug("Will ensure maxTriesReached retention and TTL index");
|
|
1149
|
+
void this.jobsRepo.ensureMaxTriesReachedRetention(config.maxTriesReachedRetentionMs).catch((error) => import_logger4.logger.error("Error ensuring maxTriesReached retention", { error }));
|
|
963
1150
|
import_logger4.logger.debug("Will ensure records for recurrent jobs");
|
|
964
1151
|
await this.ensureRecords(config);
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
import_logger4.logger.info(`Starting ${workersCount} ${workerWord}`);
|
|
968
|
-
for (let workerIndex = 0; workerIndex < workersCount; workerIndex++) {
|
|
969
|
-
this.startANewWorker(config, workersInstance, workerIndex);
|
|
970
|
-
}
|
|
1152
|
+
import_logger4.logger.info(`Starting job scheduler with concurrency ${config.workersCount}`);
|
|
1153
|
+
await this.runSchedulerLoop(config, workersInstance);
|
|
971
1154
|
}
|
|
972
1155
|
/**
|
|
973
|
-
* Starts
|
|
974
|
-
* @param userConfig - Configuration for the
|
|
1156
|
+
* Starts one job scheduler with the provided concurrency configuration.
|
|
1157
|
+
* @param userConfig - Configuration for the scheduler. Required field: jobs
|
|
975
1158
|
*/
|
|
976
1159
|
startWorkers(userConfig) {
|
|
1160
|
+
const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
|
|
1161
|
+
if (maxTriesReachedRetentionMs !== null && (!Number.isFinite(maxTriesReachedRetentionMs) || maxTriesReachedRetentionMs < 0)) {
|
|
1162
|
+
throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
|
|
1163
|
+
}
|
|
977
1164
|
const config = {
|
|
978
|
-
cooldownPeriod: 100,
|
|
979
1165
|
pollInterval: 3e3,
|
|
980
1166
|
workersCount: 4,
|
|
981
1167
|
defaultLockTime: 30 * 1e3,
|
|
982
|
-
...userConfig
|
|
1168
|
+
...userConfig,
|
|
1169
|
+
maxTriesReachedRetentionMs
|
|
983
1170
|
};
|
|
984
1171
|
setNameToJobs(config.jobs);
|
|
985
1172
|
const workersInstance = this.createWorkersInstanceDefinition(config);
|
|
986
|
-
import_logger4.logger.debug("Starting
|
|
987
|
-
this.
|
|
1173
|
+
import_logger4.logger.debug("Starting job scheduler", config);
|
|
1174
|
+
workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
|
|
1175
|
+
workersInstance.running = false;
|
|
1176
|
+
import_logger4.logger.error("Job scheduler stopped after an unexpected error.", { error });
|
|
1177
|
+
});
|
|
988
1178
|
return workersInstance;
|
|
989
1179
|
}
|
|
990
1180
|
};
|
|
@@ -1151,6 +1341,7 @@ var scheduleJobs = (jobs) => {
|
|
|
1151
1341
|
};
|
|
1152
1342
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1153
1343
|
0 && (module.exports = {
|
|
1344
|
+
DEFAULT_MAX_TRIES_REACHED_RETENTION_MS,
|
|
1154
1345
|
EventJob,
|
|
1155
1346
|
HistoryRecordSchema,
|
|
1156
1347
|
JobRecordSchema,
|