@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.cjs
CHANGED
|
@@ -92,27 +92,170 @@ __export(index_exports, {
|
|
|
92
92
|
startWorkers: () => startWorkers
|
|
93
93
|
});
|
|
94
94
|
module.exports = __toCommonJS(index_exports);
|
|
95
|
-
var
|
|
95
|
+
var import_services6 = require("@orion-js/services");
|
|
96
96
|
|
|
97
|
-
// src/
|
|
98
|
-
var
|
|
99
|
-
|
|
97
|
+
// src/repos/JobsHistoryRepo.ts
|
|
98
|
+
var import_mongodb = require("@orion-js/mongodb");
|
|
99
|
+
|
|
100
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/isInteger.js
|
|
101
|
+
function _isInteger(n) {
|
|
102
|
+
return n << 0 === n;
|
|
103
|
+
}
|
|
104
|
+
var isInteger = Number.isInteger || _isInteger;
|
|
105
|
+
|
|
106
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/createPath.js
|
|
107
|
+
function createPath(path, delimiter = ".") {
|
|
108
|
+
return typeof path === "string" ? path.split(delimiter).map((x) => isInteger(x) ? Number(x) : x) : path;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/compare.js
|
|
112
|
+
function compare(a, b) {
|
|
113
|
+
return String(a) === String(b);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/includes.js
|
|
117
|
+
function includes(a, list) {
|
|
118
|
+
let index = -1;
|
|
119
|
+
const { length } = list;
|
|
120
|
+
while (++index < length)
|
|
121
|
+
if (compare(list[index], a))
|
|
122
|
+
return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/omit.js
|
|
127
|
+
function omit(propsToOmit, obj) {
|
|
128
|
+
if (arguments.length === 1) return (_obj) => omit(propsToOmit, _obj);
|
|
129
|
+
if (obj === null || obj === void 0)
|
|
130
|
+
return void 0;
|
|
131
|
+
const propsToOmitValue = createPath(propsToOmit, ",");
|
|
132
|
+
const willReturn = {};
|
|
133
|
+
for (const key in obj)
|
|
134
|
+
if (!includes(key, propsToOmitValue))
|
|
135
|
+
willReturn[key] = obj[key];
|
|
136
|
+
return willReturn;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/types/HistoryRecord.ts
|
|
140
|
+
var import_schema = require("@orion-js/schema");
|
|
141
|
+
var HistoryRecordSchema = (0, import_schema.schemaWithName)("HistoryRecord", {
|
|
142
|
+
_id: { type: "string" },
|
|
143
|
+
jobId: { type: "string" },
|
|
144
|
+
executionId: { type: "string" },
|
|
145
|
+
jobName: { type: "string" },
|
|
146
|
+
type: { type: "string" },
|
|
147
|
+
priority: { type: "number" },
|
|
148
|
+
tries: { type: "number" },
|
|
149
|
+
uniqueIdentifier: { type: "string", optional: true },
|
|
150
|
+
startedAt: { type: "date" },
|
|
151
|
+
endedAt: { type: "date" },
|
|
152
|
+
duration: { type: "number" },
|
|
153
|
+
expiresAt: { type: "date", optional: true },
|
|
154
|
+
status: { type: "string", enum: ["success", "error", "stale"] },
|
|
155
|
+
errorMessage: { type: "string", optional: true },
|
|
156
|
+
params: { type: "blackbox", optional: true },
|
|
157
|
+
result: { type: "any", optional: true }
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// src/repos/JobsHistoryRepo.ts
|
|
161
|
+
var _history_dec, _JobsHistoryRepo_decorators, _init;
|
|
162
|
+
_JobsHistoryRepo_decorators = [(0, import_mongodb.Repository)()], _history_dec = [(0, import_mongodb.MongoCollection)({
|
|
163
|
+
name: "orionjs.jobs_dogs_history",
|
|
164
|
+
idGeneration: "uuid",
|
|
165
|
+
schema: HistoryRecordSchema,
|
|
166
|
+
indexes: [
|
|
167
|
+
{
|
|
168
|
+
keys: {
|
|
169
|
+
jobName: 1,
|
|
170
|
+
startedAt: 1
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
keys: {
|
|
175
|
+
executionId: 1
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
keys: {
|
|
180
|
+
expiresAt: 1
|
|
181
|
+
},
|
|
182
|
+
options: {
|
|
183
|
+
expireAfterSeconds: 0
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
})];
|
|
188
|
+
var JobsHistoryRepo = class {
|
|
189
|
+
constructor() {
|
|
190
|
+
__publicField(this, "history", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
|
|
191
|
+
}
|
|
192
|
+
async saveExecution(record) {
|
|
193
|
+
await this.history.upsert(
|
|
194
|
+
{ executionId: record.executionId },
|
|
195
|
+
{
|
|
196
|
+
$setOnInsert: {
|
|
197
|
+
status: record.status
|
|
198
|
+
},
|
|
199
|
+
$set: {
|
|
200
|
+
...omit(["status"], record)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
async getExecutions(jobName, limit, skip) {
|
|
206
|
+
const cursor = this.history.find({ jobName }).sort({ startedAt: -1 });
|
|
207
|
+
if (skip) {
|
|
208
|
+
cursor.skip(skip);
|
|
209
|
+
}
|
|
210
|
+
if (limit) {
|
|
211
|
+
cursor.limit(limit);
|
|
212
|
+
}
|
|
213
|
+
return await cursor.toArray();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
_init = __decoratorStart(null);
|
|
217
|
+
__decorateElement(_init, 5, "history", _history_dec, JobsHistoryRepo);
|
|
218
|
+
JobsHistoryRepo = __decorateElement(_init, 0, "JobsHistoryRepo", _JobsHistoryRepo_decorators, JobsHistoryRepo);
|
|
219
|
+
__runInitializers(_init, 1, JobsHistoryRepo);
|
|
100
220
|
|
|
101
221
|
// src/repos/JobsRepo.ts
|
|
102
222
|
var import_helpers = require("@orion-js/helpers");
|
|
103
223
|
var import_logger = require("@orion-js/logger");
|
|
104
|
-
var
|
|
224
|
+
var import_mongodb2 = require("@orion-js/mongodb");
|
|
225
|
+
var import_schema3 = require("@orion-js/schema");
|
|
226
|
+
|
|
227
|
+
// src/services/partitions.ts
|
|
228
|
+
function validatePartitionsCount(nPartitions) {
|
|
229
|
+
if (!Number.isInteger(nPartitions) || nPartitions <= 0) {
|
|
230
|
+
throw new Error("nPartitions must be a positive integer");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function getRandomPartition(nPartitions) {
|
|
234
|
+
return Math.floor(Math.random() * nPartitions);
|
|
235
|
+
}
|
|
236
|
+
function createShuffledPartitions(nPartitions) {
|
|
237
|
+
const partitions = Array.from({ length: nPartitions }, (_, partition) => partition);
|
|
238
|
+
for (let index = partitions.length - 1; index > 0; index--) {
|
|
239
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
240
|
+
[partitions[index], partitions[swapIndex]] = [partitions[swapIndex], partitions[index]];
|
|
241
|
+
}
|
|
242
|
+
return partitions;
|
|
243
|
+
}
|
|
105
244
|
|
|
106
245
|
// src/types/JobRecord.ts
|
|
107
|
-
var
|
|
108
|
-
var JobStatusEnum = (0,
|
|
109
|
-
var JobRecordSchema = (0,
|
|
246
|
+
var import_schema2 = require("@orion-js/schema");
|
|
247
|
+
var JobStatusEnum = (0, import_schema2.createEnum)("JobStatus", ["pending", "maxTriesReached"]);
|
|
248
|
+
var JobRecordSchema = (0, import_schema2.schemaWithName)("JobRecord", {
|
|
110
249
|
_id: { type: "string" },
|
|
111
250
|
jobName: { type: "string" },
|
|
112
|
-
type: { type: (0,
|
|
251
|
+
type: { type: (0, import_schema2.createEnum)("JobType", ["recurrent", "event"]) },
|
|
113
252
|
priority: { type: "number" },
|
|
114
253
|
uniqueIdentifier: { type: "string", optional: true },
|
|
115
254
|
nextRunAt: { type: "date" },
|
|
255
|
+
/**
|
|
256
|
+
* Acquisition partition. Optional so records created by previous versions can be reconciled.
|
|
257
|
+
*/
|
|
258
|
+
partition: { type: "number", optional: true },
|
|
116
259
|
lastRunAt: { type: "date", optional: true },
|
|
117
260
|
lockedUntil: { type: "date", optional: true },
|
|
118
261
|
/**
|
|
@@ -120,6 +263,11 @@ var JobRecordSchema = (0, import_schema.schemaWithName)("JobRecord", {
|
|
|
120
263
|
* this value so a stale execution cannot modify a newer claim.
|
|
121
264
|
*/
|
|
122
265
|
lockId: { type: "string", optional: true },
|
|
266
|
+
/**
|
|
267
|
+
* Internal acquisition metadata used when MongoDB supports sorted updateOne. It preserves the
|
|
268
|
+
* pre-update stale state because updateOne intentionally does not return the previous document.
|
|
269
|
+
*/
|
|
270
|
+
claimWasStale: { type: "boolean", optional: true },
|
|
123
271
|
tries: { type: "number", optional: true },
|
|
124
272
|
params: { type: "blackbox", optional: true },
|
|
125
273
|
/**
|
|
@@ -144,23 +292,30 @@ var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
|
|
|
144
292
|
// src/repos/JobsRepo.ts
|
|
145
293
|
var JOB_ACQUISITION_HINTS = {
|
|
146
294
|
byJobName: {
|
|
295
|
+
partition: 1,
|
|
147
296
|
jobName: 1,
|
|
148
297
|
priority: -1,
|
|
149
298
|
nextRunAt: 1
|
|
150
299
|
},
|
|
151
300
|
globalPriority: {
|
|
301
|
+
partition: 1,
|
|
152
302
|
priority: -1,
|
|
153
303
|
nextRunAt: 1
|
|
154
304
|
}
|
|
155
305
|
};
|
|
156
306
|
var INITIAL_JOB_ACQUISITION_HINT = "byJobName";
|
|
157
307
|
var CANDIDATE_JOB_ACQUISITION_HINT = "globalPriority";
|
|
308
|
+
var SORTED_UPDATE_ONE_MIN_WIRE_VERSION = 25;
|
|
309
|
+
var LOCK_ID_INDEX_NAME = "jobs_dogs_records_lock_id";
|
|
310
|
+
var PARTITION_JOB_NAME_INDEX_NAME = "jobs_dogs_records_partition_job_name_priority_next_run";
|
|
311
|
+
var PARTITION_PRIORITY_INDEX_NAME = "jobs_dogs_records_partition_priority_next_run";
|
|
158
312
|
var JOB_ACQUISITION_SORT = {
|
|
159
313
|
priority: -1,
|
|
160
314
|
nextRunAt: 1
|
|
161
315
|
};
|
|
162
|
-
function getJobAcquisitionSelector(jobNames, now) {
|
|
316
|
+
function getJobAcquisitionSelector(jobNames, now, partition) {
|
|
163
317
|
return {
|
|
318
|
+
partition,
|
|
164
319
|
jobName: { $in: jobNames },
|
|
165
320
|
nextRunAt: { $lte: now },
|
|
166
321
|
$and: [
|
|
@@ -171,17 +326,19 @@ function getJobAcquisitionSelector(jobNames, now) {
|
|
|
171
326
|
]
|
|
172
327
|
};
|
|
173
328
|
}
|
|
174
|
-
var _jobs_dec, _JobsRepo_decorators,
|
|
175
|
-
_JobsRepo_decorators = [(0,
|
|
329
|
+
var _jobs_dec, _JobsRepo_decorators, _init2;
|
|
330
|
+
_JobsRepo_decorators = [(0, import_mongodb2.Repository)()], _jobs_dec = [(0, import_mongodb2.MongoCollection)({
|
|
176
331
|
idGeneration: "uuid",
|
|
177
332
|
name: "orionjs.jobs_dogs_records",
|
|
178
333
|
schema: JobRecordSchema,
|
|
179
334
|
indexes: [
|
|
180
335
|
{
|
|
181
|
-
keys: JOB_ACQUISITION_HINTS.byJobName
|
|
336
|
+
keys: JOB_ACQUISITION_HINTS.byJobName,
|
|
337
|
+
options: { name: PARTITION_JOB_NAME_INDEX_NAME }
|
|
182
338
|
},
|
|
183
339
|
{
|
|
184
|
-
keys: JOB_ACQUISITION_HINTS.globalPriority
|
|
340
|
+
keys: JOB_ACQUISITION_HINTS.globalPriority,
|
|
341
|
+
options: { name: PARTITION_PRIORITY_INDEX_NAME }
|
|
185
342
|
},
|
|
186
343
|
{
|
|
187
344
|
keys: {
|
|
@@ -201,6 +358,15 @@ _JobsRepo_decorators = [(0, import_mongodb.Repository)()], _jobs_dec = [(0, impo
|
|
|
201
358
|
sparse: true
|
|
202
359
|
}
|
|
203
360
|
},
|
|
361
|
+
{
|
|
362
|
+
keys: {
|
|
363
|
+
lockId: 1
|
|
364
|
+
},
|
|
365
|
+
options: {
|
|
366
|
+
name: LOCK_ID_INDEX_NAME,
|
|
367
|
+
sparse: true
|
|
368
|
+
}
|
|
369
|
+
},
|
|
204
370
|
{
|
|
205
371
|
keys: {
|
|
206
372
|
expiresAt: 1
|
|
@@ -212,14 +378,55 @@ _JobsRepo_decorators = [(0, import_mongodb.Repository)()], _jobs_dec = [(0, impo
|
|
|
212
378
|
})];
|
|
213
379
|
var JobsRepo = class {
|
|
214
380
|
constructor() {
|
|
215
|
-
__publicField(this, "jobs", __runInitializers(
|
|
381
|
+
__publicField(this, "jobs", __runInitializers(_init2, 8, this)), __runInitializers(_init2, 11, this);
|
|
382
|
+
__publicField(this, "sortedUpdateOneSupportPromise");
|
|
383
|
+
__publicField(this, "partitionsByJobName", /* @__PURE__ */ new Map());
|
|
384
|
+
}
|
|
385
|
+
configureJobPartitions(jobNames, nPartitions) {
|
|
386
|
+
for (const jobName of jobNames) {
|
|
387
|
+
this.partitionsByJobName.set(jobName, nPartitions);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
getJobPartitionsCount(jobName) {
|
|
391
|
+
return this.partitionsByJobName.get(jobName);
|
|
216
392
|
}
|
|
217
|
-
async
|
|
393
|
+
async supportsSortedUpdateOne() {
|
|
394
|
+
if (!this.sortedUpdateOneSupportPromise) {
|
|
395
|
+
this.sortedUpdateOneSupportPromise = this.detectSortedUpdateOneSupport();
|
|
396
|
+
}
|
|
397
|
+
return this.sortedUpdateOneSupportPromise;
|
|
398
|
+
}
|
|
399
|
+
async detectSortedUpdateOneSupport() {
|
|
400
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
401
|
+
await this.jobs.createIndexesPromise;
|
|
402
|
+
const hello = await rawCollection.db.admin().command({ hello: 1 });
|
|
403
|
+
const supportsSortedUpdateOne = typeof hello.maxWireVersion === "number" && hello.maxWireVersion >= SORTED_UPDATE_ONE_MIN_WIRE_VERSION;
|
|
404
|
+
if (!supportsSortedUpdateOne) {
|
|
405
|
+
import_logger.logger.info("MongoDB does not support sorted updateOne; using findOneAndUpdate for jobs.");
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
const hasLockIdIndex = await rawCollection.indexExists(LOCK_ID_INDEX_NAME);
|
|
409
|
+
if (!hasLockIdIndex) {
|
|
410
|
+
import_logger.logger.warn(
|
|
411
|
+
`MongoDB supports sorted updateOne but index "${LOCK_ID_INDEX_NAME}" is unavailable; using findOneAndUpdate for jobs.`
|
|
412
|
+
);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
import_logger.logger.info("MongoDB supports sorted updateOne; using it for job acquisition.");
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
async getJobAndLock(jobNames, lockTime, hint = INITIAL_JOB_ACQUISITION_HINT, partition = 0) {
|
|
419
|
+
if (await this.supportsSortedUpdateOne()) {
|
|
420
|
+
return this.getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition);
|
|
421
|
+
}
|
|
422
|
+
return this.getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition);
|
|
423
|
+
}
|
|
424
|
+
async getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition) {
|
|
218
425
|
const executionId = (0, import_helpers.generateId)();
|
|
219
426
|
const now = /* @__PURE__ */ new Date();
|
|
220
427
|
const lockedUntil = new Date(now.getTime() + lockTime);
|
|
221
428
|
const job = await this.jobs.findOneAndUpdate(
|
|
222
|
-
getJobAcquisitionSelector(jobNames, now),
|
|
429
|
+
getJobAcquisitionSelector(jobNames, now, partition),
|
|
223
430
|
{
|
|
224
431
|
$set: { lockId: executionId, lockedUntil, lastRunAt: now },
|
|
225
432
|
$inc: { tries: 1 }
|
|
@@ -235,6 +442,37 @@ var JobsRepo = class {
|
|
|
235
442
|
if (!job) return;
|
|
236
443
|
const tries = (job.tries || 0) + 1;
|
|
237
444
|
const wasStale = Boolean(job.lockedUntil);
|
|
445
|
+
return this.toJobToRun(job, executionId, lockTime, tries, wasStale);
|
|
446
|
+
}
|
|
447
|
+
async getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition) {
|
|
448
|
+
const executionId = (0, import_helpers.generateId)();
|
|
449
|
+
const now = /* @__PURE__ */ new Date();
|
|
450
|
+
const lockedUntil = new Date(now.getTime() + lockTime);
|
|
451
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
452
|
+
const result = await rawCollection.updateOne(
|
|
453
|
+
getJobAcquisitionSelector(jobNames, now, partition),
|
|
454
|
+
[
|
|
455
|
+
{
|
|
456
|
+
$set: {
|
|
457
|
+
lockId: { $literal: executionId },
|
|
458
|
+
lockedUntil: { $literal: lockedUntil },
|
|
459
|
+
lastRunAt: { $literal: now },
|
|
460
|
+
tries: { $add: [{ $ifNull: ["$tries", 0] }, 1] },
|
|
461
|
+
claimWasStale: { $eq: [{ $type: "$lockedUntil" }, "date"] }
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
],
|
|
465
|
+
{
|
|
466
|
+
hint: JOB_ACQUISITION_HINTS[hint],
|
|
467
|
+
sort: JOB_ACQUISITION_SORT
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
if (result.matchedCount === 0) return;
|
|
471
|
+
const job = await rawCollection.findOne({ lockId: executionId });
|
|
472
|
+
if (!job) return;
|
|
473
|
+
return this.toJobToRun(job, executionId, lockTime, job.tries || 0, Boolean(job.claimWasStale));
|
|
474
|
+
}
|
|
475
|
+
toJobToRun(job, executionId, lockTime, tries, wasStale) {
|
|
238
476
|
if (wasStale) {
|
|
239
477
|
import_logger.logger.info(`Running job "${job.jobName}" that was staled`);
|
|
240
478
|
}
|
|
@@ -252,9 +490,9 @@ var JobsRepo = class {
|
|
|
252
490
|
wasStale
|
|
253
491
|
};
|
|
254
492
|
}
|
|
255
|
-
async getJobAcquisitionExecutionTime(jobNames, hint) {
|
|
493
|
+
async getJobAcquisitionExecutionTime(jobNames, hint, partition = 0) {
|
|
256
494
|
var _a;
|
|
257
|
-
const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date()), {
|
|
495
|
+
const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date(), partition), {
|
|
258
496
|
hint: JOB_ACQUISITION_HINTS[hint],
|
|
259
497
|
maxTimeMS: 1e3,
|
|
260
498
|
readPreference: "primary"
|
|
@@ -278,7 +516,7 @@ var JobsRepo = class {
|
|
|
278
516
|
priority: options.priority,
|
|
279
517
|
...options.resetTries ? { tries: 0 } : {}
|
|
280
518
|
},
|
|
281
|
-
$unset: { lockId: "", lockedUntil: "" }
|
|
519
|
+
$unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
|
|
282
520
|
};
|
|
283
521
|
const result = await this.jobs.updateOne(
|
|
284
522
|
options.lockId ? { _id: options.jobId, lockId: options.lockId } : options.jobId,
|
|
@@ -312,6 +550,7 @@ var JobsRepo = class {
|
|
|
312
550
|
$unset: {
|
|
313
551
|
lockId: "",
|
|
314
552
|
lockedUntil: "",
|
|
553
|
+
claimWasStale: "",
|
|
315
554
|
...retentionMs === null ? { expiresAt: "" } : {}
|
|
316
555
|
}
|
|
317
556
|
}
|
|
@@ -368,12 +607,12 @@ var JobsRepo = class {
|
|
|
368
607
|
lockedUntil: { $exists: true }
|
|
369
608
|
},
|
|
370
609
|
{
|
|
371
|
-
$unset: { lockId: "", lockedUntil: "" }
|
|
610
|
+
$unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
|
|
372
611
|
}
|
|
373
612
|
);
|
|
374
613
|
return result.modifiedCount;
|
|
375
614
|
}
|
|
376
|
-
async ensureJobRecord(job) {
|
|
615
|
+
async ensureJobRecord(job, partition = 0) {
|
|
377
616
|
const result = await this.jobs.upsert(
|
|
378
617
|
{
|
|
379
618
|
jobName: job.name
|
|
@@ -387,7 +626,8 @@ var JobsRepo = class {
|
|
|
387
626
|
status: ""
|
|
388
627
|
},
|
|
389
628
|
$setOnInsert: {
|
|
390
|
-
nextRunAt: /* @__PURE__ */ new Date()
|
|
629
|
+
nextRunAt: /* @__PURE__ */ new Date(),
|
|
630
|
+
partition
|
|
391
631
|
}
|
|
392
632
|
}
|
|
393
633
|
);
|
|
@@ -405,7 +645,8 @@ var JobsRepo = class {
|
|
|
405
645
|
params: options.params,
|
|
406
646
|
nextRunAt: options.nextRunAt,
|
|
407
647
|
priority: options.priority,
|
|
408
|
-
type: "event"
|
|
648
|
+
type: "event",
|
|
649
|
+
...options.partition === void 0 ? {} : { partition: options.partition }
|
|
409
650
|
});
|
|
410
651
|
} catch (error) {
|
|
411
652
|
if (error.isValidationError && Object.values(error.validationErrors).includes("notUnique") && options.uniqueIdentifier) {
|
|
@@ -421,30 +662,75 @@ var JobsRepo = class {
|
|
|
421
662
|
if (jobs.length === 0) {
|
|
422
663
|
return { scheduledCount: 0, skippedCount: 0, errors: [] };
|
|
423
664
|
}
|
|
424
|
-
let scheduledCount = 0;
|
|
425
|
-
let skippedCount = 0;
|
|
426
665
|
const errors = [];
|
|
666
|
+
const preparedJobs = [];
|
|
427
667
|
for (let i = 0; i < jobs.length; i++) {
|
|
428
668
|
const job = jobs[i];
|
|
429
669
|
try {
|
|
430
|
-
await
|
|
670
|
+
const record = await (0, import_schema3.clean)(JobRecordSchema, {
|
|
671
|
+
_id: this.jobs.generateId(),
|
|
431
672
|
jobName: job.name,
|
|
432
673
|
uniqueIdentifier: job.uniqueIdentifier,
|
|
433
674
|
params: job.params,
|
|
434
675
|
nextRunAt: job.nextRunAt,
|
|
435
676
|
priority: job.priority,
|
|
436
|
-
type: "event"
|
|
677
|
+
type: "event",
|
|
678
|
+
...job.partition === void 0 ? {} : { partition: job.partition }
|
|
437
679
|
});
|
|
438
|
-
|
|
680
|
+
await (0, import_schema3.validate)(JobRecordSchema, record);
|
|
681
|
+
preparedJobs.push({ index: i, job, record });
|
|
439
682
|
} catch (error) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
683
|
+
errors.push({
|
|
684
|
+
index: i,
|
|
685
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
686
|
+
job
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (preparedJobs.length === 0) {
|
|
691
|
+
return { scheduledCount: 0, skippedCount: 0, errors };
|
|
692
|
+
}
|
|
693
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
694
|
+
await this.jobs.createIndexesPromise;
|
|
695
|
+
const operations = preparedJobs.map(
|
|
696
|
+
({ record }) => record.uniqueIdentifier === void 0 ? { insertOne: { document: record } } : {
|
|
697
|
+
updateOne: {
|
|
698
|
+
filter: { uniqueIdentifier: record.uniqueIdentifier },
|
|
699
|
+
update: { $setOnInsert: record },
|
|
700
|
+
upsert: true
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
let scheduledCount = 0;
|
|
705
|
+
let skippedCount = 0;
|
|
706
|
+
try {
|
|
707
|
+
const result = await rawCollection.bulkWrite(operations, { ordered: false });
|
|
708
|
+
scheduledCount = result.insertedCount + result.upsertedCount;
|
|
709
|
+
skippedCount = result.matchedCount;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
const writeErrors = error instanceof import_mongodb2.MongoDB.MongoBulkWriteError ? Array.isArray(error.writeErrors) ? error.writeErrors : [error.writeErrors] : [];
|
|
712
|
+
if (!(error instanceof import_mongodb2.MongoDB.MongoBulkWriteError) || writeErrors.length === 0) {
|
|
713
|
+
for (const preparedJob of preparedJobs) {
|
|
444
714
|
errors.push({
|
|
445
|
-
index:
|
|
715
|
+
index: preparedJob.index,
|
|
446
716
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
447
|
-
job
|
|
717
|
+
job: preparedJob.job
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
scheduledCount = error.result.insertedCount + error.result.upsertedCount;
|
|
722
|
+
skippedCount = error.result.matchedCount;
|
|
723
|
+
for (const writeError of writeErrors) {
|
|
724
|
+
const preparedJob = preparedJobs[writeError.index];
|
|
725
|
+
if (!preparedJob) continue;
|
|
726
|
+
if (writeError.code === 11e3 && preparedJob.record.uniqueIdentifier !== void 0) {
|
|
727
|
+
skippedCount++;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
errors.push({
|
|
731
|
+
index: preparedJob.index,
|
|
732
|
+
error: new Error(writeError.errmsg),
|
|
733
|
+
job: preparedJob.job
|
|
448
734
|
});
|
|
449
735
|
}
|
|
450
736
|
}
|
|
@@ -458,11 +744,55 @@ var JobsRepo = class {
|
|
|
458
744
|
errors
|
|
459
745
|
};
|
|
460
746
|
}
|
|
747
|
+
async reconcilePartitions(options) {
|
|
748
|
+
if (options.jobNames.length === 0) return 0;
|
|
749
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
750
|
+
await this.jobs.createIndexesPromise;
|
|
751
|
+
const now = /* @__PURE__ */ new Date();
|
|
752
|
+
const invalidPartition = {
|
|
753
|
+
$or: [
|
|
754
|
+
{ partition: { $exists: false } },
|
|
755
|
+
{ partition: { $lt: 0 } },
|
|
756
|
+
{ partition: { $gte: options.nPartitions } }
|
|
757
|
+
]
|
|
758
|
+
};
|
|
759
|
+
const availableLock = {
|
|
760
|
+
$or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: now } }]
|
|
761
|
+
};
|
|
762
|
+
const runnableJob = {
|
|
763
|
+
$or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }]
|
|
764
|
+
};
|
|
765
|
+
const jobs = await rawCollection.find({
|
|
766
|
+
jobName: { $in: options.jobNames },
|
|
767
|
+
$and: [invalidPartition, availableLock, runnableJob]
|
|
768
|
+
}).project({ _id: 1 }).limit(options.batchSize).toArray();
|
|
769
|
+
if (jobs.length === 0) return 0;
|
|
770
|
+
const result = await rawCollection.bulkWrite(
|
|
771
|
+
jobs.map((job) => ({
|
|
772
|
+
updateOne: {
|
|
773
|
+
filter: {
|
|
774
|
+
_id: job._id,
|
|
775
|
+
$and: [invalidPartition, availableLock, runnableJob]
|
|
776
|
+
},
|
|
777
|
+
update: { $set: { partition: getRandomPartition(options.nPartitions) } }
|
|
778
|
+
}
|
|
779
|
+
})),
|
|
780
|
+
{ ordered: false }
|
|
781
|
+
);
|
|
782
|
+
return result.modifiedCount;
|
|
783
|
+
}
|
|
461
784
|
};
|
|
462
|
-
|
|
463
|
-
__decorateElement(
|
|
464
|
-
JobsRepo = __decorateElement(
|
|
465
|
-
__runInitializers(
|
|
785
|
+
_init2 = __decoratorStart(null);
|
|
786
|
+
__decorateElement(_init2, 5, "jobs", _jobs_dec, JobsRepo);
|
|
787
|
+
JobsRepo = __decorateElement(_init2, 0, "JobsRepo", _JobsRepo_decorators, JobsRepo);
|
|
788
|
+
__runInitializers(_init2, 1, JobsRepo);
|
|
789
|
+
|
|
790
|
+
// src/schedule.ts
|
|
791
|
+
var import_services2 = require("@orion-js/services");
|
|
792
|
+
|
|
793
|
+
// src/services/EventsService.ts
|
|
794
|
+
var import_logger2 = require("@orion-js/logger");
|
|
795
|
+
var import_services = require("@orion-js/services");
|
|
466
796
|
|
|
467
797
|
// src/services/getNextRunDate.ts
|
|
468
798
|
var import_cron_parser = require("cron-parser");
|
|
@@ -492,176 +822,65 @@ var getNextRunDate = (options) => {
|
|
|
492
822
|
};
|
|
493
823
|
|
|
494
824
|
// src/services/EventsService.ts
|
|
495
|
-
var _jobsRepo_dec, _EventsService_decorators,
|
|
825
|
+
var _jobsRepo_dec, _EventsService_decorators, _init3;
|
|
496
826
|
_EventsService_decorators = [(0, import_services.Service)()], _jobsRepo_dec = [(0, import_services.Inject)(() => JobsRepo)];
|
|
497
827
|
var EventsService = class {
|
|
498
828
|
constructor() {
|
|
499
|
-
__publicField(this, "jobsRepo", __runInitializers(
|
|
829
|
+
__publicField(this, "jobsRepo", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
|
|
500
830
|
}
|
|
501
|
-
async scheduleJob(options) {
|
|
831
|
+
async scheduleJob(options, nPartitions) {
|
|
502
832
|
import_logger2.logger.debug("Scheduling job...", options);
|
|
833
|
+
const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
|
|
503
834
|
await this.jobsRepo.scheduleJob({
|
|
504
835
|
name: options.name,
|
|
505
836
|
priority: options.priority || 100,
|
|
506
837
|
nextRunAt: getNextRunDate(options),
|
|
507
838
|
params: options.params || null,
|
|
508
|
-
uniqueIdentifier: options.uniqueIdentifier
|
|
839
|
+
uniqueIdentifier: options.uniqueIdentifier,
|
|
840
|
+
partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
|
|
509
841
|
});
|
|
510
842
|
}
|
|
511
|
-
async scheduleJobs(jobs) {
|
|
843
|
+
async scheduleJobs(jobs, nPartitions) {
|
|
512
844
|
import_logger2.logger.debug(`Scheduling ${jobs.length} jobs...`);
|
|
513
|
-
const jobRecords = jobs.map((options) =>
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
845
|
+
const jobRecords = jobs.map((options) => {
|
|
846
|
+
const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
|
|
847
|
+
return {
|
|
848
|
+
name: options.name,
|
|
849
|
+
priority: options.priority || 100,
|
|
850
|
+
nextRunAt: getNextRunDate(options),
|
|
851
|
+
params: options.params || null,
|
|
852
|
+
uniqueIdentifier: options.uniqueIdentifier,
|
|
853
|
+
partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
|
|
854
|
+
};
|
|
855
|
+
});
|
|
520
856
|
return await this.jobsRepo.scheduleJobs(jobRecords);
|
|
521
857
|
}
|
|
522
858
|
};
|
|
523
|
-
|
|
524
|
-
__decorateElement(
|
|
525
|
-
EventsService = __decorateElement(
|
|
526
|
-
__runInitializers(
|
|
859
|
+
_init3 = __decoratorStart(null);
|
|
860
|
+
__decorateElement(_init3, 5, "jobsRepo", _jobsRepo_dec, EventsService);
|
|
861
|
+
EventsService = __decorateElement(_init3, 0, "EventsService", _EventsService_decorators, EventsService);
|
|
862
|
+
__runInitializers(_init3, 1, EventsService);
|
|
863
|
+
|
|
864
|
+
// src/schedule.ts
|
|
865
|
+
var eventsService = (0, import_services2.getInstance)(EventsService);
|
|
866
|
+
function scheduleJobInternal(options, nPartitions) {
|
|
867
|
+
return eventsService.scheduleJob(options, nPartitions);
|
|
868
|
+
}
|
|
869
|
+
function scheduleJobsInternal(jobs, nPartitions) {
|
|
870
|
+
return eventsService.scheduleJobs(jobs, nPartitions);
|
|
871
|
+
}
|
|
527
872
|
|
|
528
873
|
// src/services/WorkerService.ts
|
|
529
874
|
var import_helpers2 = require("@orion-js/helpers");
|
|
530
875
|
var import_logger4 = require("@orion-js/logger");
|
|
531
|
-
var
|
|
876
|
+
var import_services4 = require("@orion-js/services");
|
|
532
877
|
|
|
533
878
|
// src/services/Executor.ts
|
|
534
879
|
var import_api = require("@opentelemetry/api");
|
|
535
880
|
var import_logger3 = require("@orion-js/logger");
|
|
536
|
-
var
|
|
537
|
-
|
|
538
|
-
// src/repos/JobsHistoryRepo.ts
|
|
539
|
-
var import_mongodb2 = require("@orion-js/mongodb");
|
|
540
|
-
|
|
541
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/isInteger.js
|
|
542
|
-
function _isInteger(n) {
|
|
543
|
-
return n << 0 === n;
|
|
544
|
-
}
|
|
545
|
-
var isInteger = Number.isInteger || _isInteger;
|
|
546
|
-
|
|
547
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/createPath.js
|
|
548
|
-
function createPath(path, delimiter = ".") {
|
|
549
|
-
return typeof path === "string" ? path.split(delimiter).map((x) => isInteger(x) ? Number(x) : x) : path;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/compare.js
|
|
553
|
-
function compare(a, b) {
|
|
554
|
-
return String(a) === String(b);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/includes.js
|
|
558
|
-
function includes(a, list) {
|
|
559
|
-
let index = -1;
|
|
560
|
-
const { length } = list;
|
|
561
|
-
while (++index < length)
|
|
562
|
-
if (compare(list[index], a))
|
|
563
|
-
return true;
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/omit.js
|
|
568
|
-
function omit(propsToOmit, obj) {
|
|
569
|
-
if (arguments.length === 1) return (_obj) => omit(propsToOmit, _obj);
|
|
570
|
-
if (obj === null || obj === void 0)
|
|
571
|
-
return void 0;
|
|
572
|
-
const propsToOmitValue = createPath(propsToOmit, ",");
|
|
573
|
-
const willReturn = {};
|
|
574
|
-
for (const key in obj)
|
|
575
|
-
if (!includes(key, propsToOmitValue))
|
|
576
|
-
willReturn[key] = obj[key];
|
|
577
|
-
return willReturn;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// src/types/HistoryRecord.ts
|
|
581
|
-
var import_schema2 = require("@orion-js/schema");
|
|
582
|
-
var HistoryRecordSchema = (0, import_schema2.schemaWithName)("HistoryRecord", {
|
|
583
|
-
_id: { type: "string" },
|
|
584
|
-
jobId: { type: "string" },
|
|
585
|
-
executionId: { type: "string" },
|
|
586
|
-
jobName: { type: "string" },
|
|
587
|
-
type: { type: "string" },
|
|
588
|
-
priority: { type: "number" },
|
|
589
|
-
tries: { type: "number" },
|
|
590
|
-
uniqueIdentifier: { type: "string", optional: true },
|
|
591
|
-
startedAt: { type: "date" },
|
|
592
|
-
endedAt: { type: "date" },
|
|
593
|
-
duration: { type: "number" },
|
|
594
|
-
expiresAt: { type: "date", optional: true },
|
|
595
|
-
status: { type: "string", enum: ["success", "error", "stale"] },
|
|
596
|
-
errorMessage: { type: "string", optional: true },
|
|
597
|
-
params: { type: "blackbox", optional: true },
|
|
598
|
-
result: { type: "any", optional: true }
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
// src/repos/JobsHistoryRepo.ts
|
|
602
|
-
var _history_dec, _JobsHistoryRepo_decorators, _init3;
|
|
603
|
-
_JobsHistoryRepo_decorators = [(0, import_mongodb2.Repository)()], _history_dec = [(0, import_mongodb2.MongoCollection)({
|
|
604
|
-
name: "orionjs.jobs_dogs_history",
|
|
605
|
-
idGeneration: "uuid",
|
|
606
|
-
schema: HistoryRecordSchema,
|
|
607
|
-
indexes: [
|
|
608
|
-
{
|
|
609
|
-
keys: {
|
|
610
|
-
jobName: 1,
|
|
611
|
-
startedAt: 1
|
|
612
|
-
}
|
|
613
|
-
},
|
|
614
|
-
{
|
|
615
|
-
keys: {
|
|
616
|
-
executionId: 1
|
|
617
|
-
}
|
|
618
|
-
},
|
|
619
|
-
{
|
|
620
|
-
keys: {
|
|
621
|
-
expiresAt: 1
|
|
622
|
-
},
|
|
623
|
-
options: {
|
|
624
|
-
expireAfterSeconds: 0
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
]
|
|
628
|
-
})];
|
|
629
|
-
var JobsHistoryRepo = class {
|
|
630
|
-
constructor() {
|
|
631
|
-
__publicField(this, "history", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
|
|
632
|
-
}
|
|
633
|
-
async saveExecution(record) {
|
|
634
|
-
await this.history.upsert(
|
|
635
|
-
{ executionId: record.executionId },
|
|
636
|
-
{
|
|
637
|
-
$setOnInsert: {
|
|
638
|
-
status: record.status
|
|
639
|
-
},
|
|
640
|
-
$set: {
|
|
641
|
-
...omit(["status"], record)
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
async getExecutions(jobName, limit, skip) {
|
|
647
|
-
const cursor = this.history.find({ jobName }).sort({ startedAt: -1 });
|
|
648
|
-
if (skip) {
|
|
649
|
-
cursor.skip(skip);
|
|
650
|
-
}
|
|
651
|
-
if (limit) {
|
|
652
|
-
cursor.limit(limit);
|
|
653
|
-
}
|
|
654
|
-
return await cursor.toArray();
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
_init3 = __decoratorStart(null);
|
|
658
|
-
__decorateElement(_init3, 5, "history", _history_dec, JobsHistoryRepo);
|
|
659
|
-
JobsHistoryRepo = __decorateElement(_init3, 0, "JobsHistoryRepo", _JobsHistoryRepo_decorators, JobsHistoryRepo);
|
|
660
|
-
__runInitializers(_init3, 1, JobsHistoryRepo);
|
|
661
|
-
|
|
662
|
-
// src/services/Executor.ts
|
|
881
|
+
var import_services3 = require("@orion-js/services");
|
|
663
882
|
var _jobsHistoryRepo_dec, _jobsRepo_dec2, _Executor_decorators, _init4;
|
|
664
|
-
_Executor_decorators = [(0,
|
|
883
|
+
_Executor_decorators = [(0, import_services3.Service)()], _jobsRepo_dec2 = [(0, import_services3.Inject)(() => JobsRepo)], _jobsHistoryRepo_dec = [(0, import_services3.Inject)(() => JobsHistoryRepo)];
|
|
665
884
|
var Executor = class {
|
|
666
885
|
constructor() {
|
|
667
886
|
__publicField(this, "jobsRepo", __runInitializers(_init4, 8, this)), __runInitializers(_init4, 11, this);
|
|
@@ -1001,10 +1220,14 @@ __runInitializers(_init4, 1, Executor);
|
|
|
1001
1220
|
// src/services/WorkerService.ts
|
|
1002
1221
|
var FILTERED_JOB_NAMES_ACQUISITION_HINT = "byJobName";
|
|
1003
1222
|
var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
|
|
1004
|
-
_WorkerService_decorators = [(0,
|
|
1223
|
+
_WorkerService_decorators = [(0, import_services4.Service)()], _jobsRepo_dec3 = [(0, import_services4.Inject)(() => JobsRepo)], _executor_dec = [(0, import_services4.Inject)(() => Executor)];
|
|
1005
1224
|
var WorkerService = class {
|
|
1006
1225
|
constructor() {
|
|
1007
1226
|
__publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
|
|
1227
|
+
__publicField(this, "partitionReconcilerIntervalMs", 5 * 60 * 1e3);
|
|
1228
|
+
__publicField(this, "partitionReconcilerJitterMs", 30 * 1e3);
|
|
1229
|
+
__publicField(this, "partitionReconcilerBatchPauseMs", 250);
|
|
1230
|
+
__publicField(this, "partitionReconcilerBatchSize", 500);
|
|
1008
1231
|
__publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
|
|
1009
1232
|
__publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
|
|
1010
1233
|
}
|
|
@@ -1089,6 +1312,15 @@ var WorkerService = class {
|
|
|
1089
1312
|
if (capacityPromises.length === 0) return;
|
|
1090
1313
|
await Promise.race([...capacityPromises, workersInstance.stopSignal]);
|
|
1091
1314
|
}
|
|
1315
|
+
getNextPartition(config, workersInstance) {
|
|
1316
|
+
const partition = workersInstance.partitionOrder[workersInstance.partitionCursor];
|
|
1317
|
+
workersInstance.partitionCursor++;
|
|
1318
|
+
if (workersInstance.partitionCursor >= config.nPartitions) {
|
|
1319
|
+
workersInstance.partitionOrder = createShuffledPartitions(config.nPartitions);
|
|
1320
|
+
workersInstance.partitionCursor = 0;
|
|
1321
|
+
}
|
|
1322
|
+
return partition;
|
|
1323
|
+
}
|
|
1092
1324
|
async runSchedulerLoop(config, workersInstance) {
|
|
1093
1325
|
const jobNames = this.getJobNames(config.jobs);
|
|
1094
1326
|
const executeConfig = {
|
|
@@ -1098,6 +1330,7 @@ var WorkerService = class {
|
|
|
1098
1330
|
onMaxTriesReached: config.onMaxTriesReached
|
|
1099
1331
|
};
|
|
1100
1332
|
import_logger4.logger.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
|
|
1333
|
+
let consecutivePartitionMisses = 0;
|
|
1101
1334
|
while (workersInstance.running) {
|
|
1102
1335
|
let didMiss = false;
|
|
1103
1336
|
let allJobNamesAtCapacity = false;
|
|
@@ -1109,21 +1342,30 @@ var WorkerService = class {
|
|
|
1109
1342
|
break;
|
|
1110
1343
|
}
|
|
1111
1344
|
const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
|
|
1345
|
+
const partition = this.getNextPartition(config, workersInstance);
|
|
1112
1346
|
const jobToRun = await this.jobsRepo.getJobAndLock(
|
|
1113
1347
|
availableJobNames,
|
|
1114
1348
|
config.defaultLockTime,
|
|
1115
|
-
jobAcquisitionHint
|
|
1349
|
+
jobAcquisitionHint,
|
|
1350
|
+
partition
|
|
1116
1351
|
);
|
|
1117
1352
|
if (!jobToRun) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1353
|
+
consecutivePartitionMisses++;
|
|
1354
|
+
import_logger4.logger.debug(`No job to run in partition ${partition}`);
|
|
1355
|
+
if (consecutivePartitionMisses >= config.nPartitions) {
|
|
1356
|
+
consecutivePartitionMisses = 0;
|
|
1357
|
+
didMiss = true;
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
continue;
|
|
1121
1361
|
}
|
|
1362
|
+
consecutivePartitionMisses = 0;
|
|
1122
1363
|
this.startExecution(executeConfig, workersInstance, jobToRun);
|
|
1123
1364
|
if (!workersInstance.running) break;
|
|
1124
1365
|
}
|
|
1125
1366
|
} catch (error) {
|
|
1126
1367
|
import_logger4.logger.error("Error in job scheduler.", { error });
|
|
1368
|
+
consecutivePartitionMisses = 0;
|
|
1127
1369
|
didMiss = true;
|
|
1128
1370
|
}
|
|
1129
1371
|
if (!workersInstance.running) return;
|
|
@@ -1159,11 +1401,13 @@ var WorkerService = class {
|
|
|
1159
1401
|
workersInstance.jobAcquisitionHintProbeCycle++;
|
|
1160
1402
|
try {
|
|
1161
1403
|
for (let sample = 0; sample < 3; sample++) {
|
|
1404
|
+
const partition = getRandomPartition(config.nPartitions);
|
|
1162
1405
|
for (const hint of [firstHint, secondHint]) {
|
|
1163
1406
|
if (!workersInstance.running) return;
|
|
1164
1407
|
const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
|
|
1165
1408
|
jobNames,
|
|
1166
|
-
hint
|
|
1409
|
+
hint,
|
|
1410
|
+
partition
|
|
1167
1411
|
);
|
|
1168
1412
|
if (!workersInstance.running) return;
|
|
1169
1413
|
byHint[hint].push(executionTimeMillis);
|
|
@@ -1235,6 +1479,44 @@ var WorkerService = class {
|
|
|
1235
1479
|
await this.waitForNextJobAcquisitionHintProbe(workersInstance);
|
|
1236
1480
|
}
|
|
1237
1481
|
}
|
|
1482
|
+
async reconcilePartitionBatch(config) {
|
|
1483
|
+
if (typeof this.jobsRepo.reconcilePartitions !== "function") return 0;
|
|
1484
|
+
try {
|
|
1485
|
+
return await this.jobsRepo.reconcilePartitions({
|
|
1486
|
+
jobNames: this.getJobNames(config.jobs),
|
|
1487
|
+
nPartitions: config.nPartitions,
|
|
1488
|
+
batchSize: this.partitionReconcilerBatchSize
|
|
1489
|
+
});
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
import_logger4.logger.warn("Job partition reconciliation failed; it will be retried.", { error });
|
|
1492
|
+
return 0;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
async waitForNextPartitionReconcile(workersInstance, delayMs) {
|
|
1496
|
+
await new Promise((resolve) => {
|
|
1497
|
+
let resolved = false;
|
|
1498
|
+
const finishWait = () => {
|
|
1499
|
+
if (resolved) return;
|
|
1500
|
+
resolved = true;
|
|
1501
|
+
clearTimeout(workersInstance.partitionReconcilerTimer);
|
|
1502
|
+
workersInstance.partitionReconcilerTimer = void 0;
|
|
1503
|
+
workersInstance.cancelPartitionReconcilerWait = void 0;
|
|
1504
|
+
resolve();
|
|
1505
|
+
};
|
|
1506
|
+
workersInstance.partitionReconcilerTimer = setTimeout(finishWait, delayMs);
|
|
1507
|
+
workersInstance.cancelPartitionReconcilerWait = finishWait;
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
async runPartitionReconcilerLoop(config, workersInstance, initialReconcilePromise) {
|
|
1511
|
+
let modifiedCount = await initialReconcilePromise;
|
|
1512
|
+
while (workersInstance.running) {
|
|
1513
|
+
const didFillBatch = modifiedCount >= this.partitionReconcilerBatchSize;
|
|
1514
|
+
const delayMs = didFillBatch ? this.partitionReconcilerBatchPauseMs : this.partitionReconcilerIntervalMs + Math.floor(Math.random() * (this.partitionReconcilerJitterMs + 1));
|
|
1515
|
+
await this.waitForNextPartitionReconcile(workersInstance, delayMs);
|
|
1516
|
+
if (!workersInstance.running) return;
|
|
1517
|
+
modifiedCount = await this.reconcilePartitionBatch(config);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1238
1520
|
createWorkersInstanceDefinition(config) {
|
|
1239
1521
|
let signalStop;
|
|
1240
1522
|
const stopSignal = new Promise((resolve) => {
|
|
@@ -1250,7 +1532,10 @@ var WorkerService = class {
|
|
|
1250
1532
|
jobAcquisitionHintProbeCycle: 0,
|
|
1251
1533
|
hasCompletedInitialJobAcquisitionHintProbe: false,
|
|
1252
1534
|
pendingJobAcquisitionHintWins: 0,
|
|
1535
|
+
partitionOrder: createShuffledPartitions(config.nPartitions),
|
|
1536
|
+
partitionCursor: 0,
|
|
1253
1537
|
jobAcquisitionHintProbePromise: Promise.resolve(),
|
|
1538
|
+
partitionReconcilerPromise: Promise.resolve(),
|
|
1254
1539
|
schedulerPromise: Promise.resolve(),
|
|
1255
1540
|
stopSignal,
|
|
1256
1541
|
signalStop,
|
|
@@ -1260,14 +1545,16 @@ var WorkerService = class {
|
|
|
1260
1545
|
stop: async () => {
|
|
1261
1546
|
if (workersInstance.stopPromise) return workersInstance.stopPromise;
|
|
1262
1547
|
workersInstance.stopPromise = (async () => {
|
|
1263
|
-
var _a;
|
|
1548
|
+
var _a, _b;
|
|
1264
1549
|
import_logger4.logger.info("Stopping job scheduler...");
|
|
1265
1550
|
workersInstance.running = false;
|
|
1266
1551
|
workersInstance.signalStop();
|
|
1267
1552
|
(_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
|
|
1553
|
+
(_b = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b.call(workersInstance);
|
|
1268
1554
|
await Promise.all([
|
|
1269
1555
|
workersInstance.schedulerPromise,
|
|
1270
|
-
workersInstance.jobAcquisitionHintProbePromise
|
|
1556
|
+
workersInstance.jobAcquisitionHintProbePromise,
|
|
1557
|
+
workersInstance.partitionReconcilerPromise
|
|
1271
1558
|
]);
|
|
1272
1559
|
const activeCapacity = Array.from(
|
|
1273
1560
|
workersInstance.activeExecutions.values(),
|
|
@@ -1285,7 +1572,7 @@ var WorkerService = class {
|
|
|
1285
1572
|
await Promise.all(
|
|
1286
1573
|
jobs.filter((job) => job.type === "recurrent").map(async (job) => {
|
|
1287
1574
|
import_logger4.logger.debug(`Ensuring records for job "${job.name}"...`);
|
|
1288
|
-
await this.jobsRepo.ensureJobRecord(job);
|
|
1575
|
+
await this.jobsRepo.ensureJobRecord(job, getRandomPartition(config.nPartitions));
|
|
1289
1576
|
})
|
|
1290
1577
|
);
|
|
1291
1578
|
}
|
|
@@ -1302,29 +1589,41 @@ var WorkerService = class {
|
|
|
1302
1589
|
* @param userConfig - Configuration for the scheduler. Required field: jobs
|
|
1303
1590
|
*/
|
|
1304
1591
|
startWorkers(userConfig) {
|
|
1592
|
+
var _a, _b;
|
|
1305
1593
|
const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
|
|
1306
1594
|
if (maxTriesReachedRetentionMs !== null && (!Number.isFinite(maxTriesReachedRetentionMs) || maxTriesReachedRetentionMs < 0)) {
|
|
1307
1595
|
throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
|
|
1308
1596
|
}
|
|
1597
|
+
const nPartitions = userConfig.nPartitions ?? 1;
|
|
1598
|
+
validatePartitionsCount(nPartitions);
|
|
1309
1599
|
const config = {
|
|
1310
1600
|
pollInterval: 3e3,
|
|
1311
1601
|
workersCount: 4,
|
|
1312
1602
|
defaultLockTime: 30 * 1e3,
|
|
1313
1603
|
...userConfig,
|
|
1604
|
+
nPartitions,
|
|
1314
1605
|
maxTriesReachedRetentionMs
|
|
1315
1606
|
};
|
|
1316
|
-
setNameToJobs(config.jobs);
|
|
1607
|
+
setNameToJobs(config.jobs, config.nPartitions);
|
|
1608
|
+
(_b = (_a = this.jobsRepo).configureJobPartitions) == null ? void 0 : _b.call(_a, this.getJobNames(config.jobs), config.nPartitions);
|
|
1317
1609
|
const workersInstance = this.createWorkersInstanceDefinition(config);
|
|
1318
1610
|
import_logger4.logger.debug("Starting job scheduler", config);
|
|
1319
1611
|
workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
|
|
1320
1612
|
config,
|
|
1321
1613
|
workersInstance
|
|
1322
1614
|
);
|
|
1615
|
+
const initialReconcilePromise = this.reconcilePartitionBatch(config);
|
|
1616
|
+
workersInstance.partitionReconcilerPromise = this.runPartitionReconcilerLoop(
|
|
1617
|
+
config,
|
|
1618
|
+
workersInstance,
|
|
1619
|
+
initialReconcilePromise
|
|
1620
|
+
);
|
|
1323
1621
|
workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
|
|
1324
|
-
var
|
|
1622
|
+
var _a2, _b2;
|
|
1325
1623
|
workersInstance.running = false;
|
|
1326
1624
|
workersInstance.signalStop();
|
|
1327
|
-
(
|
|
1625
|
+
(_a2 = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a2.call(workersInstance);
|
|
1626
|
+
(_b2 = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b2.call(workersInstance);
|
|
1328
1627
|
import_logger4.logger.error("Job scheduler stopped after an unexpected error.", { error });
|
|
1329
1628
|
});
|
|
1330
1629
|
return workersInstance;
|
|
@@ -1335,17 +1634,15 @@ __decorateElement(_init5, 5, "jobsRepo", _jobsRepo_dec3, WorkerService);
|
|
|
1335
1634
|
__decorateElement(_init5, 5, "executor", _executor_dec, WorkerService);
|
|
1336
1635
|
WorkerService = __decorateElement(_init5, 0, "WorkerService", _WorkerService_decorators, WorkerService);
|
|
1337
1636
|
__runInitializers(_init5, 1, WorkerService);
|
|
1338
|
-
function setNameToJobs(jobs) {
|
|
1637
|
+
function setNameToJobs(jobs, nPartitions) {
|
|
1339
1638
|
for (const name of Object.keys(jobs)) {
|
|
1340
1639
|
jobs[name].jobName = name;
|
|
1640
|
+
jobs[name].nPartitions = nPartitions;
|
|
1341
1641
|
}
|
|
1342
1642
|
}
|
|
1343
1643
|
|
|
1344
|
-
// src/service/index.ts
|
|
1345
|
-
var import_services4 = require("@orion-js/services");
|
|
1346
|
-
|
|
1347
1644
|
// src/defineJob/index.ts
|
|
1348
|
-
var
|
|
1645
|
+
var import_schema4 = require("@orion-js/schema");
|
|
1349
1646
|
var import_cron_parser2 = require("cron-parser");
|
|
1350
1647
|
var import_parse_duration = __toESM(require("parse-duration"), 1);
|
|
1351
1648
|
function createEventJob(options) {
|
|
@@ -1359,12 +1656,15 @@ function createEventJob(options) {
|
|
|
1359
1656
|
if (!jobDefinition.jobName) {
|
|
1360
1657
|
throw new Error("This job has not been registered in the workers");
|
|
1361
1658
|
}
|
|
1362
|
-
const params = jobDefinition.params ? await (0,
|
|
1363
|
-
return await
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1659
|
+
const params = jobDefinition.params ? await (0, import_schema4.cleanAndValidate)(jobDefinition.params, scheduleOptions.params) : scheduleOptions.params;
|
|
1660
|
+
return await scheduleJobInternal(
|
|
1661
|
+
{
|
|
1662
|
+
...scheduleOptions,
|
|
1663
|
+
name: jobDefinition.jobName,
|
|
1664
|
+
params
|
|
1665
|
+
},
|
|
1666
|
+
jobDefinition.nPartitions
|
|
1667
|
+
);
|
|
1368
1668
|
};
|
|
1369
1669
|
jobDefinition.scheduleJobs = async (jobs) => {
|
|
1370
1670
|
if (!jobDefinition.jobName) {
|
|
@@ -1372,7 +1672,7 @@ function createEventJob(options) {
|
|
|
1372
1672
|
}
|
|
1373
1673
|
const processedJobs = await Promise.all(
|
|
1374
1674
|
jobs.map(async (scheduleOptions) => {
|
|
1375
|
-
const params = jobDefinition.params ? await (0,
|
|
1675
|
+
const params = jobDefinition.params ? await (0, import_schema4.cleanAndValidate)(jobDefinition.params, scheduleOptions.params) : scheduleOptions.params;
|
|
1376
1676
|
return {
|
|
1377
1677
|
...scheduleOptions,
|
|
1378
1678
|
name: jobDefinition.jobName,
|
|
@@ -1380,7 +1680,7 @@ function createEventJob(options) {
|
|
|
1380
1680
|
};
|
|
1381
1681
|
})
|
|
1382
1682
|
);
|
|
1383
|
-
return await
|
|
1683
|
+
return await scheduleJobsInternal(processedJobs, jobDefinition.nPartitions);
|
|
1384
1684
|
};
|
|
1385
1685
|
return jobDefinition;
|
|
1386
1686
|
}
|
|
@@ -1409,13 +1709,14 @@ var defineJob = (options) => {
|
|
|
1409
1709
|
};
|
|
1410
1710
|
|
|
1411
1711
|
// src/service/index.ts
|
|
1712
|
+
var import_services5 = require("@orion-js/services");
|
|
1412
1713
|
var serviceMetadata = /* @__PURE__ */ new WeakMap();
|
|
1413
1714
|
var jobsMetadata = /* @__PURE__ */ new Map();
|
|
1414
1715
|
var jobEntriesByClass = /* @__PURE__ */ new Map();
|
|
1415
1716
|
var pendingJobEntries = {};
|
|
1416
1717
|
function Jobs() {
|
|
1417
1718
|
return (target, context) => {
|
|
1418
|
-
(0,
|
|
1719
|
+
(0, import_services5.Service)()(target, context);
|
|
1419
1720
|
serviceMetadata.set(target, { _serviceType: "jobs" });
|
|
1420
1721
|
if (Object.keys(pendingJobEntries).length > 0) {
|
|
1421
1722
|
jobEntriesByClass.set(target, pendingJobEntries);
|
|
@@ -1464,7 +1765,7 @@ function initializeJobsIfNeeded(instance) {
|
|
|
1464
1765
|
jobsMetadata.set(instance, jobs);
|
|
1465
1766
|
}
|
|
1466
1767
|
function getServiceJobs(target) {
|
|
1467
|
-
const instance = (0,
|
|
1768
|
+
const instance = (0, import_services5.getInstance)(target);
|
|
1468
1769
|
if (!serviceMetadata.has(instance.constructor)) {
|
|
1469
1770
|
throw new Error("You must pass a class decorated with @Jobs to getServiceJobs");
|
|
1470
1771
|
}
|
|
@@ -1478,18 +1779,17 @@ function getServiceJobs(target) {
|
|
|
1478
1779
|
}
|
|
1479
1780
|
|
|
1480
1781
|
// src/index.ts
|
|
1481
|
-
var workerService = (0,
|
|
1482
|
-
var
|
|
1483
|
-
var
|
|
1484
|
-
var jobsRepo = (0, import_services5.getInstance)(JobsRepo);
|
|
1782
|
+
var workerService = (0, import_services6.getInstance)(WorkerService);
|
|
1783
|
+
var jobsHistoryRepo = (0, import_services6.getInstance)(JobsHistoryRepo);
|
|
1784
|
+
var jobsRepo = (0, import_services6.getInstance)(JobsRepo);
|
|
1485
1785
|
var startWorkers = (config) => {
|
|
1486
1786
|
return workerService.startWorkers(config);
|
|
1487
1787
|
};
|
|
1488
1788
|
var scheduleJob = (options) => {
|
|
1489
|
-
return
|
|
1789
|
+
return scheduleJobInternal(options);
|
|
1490
1790
|
};
|
|
1491
1791
|
var scheduleJobs = (jobs) => {
|
|
1492
|
-
return
|
|
1792
|
+
return scheduleJobsInternal(jobs);
|
|
1493
1793
|
};
|
|
1494
1794
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1495
1795
|
0 && (module.exports = {
|