@orion-js/dogs 4.6.4 → 4.6.6
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 +517 -217
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +507 -207
- package/dist/index.js.map +1 -1
- package/dist/repos/JobsRepo.d.ts +19 -3
- package/dist/schedule.d.ts +4 -0
- package/dist/services/EventsService.d.ts +2 -2
- package/dist/services/WorkerService.d.ts +13 -0
- package/dist/services/partitions.d.ts +3 -0
- package/dist/types/Events.d.ts +1 -0
- package/dist/types/JobRecord.d.ts +15 -0
- package/dist/types/JobsDefinition.d.ts +2 -0
- package/dist/types/StartConfig.d.ts +5 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -48,27 +48,170 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
|
|
|
48
48
|
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
49
49
|
|
|
50
50
|
// src/index.ts
|
|
51
|
-
import { getInstance as
|
|
51
|
+
import { getInstance as getInstance3 } from "@orion-js/services";
|
|
52
52
|
|
|
53
|
-
// src/
|
|
54
|
-
import {
|
|
55
|
-
|
|
53
|
+
// src/repos/JobsHistoryRepo.ts
|
|
54
|
+
import { MongoCollection, Repository } from "@orion-js/mongodb";
|
|
55
|
+
|
|
56
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/isInteger.js
|
|
57
|
+
function _isInteger(n) {
|
|
58
|
+
return n << 0 === n;
|
|
59
|
+
}
|
|
60
|
+
var isInteger = Number.isInteger || _isInteger;
|
|
61
|
+
|
|
62
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/createPath.js
|
|
63
|
+
function createPath(path, delimiter = ".") {
|
|
64
|
+
return typeof path === "string" ? path.split(delimiter).map((x) => isInteger(x) ? Number(x) : x) : path;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/compare.js
|
|
68
|
+
function compare(a, b) {
|
|
69
|
+
return String(a) === String(b);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/includes.js
|
|
73
|
+
function includes(a, list) {
|
|
74
|
+
let index = -1;
|
|
75
|
+
const { length } = list;
|
|
76
|
+
while (++index < length)
|
|
77
|
+
if (compare(list[index], a))
|
|
78
|
+
return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/omit.js
|
|
83
|
+
function omit(propsToOmit, obj) {
|
|
84
|
+
if (arguments.length === 1) return (_obj) => omit(propsToOmit, _obj);
|
|
85
|
+
if (obj === null || obj === void 0)
|
|
86
|
+
return void 0;
|
|
87
|
+
const propsToOmitValue = createPath(propsToOmit, ",");
|
|
88
|
+
const willReturn = {};
|
|
89
|
+
for (const key in obj)
|
|
90
|
+
if (!includes(key, propsToOmitValue))
|
|
91
|
+
willReturn[key] = obj[key];
|
|
92
|
+
return willReturn;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/types/HistoryRecord.ts
|
|
96
|
+
import { schemaWithName } from "@orion-js/schema";
|
|
97
|
+
var HistoryRecordSchema = schemaWithName("HistoryRecord", {
|
|
98
|
+
_id: { type: "string" },
|
|
99
|
+
jobId: { type: "string" },
|
|
100
|
+
executionId: { type: "string" },
|
|
101
|
+
jobName: { type: "string" },
|
|
102
|
+
type: { type: "string" },
|
|
103
|
+
priority: { type: "number" },
|
|
104
|
+
tries: { type: "number" },
|
|
105
|
+
uniqueIdentifier: { type: "string", optional: true },
|
|
106
|
+
startedAt: { type: "date" },
|
|
107
|
+
endedAt: { type: "date" },
|
|
108
|
+
duration: { type: "number" },
|
|
109
|
+
expiresAt: { type: "date", optional: true },
|
|
110
|
+
status: { type: "string", enum: ["success", "error", "stale"] },
|
|
111
|
+
errorMessage: { type: "string", optional: true },
|
|
112
|
+
params: { type: "blackbox", optional: true },
|
|
113
|
+
result: { type: "any", optional: true }
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// src/repos/JobsHistoryRepo.ts
|
|
117
|
+
var _history_dec, _JobsHistoryRepo_decorators, _init;
|
|
118
|
+
_JobsHistoryRepo_decorators = [Repository()], _history_dec = [MongoCollection({
|
|
119
|
+
name: "orionjs.jobs_dogs_history",
|
|
120
|
+
idGeneration: "uuid",
|
|
121
|
+
schema: HistoryRecordSchema,
|
|
122
|
+
indexes: [
|
|
123
|
+
{
|
|
124
|
+
keys: {
|
|
125
|
+
jobName: 1,
|
|
126
|
+
startedAt: 1
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
keys: {
|
|
131
|
+
executionId: 1
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
keys: {
|
|
136
|
+
expiresAt: 1
|
|
137
|
+
},
|
|
138
|
+
options: {
|
|
139
|
+
expireAfterSeconds: 0
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
})];
|
|
144
|
+
var JobsHistoryRepo = class {
|
|
145
|
+
constructor() {
|
|
146
|
+
__publicField(this, "history", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
|
|
147
|
+
}
|
|
148
|
+
async saveExecution(record) {
|
|
149
|
+
await this.history.upsert(
|
|
150
|
+
{ executionId: record.executionId },
|
|
151
|
+
{
|
|
152
|
+
$setOnInsert: {
|
|
153
|
+
status: record.status
|
|
154
|
+
},
|
|
155
|
+
$set: {
|
|
156
|
+
...omit(["status"], record)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
async getExecutions(jobName, limit, skip) {
|
|
162
|
+
const cursor = this.history.find({ jobName }).sort({ startedAt: -1 });
|
|
163
|
+
if (skip) {
|
|
164
|
+
cursor.skip(skip);
|
|
165
|
+
}
|
|
166
|
+
if (limit) {
|
|
167
|
+
cursor.limit(limit);
|
|
168
|
+
}
|
|
169
|
+
return await cursor.toArray();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
_init = __decoratorStart(null);
|
|
173
|
+
__decorateElement(_init, 5, "history", _history_dec, JobsHistoryRepo);
|
|
174
|
+
JobsHistoryRepo = __decorateElement(_init, 0, "JobsHistoryRepo", _JobsHistoryRepo_decorators, JobsHistoryRepo);
|
|
175
|
+
__runInitializers(_init, 1, JobsHistoryRepo);
|
|
56
176
|
|
|
57
177
|
// src/repos/JobsRepo.ts
|
|
58
178
|
import { generateId } from "@orion-js/helpers";
|
|
59
179
|
import { logger } from "@orion-js/logger";
|
|
60
|
-
import { MongoCollection, Repository } from "@orion-js/mongodb";
|
|
180
|
+
import { MongoCollection as MongoCollection2, MongoDB as MongoDB2, Repository as Repository2 } from "@orion-js/mongodb";
|
|
181
|
+
import { clean, validate } from "@orion-js/schema";
|
|
182
|
+
|
|
183
|
+
// src/services/partitions.ts
|
|
184
|
+
function validatePartitionsCount(nPartitions) {
|
|
185
|
+
if (!Number.isInteger(nPartitions) || nPartitions <= 0) {
|
|
186
|
+
throw new Error("nPartitions must be a positive integer");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function getRandomPartition(nPartitions) {
|
|
190
|
+
return Math.floor(Math.random() * nPartitions);
|
|
191
|
+
}
|
|
192
|
+
function createShuffledPartitions(nPartitions) {
|
|
193
|
+
const partitions = Array.from({ length: nPartitions }, (_, partition) => partition);
|
|
194
|
+
for (let index = partitions.length - 1; index > 0; index--) {
|
|
195
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
196
|
+
[partitions[index], partitions[swapIndex]] = [partitions[swapIndex], partitions[index]];
|
|
197
|
+
}
|
|
198
|
+
return partitions;
|
|
199
|
+
}
|
|
61
200
|
|
|
62
201
|
// src/types/JobRecord.ts
|
|
63
|
-
import { createEnum, schemaWithName } from "@orion-js/schema";
|
|
202
|
+
import { createEnum, schemaWithName as schemaWithName2 } from "@orion-js/schema";
|
|
64
203
|
var JobStatusEnum = createEnum("JobStatus", ["pending", "maxTriesReached"]);
|
|
65
|
-
var JobRecordSchema =
|
|
204
|
+
var JobRecordSchema = schemaWithName2("JobRecord", {
|
|
66
205
|
_id: { type: "string" },
|
|
67
206
|
jobName: { type: "string" },
|
|
68
207
|
type: { type: createEnum("JobType", ["recurrent", "event"]) },
|
|
69
208
|
priority: { type: "number" },
|
|
70
209
|
uniqueIdentifier: { type: "string", optional: true },
|
|
71
210
|
nextRunAt: { type: "date" },
|
|
211
|
+
/**
|
|
212
|
+
* Acquisition partition. Optional so records created by previous versions can be reconciled.
|
|
213
|
+
*/
|
|
214
|
+
partition: { type: "number", optional: true },
|
|
72
215
|
lastRunAt: { type: "date", optional: true },
|
|
73
216
|
lockedUntil: { type: "date", optional: true },
|
|
74
217
|
/**
|
|
@@ -76,6 +219,11 @@ var JobRecordSchema = schemaWithName("JobRecord", {
|
|
|
76
219
|
* this value so a stale execution cannot modify a newer claim.
|
|
77
220
|
*/
|
|
78
221
|
lockId: { type: "string", optional: true },
|
|
222
|
+
/**
|
|
223
|
+
* Internal acquisition metadata used when MongoDB supports sorted updateOne. It preserves the
|
|
224
|
+
* pre-update stale state because updateOne intentionally does not return the previous document.
|
|
225
|
+
*/
|
|
226
|
+
claimWasStale: { type: "boolean", optional: true },
|
|
79
227
|
tries: { type: "number", optional: true },
|
|
80
228
|
params: { type: "blackbox", optional: true },
|
|
81
229
|
/**
|
|
@@ -100,23 +248,30 @@ var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
|
|
|
100
248
|
// src/repos/JobsRepo.ts
|
|
101
249
|
var JOB_ACQUISITION_HINTS = {
|
|
102
250
|
byJobName: {
|
|
251
|
+
partition: 1,
|
|
103
252
|
jobName: 1,
|
|
104
253
|
priority: -1,
|
|
105
254
|
nextRunAt: 1
|
|
106
255
|
},
|
|
107
256
|
globalPriority: {
|
|
257
|
+
partition: 1,
|
|
108
258
|
priority: -1,
|
|
109
259
|
nextRunAt: 1
|
|
110
260
|
}
|
|
111
261
|
};
|
|
112
262
|
var INITIAL_JOB_ACQUISITION_HINT = "byJobName";
|
|
113
263
|
var CANDIDATE_JOB_ACQUISITION_HINT = "globalPriority";
|
|
264
|
+
var SORTED_UPDATE_ONE_MIN_WIRE_VERSION = 25;
|
|
265
|
+
var LOCK_ID_INDEX_NAME = "jobs_dogs_records_lock_id";
|
|
266
|
+
var PARTITION_JOB_NAME_INDEX_NAME = "jobs_dogs_records_partition_job_name_priority_next_run";
|
|
267
|
+
var PARTITION_PRIORITY_INDEX_NAME = "jobs_dogs_records_partition_priority_next_run";
|
|
114
268
|
var JOB_ACQUISITION_SORT = {
|
|
115
269
|
priority: -1,
|
|
116
270
|
nextRunAt: 1
|
|
117
271
|
};
|
|
118
|
-
function getJobAcquisitionSelector(jobNames, now) {
|
|
272
|
+
function getJobAcquisitionSelector(jobNames, now, partition) {
|
|
119
273
|
return {
|
|
274
|
+
partition,
|
|
120
275
|
jobName: { $in: jobNames },
|
|
121
276
|
nextRunAt: { $lte: now },
|
|
122
277
|
$and: [
|
|
@@ -127,17 +282,19 @@ function getJobAcquisitionSelector(jobNames, now) {
|
|
|
127
282
|
]
|
|
128
283
|
};
|
|
129
284
|
}
|
|
130
|
-
var _jobs_dec, _JobsRepo_decorators,
|
|
131
|
-
_JobsRepo_decorators = [
|
|
285
|
+
var _jobs_dec, _JobsRepo_decorators, _init2;
|
|
286
|
+
_JobsRepo_decorators = [Repository2()], _jobs_dec = [MongoCollection2({
|
|
132
287
|
idGeneration: "uuid",
|
|
133
288
|
name: "orionjs.jobs_dogs_records",
|
|
134
289
|
schema: JobRecordSchema,
|
|
135
290
|
indexes: [
|
|
136
291
|
{
|
|
137
|
-
keys: JOB_ACQUISITION_HINTS.byJobName
|
|
292
|
+
keys: JOB_ACQUISITION_HINTS.byJobName,
|
|
293
|
+
options: { name: PARTITION_JOB_NAME_INDEX_NAME }
|
|
138
294
|
},
|
|
139
295
|
{
|
|
140
|
-
keys: JOB_ACQUISITION_HINTS.globalPriority
|
|
296
|
+
keys: JOB_ACQUISITION_HINTS.globalPriority,
|
|
297
|
+
options: { name: PARTITION_PRIORITY_INDEX_NAME }
|
|
141
298
|
},
|
|
142
299
|
{
|
|
143
300
|
keys: {
|
|
@@ -157,6 +314,15 @@ _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
|
|
|
157
314
|
sparse: true
|
|
158
315
|
}
|
|
159
316
|
},
|
|
317
|
+
{
|
|
318
|
+
keys: {
|
|
319
|
+
lockId: 1
|
|
320
|
+
},
|
|
321
|
+
options: {
|
|
322
|
+
name: LOCK_ID_INDEX_NAME,
|
|
323
|
+
sparse: true
|
|
324
|
+
}
|
|
325
|
+
},
|
|
160
326
|
{
|
|
161
327
|
keys: {
|
|
162
328
|
expiresAt: 1
|
|
@@ -168,14 +334,55 @@ _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
|
|
|
168
334
|
})];
|
|
169
335
|
var JobsRepo = class {
|
|
170
336
|
constructor() {
|
|
171
|
-
__publicField(this, "jobs", __runInitializers(
|
|
337
|
+
__publicField(this, "jobs", __runInitializers(_init2, 8, this)), __runInitializers(_init2, 11, this);
|
|
338
|
+
__publicField(this, "sortedUpdateOneSupportPromise");
|
|
339
|
+
__publicField(this, "partitionsByJobName", /* @__PURE__ */ new Map());
|
|
340
|
+
}
|
|
341
|
+
configureJobPartitions(jobNames, nPartitions) {
|
|
342
|
+
for (const jobName of jobNames) {
|
|
343
|
+
this.partitionsByJobName.set(jobName, nPartitions);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
getJobPartitionsCount(jobName) {
|
|
347
|
+
return this.partitionsByJobName.get(jobName);
|
|
348
|
+
}
|
|
349
|
+
async supportsSortedUpdateOne() {
|
|
350
|
+
if (!this.sortedUpdateOneSupportPromise) {
|
|
351
|
+
this.sortedUpdateOneSupportPromise = this.detectSortedUpdateOneSupport();
|
|
352
|
+
}
|
|
353
|
+
return this.sortedUpdateOneSupportPromise;
|
|
354
|
+
}
|
|
355
|
+
async detectSortedUpdateOneSupport() {
|
|
356
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
357
|
+
await this.jobs.createIndexesPromise;
|
|
358
|
+
const hello = await rawCollection.db.admin().command({ hello: 1 });
|
|
359
|
+
const supportsSortedUpdateOne = typeof hello.maxWireVersion === "number" && hello.maxWireVersion >= SORTED_UPDATE_ONE_MIN_WIRE_VERSION;
|
|
360
|
+
if (!supportsSortedUpdateOne) {
|
|
361
|
+
logger.info("MongoDB does not support sorted updateOne; using findOneAndUpdate for jobs.");
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
const hasLockIdIndex = await rawCollection.indexExists(LOCK_ID_INDEX_NAME);
|
|
365
|
+
if (!hasLockIdIndex) {
|
|
366
|
+
logger.warn(
|
|
367
|
+
`MongoDB supports sorted updateOne but index "${LOCK_ID_INDEX_NAME}" is unavailable; using findOneAndUpdate for jobs.`
|
|
368
|
+
);
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
logger.info("MongoDB supports sorted updateOne; using it for job acquisition.");
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
async getJobAndLock(jobNames, lockTime, hint = INITIAL_JOB_ACQUISITION_HINT, partition = 0) {
|
|
375
|
+
if (await this.supportsSortedUpdateOne()) {
|
|
376
|
+
return this.getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition);
|
|
377
|
+
}
|
|
378
|
+
return this.getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition);
|
|
172
379
|
}
|
|
173
|
-
async
|
|
380
|
+
async getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition) {
|
|
174
381
|
const executionId = generateId();
|
|
175
382
|
const now = /* @__PURE__ */ new Date();
|
|
176
383
|
const lockedUntil = new Date(now.getTime() + lockTime);
|
|
177
384
|
const job = await this.jobs.findOneAndUpdate(
|
|
178
|
-
getJobAcquisitionSelector(jobNames, now),
|
|
385
|
+
getJobAcquisitionSelector(jobNames, now, partition),
|
|
179
386
|
{
|
|
180
387
|
$set: { lockId: executionId, lockedUntil, lastRunAt: now },
|
|
181
388
|
$inc: { tries: 1 }
|
|
@@ -191,6 +398,37 @@ var JobsRepo = class {
|
|
|
191
398
|
if (!job) return;
|
|
192
399
|
const tries = (job.tries || 0) + 1;
|
|
193
400
|
const wasStale = Boolean(job.lockedUntil);
|
|
401
|
+
return this.toJobToRun(job, executionId, lockTime, tries, wasStale);
|
|
402
|
+
}
|
|
403
|
+
async getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition) {
|
|
404
|
+
const executionId = generateId();
|
|
405
|
+
const now = /* @__PURE__ */ new Date();
|
|
406
|
+
const lockedUntil = new Date(now.getTime() + lockTime);
|
|
407
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
408
|
+
const result = await rawCollection.updateOne(
|
|
409
|
+
getJobAcquisitionSelector(jobNames, now, partition),
|
|
410
|
+
[
|
|
411
|
+
{
|
|
412
|
+
$set: {
|
|
413
|
+
lockId: { $literal: executionId },
|
|
414
|
+
lockedUntil: { $literal: lockedUntil },
|
|
415
|
+
lastRunAt: { $literal: now },
|
|
416
|
+
tries: { $add: [{ $ifNull: ["$tries", 0] }, 1] },
|
|
417
|
+
claimWasStale: { $eq: [{ $type: "$lockedUntil" }, "date"] }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
],
|
|
421
|
+
{
|
|
422
|
+
hint: JOB_ACQUISITION_HINTS[hint],
|
|
423
|
+
sort: JOB_ACQUISITION_SORT
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
if (result.matchedCount === 0) return;
|
|
427
|
+
const job = await rawCollection.findOne({ lockId: executionId });
|
|
428
|
+
if (!job) return;
|
|
429
|
+
return this.toJobToRun(job, executionId, lockTime, job.tries || 0, Boolean(job.claimWasStale));
|
|
430
|
+
}
|
|
431
|
+
toJobToRun(job, executionId, lockTime, tries, wasStale) {
|
|
194
432
|
if (wasStale) {
|
|
195
433
|
logger.info(`Running job "${job.jobName}" that was staled`);
|
|
196
434
|
}
|
|
@@ -208,9 +446,9 @@ var JobsRepo = class {
|
|
|
208
446
|
wasStale
|
|
209
447
|
};
|
|
210
448
|
}
|
|
211
|
-
async getJobAcquisitionExecutionTime(jobNames, hint) {
|
|
449
|
+
async getJobAcquisitionExecutionTime(jobNames, hint, partition = 0) {
|
|
212
450
|
var _a;
|
|
213
|
-
const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date()), {
|
|
451
|
+
const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date(), partition), {
|
|
214
452
|
hint: JOB_ACQUISITION_HINTS[hint],
|
|
215
453
|
maxTimeMS: 1e3,
|
|
216
454
|
readPreference: "primary"
|
|
@@ -234,7 +472,7 @@ var JobsRepo = class {
|
|
|
234
472
|
priority: options.priority,
|
|
235
473
|
...options.resetTries ? { tries: 0 } : {}
|
|
236
474
|
},
|
|
237
|
-
$unset: { lockId: "", lockedUntil: "" }
|
|
475
|
+
$unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
|
|
238
476
|
};
|
|
239
477
|
const result = await this.jobs.updateOne(
|
|
240
478
|
options.lockId ? { _id: options.jobId, lockId: options.lockId } : options.jobId,
|
|
@@ -268,6 +506,7 @@ var JobsRepo = class {
|
|
|
268
506
|
$unset: {
|
|
269
507
|
lockId: "",
|
|
270
508
|
lockedUntil: "",
|
|
509
|
+
claimWasStale: "",
|
|
271
510
|
...retentionMs === null ? { expiresAt: "" } : {}
|
|
272
511
|
}
|
|
273
512
|
}
|
|
@@ -324,12 +563,12 @@ var JobsRepo = class {
|
|
|
324
563
|
lockedUntil: { $exists: true }
|
|
325
564
|
},
|
|
326
565
|
{
|
|
327
|
-
$unset: { lockId: "", lockedUntil: "" }
|
|
566
|
+
$unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
|
|
328
567
|
}
|
|
329
568
|
);
|
|
330
569
|
return result.modifiedCount;
|
|
331
570
|
}
|
|
332
|
-
async ensureJobRecord(job) {
|
|
571
|
+
async ensureJobRecord(job, partition = 0) {
|
|
333
572
|
const result = await this.jobs.upsert(
|
|
334
573
|
{
|
|
335
574
|
jobName: job.name
|
|
@@ -343,7 +582,8 @@ var JobsRepo = class {
|
|
|
343
582
|
status: ""
|
|
344
583
|
},
|
|
345
584
|
$setOnInsert: {
|
|
346
|
-
nextRunAt: /* @__PURE__ */ new Date()
|
|
585
|
+
nextRunAt: /* @__PURE__ */ new Date(),
|
|
586
|
+
partition
|
|
347
587
|
}
|
|
348
588
|
}
|
|
349
589
|
);
|
|
@@ -361,7 +601,8 @@ var JobsRepo = class {
|
|
|
361
601
|
params: options.params,
|
|
362
602
|
nextRunAt: options.nextRunAt,
|
|
363
603
|
priority: options.priority,
|
|
364
|
-
type: "event"
|
|
604
|
+
type: "event",
|
|
605
|
+
...options.partition === void 0 ? {} : { partition: options.partition }
|
|
365
606
|
});
|
|
366
607
|
} catch (error) {
|
|
367
608
|
if (error.isValidationError && Object.values(error.validationErrors).includes("notUnique") && options.uniqueIdentifier) {
|
|
@@ -377,30 +618,75 @@ var JobsRepo = class {
|
|
|
377
618
|
if (jobs.length === 0) {
|
|
378
619
|
return { scheduledCount: 0, skippedCount: 0, errors: [] };
|
|
379
620
|
}
|
|
380
|
-
let scheduledCount = 0;
|
|
381
|
-
let skippedCount = 0;
|
|
382
621
|
const errors = [];
|
|
622
|
+
const preparedJobs = [];
|
|
383
623
|
for (let i = 0; i < jobs.length; i++) {
|
|
384
624
|
const job = jobs[i];
|
|
385
625
|
try {
|
|
386
|
-
await
|
|
626
|
+
const record = await clean(JobRecordSchema, {
|
|
627
|
+
_id: this.jobs.generateId(),
|
|
387
628
|
jobName: job.name,
|
|
388
629
|
uniqueIdentifier: job.uniqueIdentifier,
|
|
389
630
|
params: job.params,
|
|
390
631
|
nextRunAt: job.nextRunAt,
|
|
391
632
|
priority: job.priority,
|
|
392
|
-
type: "event"
|
|
633
|
+
type: "event",
|
|
634
|
+
...job.partition === void 0 ? {} : { partition: job.partition }
|
|
393
635
|
});
|
|
394
|
-
|
|
636
|
+
await validate(JobRecordSchema, record);
|
|
637
|
+
preparedJobs.push({ index: i, job, record });
|
|
395
638
|
} catch (error) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
639
|
+
errors.push({
|
|
640
|
+
index: i,
|
|
641
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
642
|
+
job
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (preparedJobs.length === 0) {
|
|
647
|
+
return { scheduledCount: 0, skippedCount: 0, errors };
|
|
648
|
+
}
|
|
649
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
650
|
+
await this.jobs.createIndexesPromise;
|
|
651
|
+
const operations = preparedJobs.map(
|
|
652
|
+
({ record }) => record.uniqueIdentifier === void 0 ? { insertOne: { document: record } } : {
|
|
653
|
+
updateOne: {
|
|
654
|
+
filter: { uniqueIdentifier: record.uniqueIdentifier },
|
|
655
|
+
update: { $setOnInsert: record },
|
|
656
|
+
upsert: true
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
);
|
|
660
|
+
let scheduledCount = 0;
|
|
661
|
+
let skippedCount = 0;
|
|
662
|
+
try {
|
|
663
|
+
const result = await rawCollection.bulkWrite(operations, { ordered: false });
|
|
664
|
+
scheduledCount = result.insertedCount + result.upsertedCount;
|
|
665
|
+
skippedCount = result.matchedCount;
|
|
666
|
+
} catch (error) {
|
|
667
|
+
const writeErrors = error instanceof MongoDB2.MongoBulkWriteError ? Array.isArray(error.writeErrors) ? error.writeErrors : [error.writeErrors] : [];
|
|
668
|
+
if (!(error instanceof MongoDB2.MongoBulkWriteError) || writeErrors.length === 0) {
|
|
669
|
+
for (const preparedJob of preparedJobs) {
|
|
400
670
|
errors.push({
|
|
401
|
-
index:
|
|
671
|
+
index: preparedJob.index,
|
|
402
672
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
403
|
-
job
|
|
673
|
+
job: preparedJob.job
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
scheduledCount = error.result.insertedCount + error.result.upsertedCount;
|
|
678
|
+
skippedCount = error.result.matchedCount;
|
|
679
|
+
for (const writeError of writeErrors) {
|
|
680
|
+
const preparedJob = preparedJobs[writeError.index];
|
|
681
|
+
if (!preparedJob) continue;
|
|
682
|
+
if (writeError.code === 11e3 && preparedJob.record.uniqueIdentifier !== void 0) {
|
|
683
|
+
skippedCount++;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
errors.push({
|
|
687
|
+
index: preparedJob.index,
|
|
688
|
+
error: new Error(writeError.errmsg),
|
|
689
|
+
job: preparedJob.job
|
|
404
690
|
});
|
|
405
691
|
}
|
|
406
692
|
}
|
|
@@ -414,11 +700,55 @@ var JobsRepo = class {
|
|
|
414
700
|
errors
|
|
415
701
|
};
|
|
416
702
|
}
|
|
703
|
+
async reconcilePartitions(options) {
|
|
704
|
+
if (options.jobNames.length === 0) return 0;
|
|
705
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
706
|
+
await this.jobs.createIndexesPromise;
|
|
707
|
+
const now = /* @__PURE__ */ new Date();
|
|
708
|
+
const invalidPartition = {
|
|
709
|
+
$or: [
|
|
710
|
+
{ partition: { $exists: false } },
|
|
711
|
+
{ partition: { $lt: 0 } },
|
|
712
|
+
{ partition: { $gte: options.nPartitions } }
|
|
713
|
+
]
|
|
714
|
+
};
|
|
715
|
+
const availableLock = {
|
|
716
|
+
$or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: now } }]
|
|
717
|
+
};
|
|
718
|
+
const runnableJob = {
|
|
719
|
+
$or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }]
|
|
720
|
+
};
|
|
721
|
+
const jobs = await rawCollection.find({
|
|
722
|
+
jobName: { $in: options.jobNames },
|
|
723
|
+
$and: [invalidPartition, availableLock, runnableJob]
|
|
724
|
+
}).project({ _id: 1 }).limit(options.batchSize).toArray();
|
|
725
|
+
if (jobs.length === 0) return 0;
|
|
726
|
+
const result = await rawCollection.bulkWrite(
|
|
727
|
+
jobs.map((job) => ({
|
|
728
|
+
updateOne: {
|
|
729
|
+
filter: {
|
|
730
|
+
_id: job._id,
|
|
731
|
+
$and: [invalidPartition, availableLock, runnableJob]
|
|
732
|
+
},
|
|
733
|
+
update: { $set: { partition: getRandomPartition(options.nPartitions) } }
|
|
734
|
+
}
|
|
735
|
+
})),
|
|
736
|
+
{ ordered: false }
|
|
737
|
+
);
|
|
738
|
+
return result.modifiedCount;
|
|
739
|
+
}
|
|
417
740
|
};
|
|
418
|
-
|
|
419
|
-
__decorateElement(
|
|
420
|
-
JobsRepo = __decorateElement(
|
|
421
|
-
__runInitializers(
|
|
741
|
+
_init2 = __decoratorStart(null);
|
|
742
|
+
__decorateElement(_init2, 5, "jobs", _jobs_dec, JobsRepo);
|
|
743
|
+
JobsRepo = __decorateElement(_init2, 0, "JobsRepo", _JobsRepo_decorators, JobsRepo);
|
|
744
|
+
__runInitializers(_init2, 1, JobsRepo);
|
|
745
|
+
|
|
746
|
+
// src/schedule.ts
|
|
747
|
+
import { getInstance } from "@orion-js/services";
|
|
748
|
+
|
|
749
|
+
// src/services/EventsService.ts
|
|
750
|
+
import { logger as logger2 } from "@orion-js/logger";
|
|
751
|
+
import { Inject, Service } from "@orion-js/services";
|
|
422
752
|
|
|
423
753
|
// src/services/getNextRunDate.ts
|
|
424
754
|
import { CronExpressionParser } from "cron-parser";
|
|
@@ -448,38 +778,53 @@ var getNextRunDate = (options) => {
|
|
|
448
778
|
};
|
|
449
779
|
|
|
450
780
|
// src/services/EventsService.ts
|
|
451
|
-
var _jobsRepo_dec, _EventsService_decorators,
|
|
781
|
+
var _jobsRepo_dec, _EventsService_decorators, _init3;
|
|
452
782
|
_EventsService_decorators = [Service()], _jobsRepo_dec = [Inject(() => JobsRepo)];
|
|
453
783
|
var EventsService = class {
|
|
454
784
|
constructor() {
|
|
455
|
-
__publicField(this, "jobsRepo", __runInitializers(
|
|
785
|
+
__publicField(this, "jobsRepo", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
|
|
456
786
|
}
|
|
457
|
-
async scheduleJob(options) {
|
|
787
|
+
async scheduleJob(options, nPartitions) {
|
|
458
788
|
logger2.debug("Scheduling job...", options);
|
|
789
|
+
const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
|
|
459
790
|
await this.jobsRepo.scheduleJob({
|
|
460
791
|
name: options.name,
|
|
461
792
|
priority: options.priority || 100,
|
|
462
793
|
nextRunAt: getNextRunDate(options),
|
|
463
794
|
params: options.params || null,
|
|
464
|
-
uniqueIdentifier: options.uniqueIdentifier
|
|
795
|
+
uniqueIdentifier: options.uniqueIdentifier,
|
|
796
|
+
partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
|
|
465
797
|
});
|
|
466
798
|
}
|
|
467
|
-
async scheduleJobs(jobs) {
|
|
799
|
+
async scheduleJobs(jobs, nPartitions) {
|
|
468
800
|
logger2.debug(`Scheduling ${jobs.length} jobs...`);
|
|
469
|
-
const jobRecords = jobs.map((options) =>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
801
|
+
const jobRecords = jobs.map((options) => {
|
|
802
|
+
const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
|
|
803
|
+
return {
|
|
804
|
+
name: options.name,
|
|
805
|
+
priority: options.priority || 100,
|
|
806
|
+
nextRunAt: getNextRunDate(options),
|
|
807
|
+
params: options.params || null,
|
|
808
|
+
uniqueIdentifier: options.uniqueIdentifier,
|
|
809
|
+
partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
|
|
810
|
+
};
|
|
811
|
+
});
|
|
476
812
|
return await this.jobsRepo.scheduleJobs(jobRecords);
|
|
477
813
|
}
|
|
478
814
|
};
|
|
479
|
-
|
|
480
|
-
__decorateElement(
|
|
481
|
-
EventsService = __decorateElement(
|
|
482
|
-
__runInitializers(
|
|
815
|
+
_init3 = __decoratorStart(null);
|
|
816
|
+
__decorateElement(_init3, 5, "jobsRepo", _jobsRepo_dec, EventsService);
|
|
817
|
+
EventsService = __decorateElement(_init3, 0, "EventsService", _EventsService_decorators, EventsService);
|
|
818
|
+
__runInitializers(_init3, 1, EventsService);
|
|
819
|
+
|
|
820
|
+
// src/schedule.ts
|
|
821
|
+
var eventsService = getInstance(EventsService);
|
|
822
|
+
function scheduleJobInternal(options, nPartitions) {
|
|
823
|
+
return eventsService.scheduleJob(options, nPartitions);
|
|
824
|
+
}
|
|
825
|
+
function scheduleJobsInternal(jobs, nPartitions) {
|
|
826
|
+
return eventsService.scheduleJobs(jobs, nPartitions);
|
|
827
|
+
}
|
|
483
828
|
|
|
484
829
|
// src/services/WorkerService.ts
|
|
485
830
|
import { sleep } from "@orion-js/helpers";
|
|
@@ -490,132 +835,6 @@ import { Inject as Inject3, Service as Service3 } from "@orion-js/services";
|
|
|
490
835
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
491
836
|
import { logger as logger3, runWithOrionAsyncContext, updateOrionAsyncContext } from "@orion-js/logger";
|
|
492
837
|
import { Inject as Inject2, Service as Service2 } from "@orion-js/services";
|
|
493
|
-
|
|
494
|
-
// src/repos/JobsHistoryRepo.ts
|
|
495
|
-
import { MongoCollection as MongoCollection2, Repository as Repository2 } from "@orion-js/mongodb";
|
|
496
|
-
|
|
497
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/isInteger.js
|
|
498
|
-
function _isInteger(n) {
|
|
499
|
-
return n << 0 === n;
|
|
500
|
-
}
|
|
501
|
-
var isInteger = Number.isInteger || _isInteger;
|
|
502
|
-
|
|
503
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/createPath.js
|
|
504
|
-
function createPath(path, delimiter = ".") {
|
|
505
|
-
return typeof path === "string" ? path.split(delimiter).map((x) => isInteger(x) ? Number(x) : x) : path;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/compare.js
|
|
509
|
-
function compare(a, b) {
|
|
510
|
-
return String(a) === String(b);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/includes.js
|
|
514
|
-
function includes(a, list) {
|
|
515
|
-
let index = -1;
|
|
516
|
-
const { length } = list;
|
|
517
|
-
while (++index < length)
|
|
518
|
-
if (compare(list[index], a))
|
|
519
|
-
return true;
|
|
520
|
-
return false;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/omit.js
|
|
524
|
-
function omit(propsToOmit, obj) {
|
|
525
|
-
if (arguments.length === 1) return (_obj) => omit(propsToOmit, _obj);
|
|
526
|
-
if (obj === null || obj === void 0)
|
|
527
|
-
return void 0;
|
|
528
|
-
const propsToOmitValue = createPath(propsToOmit, ",");
|
|
529
|
-
const willReturn = {};
|
|
530
|
-
for (const key in obj)
|
|
531
|
-
if (!includes(key, propsToOmitValue))
|
|
532
|
-
willReturn[key] = obj[key];
|
|
533
|
-
return willReturn;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// src/types/HistoryRecord.ts
|
|
537
|
-
import { schemaWithName as schemaWithName2 } from "@orion-js/schema";
|
|
538
|
-
var HistoryRecordSchema = schemaWithName2("HistoryRecord", {
|
|
539
|
-
_id: { type: "string" },
|
|
540
|
-
jobId: { type: "string" },
|
|
541
|
-
executionId: { type: "string" },
|
|
542
|
-
jobName: { type: "string" },
|
|
543
|
-
type: { type: "string" },
|
|
544
|
-
priority: { type: "number" },
|
|
545
|
-
tries: { type: "number" },
|
|
546
|
-
uniqueIdentifier: { type: "string", optional: true },
|
|
547
|
-
startedAt: { type: "date" },
|
|
548
|
-
endedAt: { type: "date" },
|
|
549
|
-
duration: { type: "number" },
|
|
550
|
-
expiresAt: { type: "date", optional: true },
|
|
551
|
-
status: { type: "string", enum: ["success", "error", "stale"] },
|
|
552
|
-
errorMessage: { type: "string", optional: true },
|
|
553
|
-
params: { type: "blackbox", optional: true },
|
|
554
|
-
result: { type: "any", optional: true }
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// src/repos/JobsHistoryRepo.ts
|
|
558
|
-
var _history_dec, _JobsHistoryRepo_decorators, _init3;
|
|
559
|
-
_JobsHistoryRepo_decorators = [Repository2()], _history_dec = [MongoCollection2({
|
|
560
|
-
name: "orionjs.jobs_dogs_history",
|
|
561
|
-
idGeneration: "uuid",
|
|
562
|
-
schema: HistoryRecordSchema,
|
|
563
|
-
indexes: [
|
|
564
|
-
{
|
|
565
|
-
keys: {
|
|
566
|
-
jobName: 1,
|
|
567
|
-
startedAt: 1
|
|
568
|
-
}
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
keys: {
|
|
572
|
-
executionId: 1
|
|
573
|
-
}
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
keys: {
|
|
577
|
-
expiresAt: 1
|
|
578
|
-
},
|
|
579
|
-
options: {
|
|
580
|
-
expireAfterSeconds: 0
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
]
|
|
584
|
-
})];
|
|
585
|
-
var JobsHistoryRepo = class {
|
|
586
|
-
constructor() {
|
|
587
|
-
__publicField(this, "history", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
|
|
588
|
-
}
|
|
589
|
-
async saveExecution(record) {
|
|
590
|
-
await this.history.upsert(
|
|
591
|
-
{ executionId: record.executionId },
|
|
592
|
-
{
|
|
593
|
-
$setOnInsert: {
|
|
594
|
-
status: record.status
|
|
595
|
-
},
|
|
596
|
-
$set: {
|
|
597
|
-
...omit(["status"], record)
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
async getExecutions(jobName, limit, skip) {
|
|
603
|
-
const cursor = this.history.find({ jobName }).sort({ startedAt: -1 });
|
|
604
|
-
if (skip) {
|
|
605
|
-
cursor.skip(skip);
|
|
606
|
-
}
|
|
607
|
-
if (limit) {
|
|
608
|
-
cursor.limit(limit);
|
|
609
|
-
}
|
|
610
|
-
return await cursor.toArray();
|
|
611
|
-
}
|
|
612
|
-
};
|
|
613
|
-
_init3 = __decoratorStart(null);
|
|
614
|
-
__decorateElement(_init3, 5, "history", _history_dec, JobsHistoryRepo);
|
|
615
|
-
JobsHistoryRepo = __decorateElement(_init3, 0, "JobsHistoryRepo", _JobsHistoryRepo_decorators, JobsHistoryRepo);
|
|
616
|
-
__runInitializers(_init3, 1, JobsHistoryRepo);
|
|
617
|
-
|
|
618
|
-
// src/services/Executor.ts
|
|
619
838
|
var _jobsHistoryRepo_dec, _jobsRepo_dec2, _Executor_decorators, _init4;
|
|
620
839
|
_Executor_decorators = [Service2()], _jobsRepo_dec2 = [Inject2(() => JobsRepo)], _jobsHistoryRepo_dec = [Inject2(() => JobsHistoryRepo)];
|
|
621
840
|
var Executor = class {
|
|
@@ -961,6 +1180,10 @@ _WorkerService_decorators = [Service3()], _jobsRepo_dec3 = [Inject3(() => JobsRe
|
|
|
961
1180
|
var WorkerService = class {
|
|
962
1181
|
constructor() {
|
|
963
1182
|
__publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
|
|
1183
|
+
__publicField(this, "partitionReconcilerIntervalMs", 5 * 60 * 1e3);
|
|
1184
|
+
__publicField(this, "partitionReconcilerJitterMs", 30 * 1e3);
|
|
1185
|
+
__publicField(this, "partitionReconcilerBatchPauseMs", 250);
|
|
1186
|
+
__publicField(this, "partitionReconcilerBatchSize", 500);
|
|
964
1187
|
__publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
|
|
965
1188
|
__publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
|
|
966
1189
|
}
|
|
@@ -1045,6 +1268,15 @@ var WorkerService = class {
|
|
|
1045
1268
|
if (capacityPromises.length === 0) return;
|
|
1046
1269
|
await Promise.race([...capacityPromises, workersInstance.stopSignal]);
|
|
1047
1270
|
}
|
|
1271
|
+
getNextPartition(config, workersInstance) {
|
|
1272
|
+
const partition = workersInstance.partitionOrder[workersInstance.partitionCursor];
|
|
1273
|
+
workersInstance.partitionCursor++;
|
|
1274
|
+
if (workersInstance.partitionCursor >= config.nPartitions) {
|
|
1275
|
+
workersInstance.partitionOrder = createShuffledPartitions(config.nPartitions);
|
|
1276
|
+
workersInstance.partitionCursor = 0;
|
|
1277
|
+
}
|
|
1278
|
+
return partition;
|
|
1279
|
+
}
|
|
1048
1280
|
async runSchedulerLoop(config, workersInstance) {
|
|
1049
1281
|
const jobNames = this.getJobNames(config.jobs);
|
|
1050
1282
|
const executeConfig = {
|
|
@@ -1054,6 +1286,7 @@ var WorkerService = class {
|
|
|
1054
1286
|
onMaxTriesReached: config.onMaxTriesReached
|
|
1055
1287
|
};
|
|
1056
1288
|
logger4.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
|
|
1289
|
+
let consecutivePartitionMisses = 0;
|
|
1057
1290
|
while (workersInstance.running) {
|
|
1058
1291
|
let didMiss = false;
|
|
1059
1292
|
let allJobNamesAtCapacity = false;
|
|
@@ -1065,21 +1298,30 @@ var WorkerService = class {
|
|
|
1065
1298
|
break;
|
|
1066
1299
|
}
|
|
1067
1300
|
const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
|
|
1301
|
+
const partition = this.getNextPartition(config, workersInstance);
|
|
1068
1302
|
const jobToRun = await this.jobsRepo.getJobAndLock(
|
|
1069
1303
|
availableJobNames,
|
|
1070
1304
|
config.defaultLockTime,
|
|
1071
|
-
jobAcquisitionHint
|
|
1305
|
+
jobAcquisitionHint,
|
|
1306
|
+
partition
|
|
1072
1307
|
);
|
|
1073
1308
|
if (!jobToRun) {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1309
|
+
consecutivePartitionMisses++;
|
|
1310
|
+
logger4.debug(`No job to run in partition ${partition}`);
|
|
1311
|
+
if (consecutivePartitionMisses >= config.nPartitions) {
|
|
1312
|
+
consecutivePartitionMisses = 0;
|
|
1313
|
+
didMiss = true;
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
continue;
|
|
1077
1317
|
}
|
|
1318
|
+
consecutivePartitionMisses = 0;
|
|
1078
1319
|
this.startExecution(executeConfig, workersInstance, jobToRun);
|
|
1079
1320
|
if (!workersInstance.running) break;
|
|
1080
1321
|
}
|
|
1081
1322
|
} catch (error) {
|
|
1082
1323
|
logger4.error("Error in job scheduler.", { error });
|
|
1324
|
+
consecutivePartitionMisses = 0;
|
|
1083
1325
|
didMiss = true;
|
|
1084
1326
|
}
|
|
1085
1327
|
if (!workersInstance.running) return;
|
|
@@ -1115,11 +1357,13 @@ var WorkerService = class {
|
|
|
1115
1357
|
workersInstance.jobAcquisitionHintProbeCycle++;
|
|
1116
1358
|
try {
|
|
1117
1359
|
for (let sample = 0; sample < 3; sample++) {
|
|
1360
|
+
const partition = getRandomPartition(config.nPartitions);
|
|
1118
1361
|
for (const hint of [firstHint, secondHint]) {
|
|
1119
1362
|
if (!workersInstance.running) return;
|
|
1120
1363
|
const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
|
|
1121
1364
|
jobNames,
|
|
1122
|
-
hint
|
|
1365
|
+
hint,
|
|
1366
|
+
partition
|
|
1123
1367
|
);
|
|
1124
1368
|
if (!workersInstance.running) return;
|
|
1125
1369
|
byHint[hint].push(executionTimeMillis);
|
|
@@ -1191,6 +1435,44 @@ var WorkerService = class {
|
|
|
1191
1435
|
await this.waitForNextJobAcquisitionHintProbe(workersInstance);
|
|
1192
1436
|
}
|
|
1193
1437
|
}
|
|
1438
|
+
async reconcilePartitionBatch(config) {
|
|
1439
|
+
if (typeof this.jobsRepo.reconcilePartitions !== "function") return 0;
|
|
1440
|
+
try {
|
|
1441
|
+
return await this.jobsRepo.reconcilePartitions({
|
|
1442
|
+
jobNames: this.getJobNames(config.jobs),
|
|
1443
|
+
nPartitions: config.nPartitions,
|
|
1444
|
+
batchSize: this.partitionReconcilerBatchSize
|
|
1445
|
+
});
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
logger4.warn("Job partition reconciliation failed; it will be retried.", { error });
|
|
1448
|
+
return 0;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
async waitForNextPartitionReconcile(workersInstance, delayMs) {
|
|
1452
|
+
await new Promise((resolve) => {
|
|
1453
|
+
let resolved = false;
|
|
1454
|
+
const finishWait = () => {
|
|
1455
|
+
if (resolved) return;
|
|
1456
|
+
resolved = true;
|
|
1457
|
+
clearTimeout(workersInstance.partitionReconcilerTimer);
|
|
1458
|
+
workersInstance.partitionReconcilerTimer = void 0;
|
|
1459
|
+
workersInstance.cancelPartitionReconcilerWait = void 0;
|
|
1460
|
+
resolve();
|
|
1461
|
+
};
|
|
1462
|
+
workersInstance.partitionReconcilerTimer = setTimeout(finishWait, delayMs);
|
|
1463
|
+
workersInstance.cancelPartitionReconcilerWait = finishWait;
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
async runPartitionReconcilerLoop(config, workersInstance, initialReconcilePromise) {
|
|
1467
|
+
let modifiedCount = await initialReconcilePromise;
|
|
1468
|
+
while (workersInstance.running) {
|
|
1469
|
+
const didFillBatch = modifiedCount >= this.partitionReconcilerBatchSize;
|
|
1470
|
+
const delayMs = didFillBatch ? this.partitionReconcilerBatchPauseMs : this.partitionReconcilerIntervalMs + Math.floor(Math.random() * (this.partitionReconcilerJitterMs + 1));
|
|
1471
|
+
await this.waitForNextPartitionReconcile(workersInstance, delayMs);
|
|
1472
|
+
if (!workersInstance.running) return;
|
|
1473
|
+
modifiedCount = await this.reconcilePartitionBatch(config);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1194
1476
|
createWorkersInstanceDefinition(config) {
|
|
1195
1477
|
let signalStop;
|
|
1196
1478
|
const stopSignal = new Promise((resolve) => {
|
|
@@ -1206,7 +1488,10 @@ var WorkerService = class {
|
|
|
1206
1488
|
jobAcquisitionHintProbeCycle: 0,
|
|
1207
1489
|
hasCompletedInitialJobAcquisitionHintProbe: false,
|
|
1208
1490
|
pendingJobAcquisitionHintWins: 0,
|
|
1491
|
+
partitionOrder: createShuffledPartitions(config.nPartitions),
|
|
1492
|
+
partitionCursor: 0,
|
|
1209
1493
|
jobAcquisitionHintProbePromise: Promise.resolve(),
|
|
1494
|
+
partitionReconcilerPromise: Promise.resolve(),
|
|
1210
1495
|
schedulerPromise: Promise.resolve(),
|
|
1211
1496
|
stopSignal,
|
|
1212
1497
|
signalStop,
|
|
@@ -1216,14 +1501,16 @@ var WorkerService = class {
|
|
|
1216
1501
|
stop: async () => {
|
|
1217
1502
|
if (workersInstance.stopPromise) return workersInstance.stopPromise;
|
|
1218
1503
|
workersInstance.stopPromise = (async () => {
|
|
1219
|
-
var _a;
|
|
1504
|
+
var _a, _b;
|
|
1220
1505
|
logger4.info("Stopping job scheduler...");
|
|
1221
1506
|
workersInstance.running = false;
|
|
1222
1507
|
workersInstance.signalStop();
|
|
1223
1508
|
(_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
|
|
1509
|
+
(_b = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b.call(workersInstance);
|
|
1224
1510
|
await Promise.all([
|
|
1225
1511
|
workersInstance.schedulerPromise,
|
|
1226
|
-
workersInstance.jobAcquisitionHintProbePromise
|
|
1512
|
+
workersInstance.jobAcquisitionHintProbePromise,
|
|
1513
|
+
workersInstance.partitionReconcilerPromise
|
|
1227
1514
|
]);
|
|
1228
1515
|
const activeCapacity = Array.from(
|
|
1229
1516
|
workersInstance.activeExecutions.values(),
|
|
@@ -1241,7 +1528,7 @@ var WorkerService = class {
|
|
|
1241
1528
|
await Promise.all(
|
|
1242
1529
|
jobs.filter((job) => job.type === "recurrent").map(async (job) => {
|
|
1243
1530
|
logger4.debug(`Ensuring records for job "${job.name}"...`);
|
|
1244
|
-
await this.jobsRepo.ensureJobRecord(job);
|
|
1531
|
+
await this.jobsRepo.ensureJobRecord(job, getRandomPartition(config.nPartitions));
|
|
1245
1532
|
})
|
|
1246
1533
|
);
|
|
1247
1534
|
}
|
|
@@ -1258,29 +1545,41 @@ var WorkerService = class {
|
|
|
1258
1545
|
* @param userConfig - Configuration for the scheduler. Required field: jobs
|
|
1259
1546
|
*/
|
|
1260
1547
|
startWorkers(userConfig) {
|
|
1548
|
+
var _a, _b;
|
|
1261
1549
|
const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
|
|
1262
1550
|
if (maxTriesReachedRetentionMs !== null && (!Number.isFinite(maxTriesReachedRetentionMs) || maxTriesReachedRetentionMs < 0)) {
|
|
1263
1551
|
throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
|
|
1264
1552
|
}
|
|
1553
|
+
const nPartitions = userConfig.nPartitions ?? 1;
|
|
1554
|
+
validatePartitionsCount(nPartitions);
|
|
1265
1555
|
const config = {
|
|
1266
1556
|
pollInterval: 3e3,
|
|
1267
1557
|
workersCount: 4,
|
|
1268
1558
|
defaultLockTime: 30 * 1e3,
|
|
1269
1559
|
...userConfig,
|
|
1560
|
+
nPartitions,
|
|
1270
1561
|
maxTriesReachedRetentionMs
|
|
1271
1562
|
};
|
|
1272
|
-
setNameToJobs(config.jobs);
|
|
1563
|
+
setNameToJobs(config.jobs, config.nPartitions);
|
|
1564
|
+
(_b = (_a = this.jobsRepo).configureJobPartitions) == null ? void 0 : _b.call(_a, this.getJobNames(config.jobs), config.nPartitions);
|
|
1273
1565
|
const workersInstance = this.createWorkersInstanceDefinition(config);
|
|
1274
1566
|
logger4.debug("Starting job scheduler", config);
|
|
1275
1567
|
workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
|
|
1276
1568
|
config,
|
|
1277
1569
|
workersInstance
|
|
1278
1570
|
);
|
|
1571
|
+
const initialReconcilePromise = this.reconcilePartitionBatch(config);
|
|
1572
|
+
workersInstance.partitionReconcilerPromise = this.runPartitionReconcilerLoop(
|
|
1573
|
+
config,
|
|
1574
|
+
workersInstance,
|
|
1575
|
+
initialReconcilePromise
|
|
1576
|
+
);
|
|
1279
1577
|
workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
|
|
1280
|
-
var
|
|
1578
|
+
var _a2, _b2;
|
|
1281
1579
|
workersInstance.running = false;
|
|
1282
1580
|
workersInstance.signalStop();
|
|
1283
|
-
(
|
|
1581
|
+
(_a2 = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a2.call(workersInstance);
|
|
1582
|
+
(_b2 = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b2.call(workersInstance);
|
|
1284
1583
|
logger4.error("Job scheduler stopped after an unexpected error.", { error });
|
|
1285
1584
|
});
|
|
1286
1585
|
return workersInstance;
|
|
@@ -1291,15 +1590,13 @@ __decorateElement(_init5, 5, "jobsRepo", _jobsRepo_dec3, WorkerService);
|
|
|
1291
1590
|
__decorateElement(_init5, 5, "executor", _executor_dec, WorkerService);
|
|
1292
1591
|
WorkerService = __decorateElement(_init5, 0, "WorkerService", _WorkerService_decorators, WorkerService);
|
|
1293
1592
|
__runInitializers(_init5, 1, WorkerService);
|
|
1294
|
-
function setNameToJobs(jobs) {
|
|
1593
|
+
function setNameToJobs(jobs, nPartitions) {
|
|
1295
1594
|
for (const name of Object.keys(jobs)) {
|
|
1296
1595
|
jobs[name].jobName = name;
|
|
1596
|
+
jobs[name].nPartitions = nPartitions;
|
|
1297
1597
|
}
|
|
1298
1598
|
}
|
|
1299
1599
|
|
|
1300
|
-
// src/service/index.ts
|
|
1301
|
-
import { getInstance, Service as Service4 } from "@orion-js/services";
|
|
1302
|
-
|
|
1303
1600
|
// src/defineJob/index.ts
|
|
1304
1601
|
import { cleanAndValidate } from "@orion-js/schema";
|
|
1305
1602
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
@@ -1316,11 +1613,14 @@ function createEventJob(options) {
|
|
|
1316
1613
|
throw new Error("This job has not been registered in the workers");
|
|
1317
1614
|
}
|
|
1318
1615
|
const params = jobDefinition.params ? await cleanAndValidate(jobDefinition.params, scheduleOptions.params) : scheduleOptions.params;
|
|
1319
|
-
return await
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1616
|
+
return await scheduleJobInternal(
|
|
1617
|
+
{
|
|
1618
|
+
...scheduleOptions,
|
|
1619
|
+
name: jobDefinition.jobName,
|
|
1620
|
+
params
|
|
1621
|
+
},
|
|
1622
|
+
jobDefinition.nPartitions
|
|
1623
|
+
);
|
|
1324
1624
|
};
|
|
1325
1625
|
jobDefinition.scheduleJobs = async (jobs) => {
|
|
1326
1626
|
if (!jobDefinition.jobName) {
|
|
@@ -1336,7 +1636,7 @@ function createEventJob(options) {
|
|
|
1336
1636
|
};
|
|
1337
1637
|
})
|
|
1338
1638
|
);
|
|
1339
|
-
return await
|
|
1639
|
+
return await scheduleJobsInternal(processedJobs, jobDefinition.nPartitions);
|
|
1340
1640
|
};
|
|
1341
1641
|
return jobDefinition;
|
|
1342
1642
|
}
|
|
@@ -1365,6 +1665,7 @@ var defineJob = (options) => {
|
|
|
1365
1665
|
};
|
|
1366
1666
|
|
|
1367
1667
|
// src/service/index.ts
|
|
1668
|
+
import { getInstance as getInstance2, Service as Service4 } from "@orion-js/services";
|
|
1368
1669
|
var serviceMetadata = /* @__PURE__ */ new WeakMap();
|
|
1369
1670
|
var jobsMetadata = /* @__PURE__ */ new Map();
|
|
1370
1671
|
var jobEntriesByClass = /* @__PURE__ */ new Map();
|
|
@@ -1420,7 +1721,7 @@ function initializeJobsIfNeeded(instance) {
|
|
|
1420
1721
|
jobsMetadata.set(instance, jobs);
|
|
1421
1722
|
}
|
|
1422
1723
|
function getServiceJobs(target) {
|
|
1423
|
-
const instance =
|
|
1724
|
+
const instance = getInstance2(target);
|
|
1424
1725
|
if (!serviceMetadata.has(instance.constructor)) {
|
|
1425
1726
|
throw new Error("You must pass a class decorated with @Jobs to getServiceJobs");
|
|
1426
1727
|
}
|
|
@@ -1434,18 +1735,17 @@ function getServiceJobs(target) {
|
|
|
1434
1735
|
}
|
|
1435
1736
|
|
|
1436
1737
|
// src/index.ts
|
|
1437
|
-
var workerService =
|
|
1438
|
-
var
|
|
1439
|
-
var
|
|
1440
|
-
var jobsRepo = getInstance2(JobsRepo);
|
|
1738
|
+
var workerService = getInstance3(WorkerService);
|
|
1739
|
+
var jobsHistoryRepo = getInstance3(JobsHistoryRepo);
|
|
1740
|
+
var jobsRepo = getInstance3(JobsRepo);
|
|
1441
1741
|
var startWorkers = (config) => {
|
|
1442
1742
|
return workerService.startWorkers(config);
|
|
1443
1743
|
};
|
|
1444
1744
|
var scheduleJob = (options) => {
|
|
1445
|
-
return
|
|
1745
|
+
return scheduleJobInternal(options);
|
|
1446
1746
|
};
|
|
1447
1747
|
var scheduleJobs = (jobs) => {
|
|
1448
|
-
return
|
|
1748
|
+
return scheduleJobsInternal(jobs);
|
|
1449
1749
|
};
|
|
1450
1750
|
export {
|
|
1451
1751
|
DEFAULT_MAX_TRIES_REACHED_RETENTION_MS,
|