@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.js
CHANGED
|
@@ -71,15 +71,32 @@ 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
|
/**
|
|
77
82
|
* Status of the job. Optional for backwards compatibility with existing records.
|
|
78
83
|
* Records without this field are treated as 'pending'.
|
|
79
84
|
*/
|
|
80
|
-
status: { type: JobStatusEnum, optional: true }
|
|
85
|
+
status: { type: JobStatusEnum, optional: true },
|
|
86
|
+
/**
|
|
87
|
+
* Date when the job reached its maximum tries. Used as the stable anchor when
|
|
88
|
+
* maxTriesReachedRetentionMs changes between worker restarts.
|
|
89
|
+
*/
|
|
90
|
+
maxTriesReachedAt: { type: "date", optional: true },
|
|
91
|
+
/**
|
|
92
|
+
* Date when MongoDB should remove a terminal event job through the TTL index.
|
|
93
|
+
*/
|
|
94
|
+
expiresAt: { type: "date", optional: true }
|
|
81
95
|
});
|
|
82
96
|
|
|
97
|
+
// src/types/StartConfig.ts
|
|
98
|
+
var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
|
|
99
|
+
|
|
83
100
|
// src/repos/JobsRepo.ts
|
|
84
101
|
var _jobs_dec, _JobsRepo_decorators, _init;
|
|
85
102
|
_JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
|
|
@@ -111,6 +128,13 @@ _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
|
|
|
111
128
|
unique: true,
|
|
112
129
|
sparse: true
|
|
113
130
|
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
keys: {
|
|
134
|
+
expiresAt: 1
|
|
135
|
+
},
|
|
136
|
+
name: "jobs_dogs_records_expires_at_ttl",
|
|
137
|
+
expireAfterSeconds: 0
|
|
114
138
|
}
|
|
115
139
|
]
|
|
116
140
|
})];
|
|
@@ -119,6 +143,7 @@ var JobsRepo = class {
|
|
|
119
143
|
__publicField(this, "jobs", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
|
|
120
144
|
}
|
|
121
145
|
async getJobAndLock(jobNames, lockTime) {
|
|
146
|
+
const executionId = generateId();
|
|
122
147
|
const lockedUntil = new Date(Date.now() + lockTime);
|
|
123
148
|
const job = await this.jobs.findOneAndUpdate(
|
|
124
149
|
{
|
|
@@ -132,7 +157,7 @@ var JobsRepo = class {
|
|
|
132
157
|
]
|
|
133
158
|
},
|
|
134
159
|
{
|
|
135
|
-
$set: { lockedUntil, lastRunAt: /* @__PURE__ */ new Date() },
|
|
160
|
+
$set: { lockId: executionId, lockedUntil, lastRunAt: /* @__PURE__ */ new Date() },
|
|
136
161
|
$inc: { tries: 1 }
|
|
137
162
|
},
|
|
138
163
|
{
|
|
@@ -153,7 +178,8 @@ var JobsRepo = class {
|
|
|
153
178
|
}
|
|
154
179
|
return {
|
|
155
180
|
jobId: job._id,
|
|
156
|
-
executionId
|
|
181
|
+
executionId,
|
|
182
|
+
lockId: executionId,
|
|
157
183
|
name: job.jobName,
|
|
158
184
|
params: job.params,
|
|
159
185
|
type: job.type,
|
|
@@ -164,8 +190,11 @@ var JobsRepo = class {
|
|
|
164
190
|
wasStale
|
|
165
191
|
};
|
|
166
192
|
}
|
|
167
|
-
async setJobRecordPriority(jobId, priority) {
|
|
168
|
-
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;
|
|
169
198
|
}
|
|
170
199
|
async scheduleNextRun(options) {
|
|
171
200
|
const updator = {
|
|
@@ -174,43 +203,89 @@ var JobsRepo = class {
|
|
|
174
203
|
priority: options.priority,
|
|
175
204
|
...options.resetTries ? { tries: 0 } : {}
|
|
176
205
|
},
|
|
177
|
-
$unset: { lockedUntil: "" }
|
|
206
|
+
$unset: { lockId: "", lockedUntil: "" }
|
|
178
207
|
};
|
|
179
|
-
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;
|
|
180
213
|
}
|
|
181
|
-
async deleteEventJob(jobId) {
|
|
182
|
-
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;
|
|
183
221
|
}
|
|
184
222
|
/**
|
|
185
223
|
* Marks an event job as having reached its maximum tries limit.
|
|
186
|
-
* The job
|
|
224
|
+
* The job won't be picked up for execution and MongoDB will delete it after
|
|
225
|
+
* the configured retention period.
|
|
187
226
|
*/
|
|
188
|
-
async markJobAsMaxTriesReached(jobId) {
|
|
189
|
-
|
|
190
|
-
|
|
227
|
+
async markJobAsMaxTriesReached(jobId, retentionMs = DEFAULT_MAX_TRIES_REACHED_RETENTION_MS, lockId) {
|
|
228
|
+
const maxTriesReachedAt = /* @__PURE__ */ new Date();
|
|
229
|
+
const result = await this.jobs.updateOne(
|
|
230
|
+
{ _id: jobId, type: "event", ...lockId ? { lockId } : {} },
|
|
191
231
|
{
|
|
192
|
-
$set: {
|
|
193
|
-
|
|
232
|
+
$set: {
|
|
233
|
+
status: "maxTriesReached",
|
|
234
|
+
maxTriesReachedAt,
|
|
235
|
+
...retentionMs === null ? {} : { expiresAt: new Date(maxTriesReachedAt.getTime() + retentionMs) }
|
|
236
|
+
},
|
|
237
|
+
$unset: {
|
|
238
|
+
lockId: "",
|
|
239
|
+
lockedUntil: "",
|
|
240
|
+
...retentionMs === null ? { expiresAt: "" } : {}
|
|
241
|
+
}
|
|
194
242
|
}
|
|
195
243
|
);
|
|
244
|
+
return result.matchedCount > 0;
|
|
196
245
|
}
|
|
197
|
-
|
|
198
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Applies the current retention configuration to existing maxTriesReached
|
|
248
|
+
* event jobs. The collection definition ensures the TTL index. Legacy records
|
|
249
|
+
* use lastRunAt as their retention anchor when available.
|
|
250
|
+
*/
|
|
251
|
+
async ensureMaxTriesReachedRetention(retentionMs) {
|
|
252
|
+
const selector = {
|
|
253
|
+
type: "event",
|
|
254
|
+
status: "maxTriesReached"
|
|
255
|
+
};
|
|
256
|
+
if (retentionMs === null) {
|
|
257
|
+
await this.jobs.updateMany(selector, { $unset: { expiresAt: "" } });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const now = /* @__PURE__ */ new Date();
|
|
261
|
+
const retentionAnchor = {
|
|
262
|
+
$ifNull: ["$maxTriesReachedAt", { $ifNull: ["$lastRunAt", now] }]
|
|
263
|
+
};
|
|
264
|
+
await this.jobs.rawCollection.updateMany(selector, [
|
|
265
|
+
{
|
|
266
|
+
$set: {
|
|
267
|
+
maxTriesReachedAt: retentionAnchor,
|
|
268
|
+
expiresAt: { $add: [retentionAnchor, retentionMs] }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
async extendLockTime(jobId, extraTime, lockId) {
|
|
274
|
+
return this.updateLockTime(jobId, extraTime, lockId);
|
|
199
275
|
}
|
|
200
276
|
/**
|
|
201
277
|
* Updates the lock time for a job to the specified duration from now.
|
|
202
278
|
* Can be used to both extend or shorten the lock time.
|
|
203
279
|
*/
|
|
204
|
-
async updateLockTime(jobId, lockDuration) {
|
|
280
|
+
async updateLockTime(jobId, lockDuration, lockId) {
|
|
205
281
|
const lockedUntil = new Date(Date.now() + lockDuration);
|
|
206
|
-
await this.jobs.updateOne(
|
|
207
|
-
{
|
|
208
|
-
_id: jobId
|
|
209
|
-
},
|
|
282
|
+
const result = await this.jobs.updateOne(
|
|
283
|
+
{ _id: jobId, ...lockId ? { lockId } : {} },
|
|
210
284
|
{
|
|
211
285
|
$set: { lockedUntil }
|
|
212
286
|
}
|
|
213
287
|
);
|
|
288
|
+
return result.matchedCount > 0;
|
|
214
289
|
}
|
|
215
290
|
async unlockAllJobs() {
|
|
216
291
|
const result = await this.jobs.updateMany(
|
|
@@ -218,7 +293,7 @@ var JobsRepo = class {
|
|
|
218
293
|
lockedUntil: { $exists: true }
|
|
219
294
|
},
|
|
220
295
|
{
|
|
221
|
-
$unset: { lockedUntil: "" }
|
|
296
|
+
$unset: { lockId: "", lockedUntil: "" }
|
|
222
297
|
}
|
|
223
298
|
);
|
|
224
299
|
return result.modifiedCount;
|
|
@@ -524,11 +599,15 @@ var Executor = class {
|
|
|
524
599
|
getEffectiveLockTime(job, jobToRun) {
|
|
525
600
|
return job.lockTime ?? jobToRun.lockTime;
|
|
526
601
|
}
|
|
527
|
-
getContext(job, jobToRun, onStale) {
|
|
602
|
+
getContext(job, jobToRun, onStale, isExecutionActive) {
|
|
528
603
|
var _a;
|
|
529
604
|
const effectiveLockTime = this.getEffectiveLockTime(job, jobToRun);
|
|
530
605
|
let staleTimeout = setTimeout(() => onStale(), effectiveLockTime);
|
|
531
606
|
(_a = staleTimeout.unref) == null ? void 0 : _a.call(staleTimeout);
|
|
607
|
+
const contextLogger = logger3.addMetadata({
|
|
608
|
+
jobName: jobToRun.name,
|
|
609
|
+
jobId: jobToRun.jobId
|
|
610
|
+
});
|
|
532
611
|
return {
|
|
533
612
|
definition: job,
|
|
534
613
|
record: jobToRun,
|
|
@@ -536,15 +615,26 @@ var Executor = class {
|
|
|
536
615
|
clearStaleTimeout: () => clearTimeout(staleTimeout),
|
|
537
616
|
extendLockTime: async (extraTime) => {
|
|
538
617
|
var _a2;
|
|
618
|
+
if (!isExecutionActive()) {
|
|
619
|
+
contextLogger.warn(`Will not extend lock for stale execution "${jobToRun.executionId}"`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
539
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
|
+
}
|
|
540
634
|
staleTimeout = setTimeout(() => onStale(), extraTime);
|
|
541
635
|
(_a2 = staleTimeout.unref) == null ? void 0 : _a2.call(staleTimeout);
|
|
542
|
-
await this.jobsRepo.extendLockTime(jobToRun.jobId, extraTime);
|
|
543
636
|
},
|
|
544
|
-
logger:
|
|
545
|
-
jobName: jobToRun.name,
|
|
546
|
-
jobId: jobToRun.jobId
|
|
547
|
-
})
|
|
637
|
+
logger: contextLogger
|
|
548
638
|
};
|
|
549
639
|
}
|
|
550
640
|
getJobDefinition(jobToRun, jobs) {
|
|
@@ -568,7 +658,7 @@ var Executor = class {
|
|
|
568
658
|
* Handles when a job has reached its maximum retry attempts.
|
|
569
659
|
* Marks the job in the database and invokes the onMaxTriesReached callback when provided.
|
|
570
660
|
*/
|
|
571
|
-
async handleMaxTriesReached(jobToRun, onMaxTriesReached) {
|
|
661
|
+
async handleMaxTriesReached(jobToRun, retentionMs, onMaxTriesReached) {
|
|
572
662
|
const jobLogger = logger3.addMetadata({
|
|
573
663
|
jobName: jobToRun.name,
|
|
574
664
|
jobId: jobToRun.jobId
|
|
@@ -576,8 +666,16 @@ var Executor = class {
|
|
|
576
666
|
jobLogger.warn(
|
|
577
667
|
`Job "${jobToRun.name}" has exceeded max tries (${jobToRun.tries}). Marking as maxTriesReached.`
|
|
578
668
|
);
|
|
579
|
-
await this.jobsRepo.markJobAsMaxTriesReached(
|
|
580
|
-
|
|
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;
|
|
581
679
|
try {
|
|
582
680
|
await onMaxTriesReached(jobToRun);
|
|
583
681
|
} catch (callbackError) {
|
|
@@ -585,30 +683,33 @@ var Executor = class {
|
|
|
585
683
|
error: callbackError
|
|
586
684
|
});
|
|
587
685
|
}
|
|
686
|
+
return true;
|
|
588
687
|
}
|
|
589
688
|
async onError(error, job, jobToRun, context) {
|
|
590
689
|
const scheduleRecurrent = async () => {
|
|
591
690
|
if (job.type === "recurrent") {
|
|
592
|
-
|
|
691
|
+
return this.jobsRepo.scheduleNextRun({
|
|
593
692
|
jobId: jobToRun.jobId,
|
|
594
693
|
nextRunAt: getNextRunDate(job),
|
|
595
694
|
resetTries: true,
|
|
596
|
-
priority: job.priority
|
|
695
|
+
priority: job.priority,
|
|
696
|
+
lockId: jobToRun.lockId
|
|
597
697
|
});
|
|
598
698
|
}
|
|
699
|
+
return true;
|
|
599
700
|
};
|
|
600
701
|
const handleRetry = async (nextRunAt) => {
|
|
601
|
-
|
|
702
|
+
return this.jobsRepo.scheduleNextRun({
|
|
602
703
|
jobId: jobToRun.jobId,
|
|
603
704
|
nextRunAt,
|
|
604
705
|
resetTries: false,
|
|
605
|
-
priority: job.type === "recurrent" ? job.priority : jobToRun.priority
|
|
706
|
+
priority: job.type === "recurrent" ? job.priority : jobToRun.priority,
|
|
707
|
+
lockId: jobToRun.lockId
|
|
606
708
|
});
|
|
607
709
|
};
|
|
608
710
|
if (!job.onError) {
|
|
609
711
|
context.logger.error(`Error executing job "${jobToRun.name}"`, { error });
|
|
610
|
-
|
|
611
|
-
return;
|
|
712
|
+
return scheduleRecurrent();
|
|
612
713
|
}
|
|
613
714
|
context.logger.info(`Error executing job "${jobToRun.name}"`, { error });
|
|
614
715
|
const result = await job.onError(
|
|
@@ -617,12 +718,12 @@ var Executor = class {
|
|
|
617
718
|
context
|
|
618
719
|
);
|
|
619
720
|
if (result.action === "dismiss") {
|
|
620
|
-
|
|
621
|
-
return;
|
|
721
|
+
return scheduleRecurrent();
|
|
622
722
|
}
|
|
623
723
|
if (result.action === "retry") {
|
|
624
|
-
|
|
724
|
+
return handleRetry(getNextRunDate(result));
|
|
625
725
|
}
|
|
726
|
+
return true;
|
|
626
727
|
}
|
|
627
728
|
async saveExecution(options) {
|
|
628
729
|
const { startedAt, status, errorMessage, result, job, jobToRun } = options;
|
|
@@ -652,57 +753,94 @@ var Executor = class {
|
|
|
652
753
|
async afterExecutionSuccess(job, jobToRun, context) {
|
|
653
754
|
if (job.type === "recurrent") {
|
|
654
755
|
context.logger.debug(`Scheduling next run for recurrent job "${jobToRun.name}"`);
|
|
655
|
-
|
|
756
|
+
return this.jobsRepo.scheduleNextRun({
|
|
656
757
|
jobId: jobToRun.jobId,
|
|
657
758
|
nextRunAt: getNextRunDate(job),
|
|
658
759
|
resetTries: true,
|
|
659
|
-
priority: job.priority
|
|
760
|
+
priority: job.priority,
|
|
761
|
+
lockId: jobToRun.lockId
|
|
660
762
|
});
|
|
661
763
|
}
|
|
662
764
|
if (job.type === "event") {
|
|
663
765
|
context.logger.debug(`Removing event job after success "${jobToRun.name}"`);
|
|
664
|
-
|
|
766
|
+
return this.jobsRepo.deleteEventJob(jobToRun.jobId, jobToRun.lockId);
|
|
665
767
|
}
|
|
768
|
+
return false;
|
|
666
769
|
}
|
|
667
|
-
async executeJob(config, jobToRun,
|
|
770
|
+
async executeJob(config, jobToRun, onExecutionStale) {
|
|
668
771
|
const job = this.getJobDefinition(jobToRun, config.jobs);
|
|
669
772
|
if (!job) return;
|
|
670
773
|
if (job.type === "event") {
|
|
671
774
|
const effectiveMaxTries = this.getEffectiveMaxTries(job, config.maxTries);
|
|
672
775
|
if (typeof effectiveMaxTries === "number" && jobToRun.tries > effectiveMaxTries) {
|
|
673
|
-
await this.handleMaxTriesReached(
|
|
776
|
+
await this.handleMaxTriesReached(
|
|
777
|
+
jobToRun,
|
|
778
|
+
config.maxTriesReachedRetentionMs,
|
|
779
|
+
config.onMaxTriesReached
|
|
780
|
+
);
|
|
674
781
|
return;
|
|
675
782
|
}
|
|
676
783
|
}
|
|
677
784
|
const effectiveLockTime = this.getEffectiveLockTime(job, jobToRun);
|
|
678
785
|
if (effectiveLockTime !== jobToRun.lockTime) {
|
|
679
|
-
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
|
+
}
|
|
680
795
|
}
|
|
681
796
|
const tracer = trace.getTracer("orionjs.dogs", "1.0");
|
|
682
797
|
await tracer.startActiveSpan(`job.${jobToRun.name}.${jobToRun.executionId}`, async (span) => {
|
|
683
798
|
try {
|
|
684
799
|
const startedAt = /* @__PURE__ */ new Date();
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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 });
|
|
703
835
|
});
|
|
836
|
+
return staleHandlingPromise;
|
|
704
837
|
};
|
|
705
|
-
|
|
838
|
+
context = this.getContext(
|
|
839
|
+
job,
|
|
840
|
+
jobToRun,
|
|
841
|
+
() => void markAsStale(),
|
|
842
|
+
() => executionStatus === "running"
|
|
843
|
+
);
|
|
706
844
|
const extraContext = {
|
|
707
845
|
controllerType: "job",
|
|
708
846
|
jobName: jobToRun.name,
|
|
@@ -718,6 +856,18 @@ var Executor = class {
|
|
|
718
856
|
});
|
|
719
857
|
const result = await job.resolve(jobToRun.params, context);
|
|
720
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
|
+
}
|
|
721
871
|
void this.saveExecution({
|
|
722
872
|
startedAt,
|
|
723
873
|
status: "success",
|
|
@@ -728,9 +878,21 @@ var Executor = class {
|
|
|
728
878
|
}).catch((error) => {
|
|
729
879
|
context.logger.error("Error saving successful execution history", { error });
|
|
730
880
|
});
|
|
731
|
-
await this.afterExecutionSuccess(job, jobToRun, context);
|
|
732
881
|
} catch (error) {
|
|
733
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
|
+
}
|
|
734
896
|
void this.saveExecution({
|
|
735
897
|
startedAt,
|
|
736
898
|
status: "error",
|
|
@@ -741,7 +903,6 @@ var Executor = class {
|
|
|
741
903
|
}).catch((saveError) => {
|
|
742
904
|
context.logger.error("Error saving failed execution history", { error: saveError });
|
|
743
905
|
});
|
|
744
|
-
await this.onError(error, job, jobToRun, context);
|
|
745
906
|
}
|
|
746
907
|
});
|
|
747
908
|
} catch (error) {
|
|
@@ -801,86 +962,131 @@ var WorkerService = class {
|
|
|
801
962
|
}
|
|
802
963
|
workersInstance.runningJobsByName.set(jobName, currentExecutions - 1);
|
|
803
964
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
workersInstance.
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
return await callback();
|
|
813
|
-
} finally {
|
|
814
|
-
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);
|
|
815
973
|
}
|
|
816
974
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
+
}
|
|
825
1002
|
});
|
|
826
1003
|
}
|
|
827
|
-
async
|
|
828
|
-
|
|
829
|
-
if (!jobToRun) {
|
|
830
|
-
logger4.debug("No job to run");
|
|
831
|
-
return false;
|
|
832
|
-
}
|
|
833
|
-
logger4.debug(`Got job [w${workerInstance.workerIndex}] to run:`, jobToRun);
|
|
834
|
-
try {
|
|
835
|
-
await this.executor.executeJob(executeConfig, jobToRun, workerInstance.respawn);
|
|
836
|
-
} finally {
|
|
837
|
-
this.releaseJobExecution(workersInstance, jobToRun.name);
|
|
838
|
-
}
|
|
839
|
-
return true;
|
|
1004
|
+
async waitForPollOrStop(config, workersInstance) {
|
|
1005
|
+
await Promise.race([sleep(config.pollInterval), workersInstance.stopSignal]);
|
|
840
1006
|
}
|
|
841
|
-
async
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
1007
|
+
async waitForCapacityOrStop(workersInstance) {
|
|
1008
|
+
const capacityPromises = Array.from(
|
|
1009
|
+
workersInstance.activeExecutions.values(),
|
|
1010
|
+
(execution) => execution.capacityPromise
|
|
845
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);
|
|
846
1017
|
const executeConfig = {
|
|
847
1018
|
jobs: config.jobs,
|
|
848
1019
|
maxTries: config.maxTries,
|
|
1020
|
+
maxTriesReachedRetentionMs: config.maxTriesReachedRetentionMs,
|
|
849
1021
|
onMaxTriesReached: config.onMaxTriesReached
|
|
850
1022
|
};
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
1023
|
+
logger4.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
|
|
1024
|
+
while (workersInstance.running) {
|
|
1025
|
+
let didMiss = false;
|
|
1026
|
+
let allJobNamesAtCapacity = false;
|
|
856
1027
|
try {
|
|
857
|
-
|
|
858
|
-
config,
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
+
}
|
|
866
1046
|
} catch (error) {
|
|
867
|
-
logger4.error("Error in job
|
|
868
|
-
|
|
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);
|
|
869
1056
|
}
|
|
870
1057
|
}
|
|
871
1058
|
}
|
|
872
1059
|
createWorkersInstanceDefinition(config) {
|
|
1060
|
+
let signalStop;
|
|
1061
|
+
const stopSignal = new Promise((resolve) => {
|
|
1062
|
+
signalStop = resolve;
|
|
1063
|
+
});
|
|
873
1064
|
const workersInstance = {
|
|
874
1065
|
running: true,
|
|
875
1066
|
workersCount: config.workersCount,
|
|
876
|
-
|
|
1067
|
+
activeExecutions: /* @__PURE__ */ new Map(),
|
|
1068
|
+
staleExecutions: /* @__PURE__ */ new Map(),
|
|
877
1069
|
runningJobsByName: /* @__PURE__ */ new Map(),
|
|
878
|
-
|
|
1070
|
+
schedulerPromise: Promise.resolve(),
|
|
1071
|
+
stopSignal,
|
|
1072
|
+
signalStop,
|
|
1073
|
+
get runningExecutions() {
|
|
1074
|
+
return this.activeExecutions.size;
|
|
1075
|
+
},
|
|
879
1076
|
stop: async () => {
|
|
880
|
-
|
|
881
|
-
workersInstance.
|
|
882
|
-
|
|
883
|
-
|
|
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;
|
|
884
1090
|
}
|
|
885
1091
|
};
|
|
886
1092
|
return workersInstance;
|
|
@@ -894,54 +1100,37 @@ var WorkerService = class {
|
|
|
894
1100
|
})
|
|
895
1101
|
);
|
|
896
1102
|
}
|
|
897
|
-
async
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
}
|
|
901
|
-
const workerInstance = {
|
|
902
|
-
running: true,
|
|
903
|
-
workerIndex,
|
|
904
|
-
stop: async () => {
|
|
905
|
-
logger4.info(`Stopping worker [w${workerIndex}]...`);
|
|
906
|
-
workerInstance.running = false;
|
|
907
|
-
await workerInstance.promise;
|
|
908
|
-
},
|
|
909
|
-
respawn: async () => {
|
|
910
|
-
logger4.info(`Respawning worker [w${workerIndex}]...`);
|
|
911
|
-
workerInstance.running = false;
|
|
912
|
-
await this.startANewWorker(config, workersInstance, workerIndex);
|
|
913
|
-
}
|
|
914
|
-
};
|
|
915
|
-
const workerPromise = this.startWorker(config, workersInstance, workerInstance);
|
|
916
|
-
workerInstance.promise = workerPromise;
|
|
917
|
-
workersInstance.workers[workerIndex] = workerInstance;
|
|
918
|
-
}
|
|
919
|
-
async runWorkers(config, workersInstance) {
|
|
1103
|
+
async runScheduler(config, workersInstance) {
|
|
1104
|
+
logger4.debug("Will ensure maxTriesReached retention and TTL index");
|
|
1105
|
+
void this.jobsRepo.ensureMaxTriesReachedRetention(config.maxTriesReachedRetentionMs).catch((error) => logger4.error("Error ensuring maxTriesReached retention", { error }));
|
|
920
1106
|
logger4.debug("Will ensure records for recurrent jobs");
|
|
921
1107
|
await this.ensureRecords(config);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
logger4.info(`Starting ${workersCount} ${workerWord}`);
|
|
925
|
-
for (let workerIndex = 0; workerIndex < workersCount; workerIndex++) {
|
|
926
|
-
this.startANewWorker(config, workersInstance, workerIndex);
|
|
927
|
-
}
|
|
1108
|
+
logger4.info(`Starting job scheduler with concurrency ${config.workersCount}`);
|
|
1109
|
+
await this.runSchedulerLoop(config, workersInstance);
|
|
928
1110
|
}
|
|
929
1111
|
/**
|
|
930
|
-
* Starts
|
|
931
|
-
* @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
|
|
932
1114
|
*/
|
|
933
1115
|
startWorkers(userConfig) {
|
|
1116
|
+
const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
|
|
1117
|
+
if (maxTriesReachedRetentionMs !== null && (!Number.isFinite(maxTriesReachedRetentionMs) || maxTriesReachedRetentionMs < 0)) {
|
|
1118
|
+
throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
|
|
1119
|
+
}
|
|
934
1120
|
const config = {
|
|
935
|
-
cooldownPeriod: 100,
|
|
936
1121
|
pollInterval: 3e3,
|
|
937
1122
|
workersCount: 4,
|
|
938
1123
|
defaultLockTime: 30 * 1e3,
|
|
939
|
-
...userConfig
|
|
1124
|
+
...userConfig,
|
|
1125
|
+
maxTriesReachedRetentionMs
|
|
940
1126
|
};
|
|
941
1127
|
setNameToJobs(config.jobs);
|
|
942
1128
|
const workersInstance = this.createWorkersInstanceDefinition(config);
|
|
943
|
-
logger4.debug("Starting
|
|
944
|
-
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
|
+
});
|
|
945
1134
|
return workersInstance;
|
|
946
1135
|
}
|
|
947
1136
|
};
|
|
@@ -1107,6 +1296,7 @@ var scheduleJobs = (jobs) => {
|
|
|
1107
1296
|
return eventsService.scheduleJobs(jobs);
|
|
1108
1297
|
};
|
|
1109
1298
|
export {
|
|
1299
|
+
DEFAULT_MAX_TRIES_REACHED_RETENTION_MS,
|
|
1110
1300
|
EventJob,
|
|
1111
1301
|
HistoryRecordSchema,
|
|
1112
1302
|
JobRecordSchema,
|