@orion-js/dogs 4.6.2 → 4.6.5
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 +461 -204
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +454 -197
- 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 +4 -3
package/dist/index.cjs
CHANGED
|
@@ -92,27 +92,169 @@ __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
|
+
|
|
226
|
+
// src/services/partitions.ts
|
|
227
|
+
function validatePartitionsCount(nPartitions) {
|
|
228
|
+
if (!Number.isInteger(nPartitions) || nPartitions <= 0) {
|
|
229
|
+
throw new Error("nPartitions must be a positive integer");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function getRandomPartition(nPartitions) {
|
|
233
|
+
return Math.floor(Math.random() * nPartitions);
|
|
234
|
+
}
|
|
235
|
+
function createShuffledPartitions(nPartitions) {
|
|
236
|
+
const partitions = Array.from({ length: nPartitions }, (_, partition) => partition);
|
|
237
|
+
for (let index = partitions.length - 1; index > 0; index--) {
|
|
238
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
239
|
+
[partitions[index], partitions[swapIndex]] = [partitions[swapIndex], partitions[index]];
|
|
240
|
+
}
|
|
241
|
+
return partitions;
|
|
242
|
+
}
|
|
105
243
|
|
|
106
244
|
// src/types/JobRecord.ts
|
|
107
|
-
var
|
|
108
|
-
var JobStatusEnum = (0,
|
|
109
|
-
var JobRecordSchema = (0,
|
|
245
|
+
var import_schema2 = require("@orion-js/schema");
|
|
246
|
+
var JobStatusEnum = (0, import_schema2.createEnum)("JobStatus", ["pending", "maxTriesReached"]);
|
|
247
|
+
var JobRecordSchema = (0, import_schema2.schemaWithName)("JobRecord", {
|
|
110
248
|
_id: { type: "string" },
|
|
111
249
|
jobName: { type: "string" },
|
|
112
|
-
type: { type: (0,
|
|
250
|
+
type: { type: (0, import_schema2.createEnum)("JobType", ["recurrent", "event"]) },
|
|
113
251
|
priority: { type: "number" },
|
|
114
252
|
uniqueIdentifier: { type: "string", optional: true },
|
|
115
253
|
nextRunAt: { type: "date" },
|
|
254
|
+
/**
|
|
255
|
+
* Acquisition partition. Optional so records created by previous versions can be reconciled.
|
|
256
|
+
*/
|
|
257
|
+
partition: { type: "number", optional: true },
|
|
116
258
|
lastRunAt: { type: "date", optional: true },
|
|
117
259
|
lockedUntil: { type: "date", optional: true },
|
|
118
260
|
/**
|
|
@@ -120,6 +262,11 @@ var JobRecordSchema = (0, import_schema.schemaWithName)("JobRecord", {
|
|
|
120
262
|
* this value so a stale execution cannot modify a newer claim.
|
|
121
263
|
*/
|
|
122
264
|
lockId: { type: "string", optional: true },
|
|
265
|
+
/**
|
|
266
|
+
* Internal acquisition metadata used when MongoDB supports sorted updateOne. It preserves the
|
|
267
|
+
* pre-update stale state because updateOne intentionally does not return the previous document.
|
|
268
|
+
*/
|
|
269
|
+
claimWasStale: { type: "boolean", optional: true },
|
|
123
270
|
tries: { type: "number", optional: true },
|
|
124
271
|
params: { type: "blackbox", optional: true },
|
|
125
272
|
/**
|
|
@@ -144,23 +291,30 @@ var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
|
|
|
144
291
|
// src/repos/JobsRepo.ts
|
|
145
292
|
var JOB_ACQUISITION_HINTS = {
|
|
146
293
|
byJobName: {
|
|
294
|
+
partition: 1,
|
|
147
295
|
jobName: 1,
|
|
148
296
|
priority: -1,
|
|
149
297
|
nextRunAt: 1
|
|
150
298
|
},
|
|
151
299
|
globalPriority: {
|
|
300
|
+
partition: 1,
|
|
152
301
|
priority: -1,
|
|
153
302
|
nextRunAt: 1
|
|
154
303
|
}
|
|
155
304
|
};
|
|
156
305
|
var INITIAL_JOB_ACQUISITION_HINT = "byJobName";
|
|
157
306
|
var CANDIDATE_JOB_ACQUISITION_HINT = "globalPriority";
|
|
307
|
+
var SORTED_UPDATE_ONE_MIN_WIRE_VERSION = 25;
|
|
308
|
+
var LOCK_ID_INDEX_NAME = "jobs_dogs_records_lock_id";
|
|
309
|
+
var PARTITION_JOB_NAME_INDEX_NAME = "jobs_dogs_records_partition_job_name_priority_next_run";
|
|
310
|
+
var PARTITION_PRIORITY_INDEX_NAME = "jobs_dogs_records_partition_priority_next_run";
|
|
158
311
|
var JOB_ACQUISITION_SORT = {
|
|
159
312
|
priority: -1,
|
|
160
313
|
nextRunAt: 1
|
|
161
314
|
};
|
|
162
|
-
function getJobAcquisitionSelector(jobNames, now) {
|
|
315
|
+
function getJobAcquisitionSelector(jobNames, now, partition) {
|
|
163
316
|
return {
|
|
317
|
+
partition,
|
|
164
318
|
jobName: { $in: jobNames },
|
|
165
319
|
nextRunAt: { $lte: now },
|
|
166
320
|
$and: [
|
|
@@ -171,17 +325,19 @@ function getJobAcquisitionSelector(jobNames, now) {
|
|
|
171
325
|
]
|
|
172
326
|
};
|
|
173
327
|
}
|
|
174
|
-
var _jobs_dec, _JobsRepo_decorators,
|
|
175
|
-
_JobsRepo_decorators = [(0,
|
|
328
|
+
var _jobs_dec, _JobsRepo_decorators, _init2;
|
|
329
|
+
_JobsRepo_decorators = [(0, import_mongodb2.Repository)()], _jobs_dec = [(0, import_mongodb2.MongoCollection)({
|
|
176
330
|
idGeneration: "uuid",
|
|
177
331
|
name: "orionjs.jobs_dogs_records",
|
|
178
332
|
schema: JobRecordSchema,
|
|
179
333
|
indexes: [
|
|
180
334
|
{
|
|
181
|
-
keys: JOB_ACQUISITION_HINTS.byJobName
|
|
335
|
+
keys: JOB_ACQUISITION_HINTS.byJobName,
|
|
336
|
+
options: { name: PARTITION_JOB_NAME_INDEX_NAME }
|
|
182
337
|
},
|
|
183
338
|
{
|
|
184
|
-
keys: JOB_ACQUISITION_HINTS.globalPriority
|
|
339
|
+
keys: JOB_ACQUISITION_HINTS.globalPriority,
|
|
340
|
+
options: { name: PARTITION_PRIORITY_INDEX_NAME }
|
|
185
341
|
},
|
|
186
342
|
{
|
|
187
343
|
keys: {
|
|
@@ -201,6 +357,15 @@ _JobsRepo_decorators = [(0, import_mongodb.Repository)()], _jobs_dec = [(0, impo
|
|
|
201
357
|
sparse: true
|
|
202
358
|
}
|
|
203
359
|
},
|
|
360
|
+
{
|
|
361
|
+
keys: {
|
|
362
|
+
lockId: 1
|
|
363
|
+
},
|
|
364
|
+
options: {
|
|
365
|
+
name: LOCK_ID_INDEX_NAME,
|
|
366
|
+
sparse: true
|
|
367
|
+
}
|
|
368
|
+
},
|
|
204
369
|
{
|
|
205
370
|
keys: {
|
|
206
371
|
expiresAt: 1
|
|
@@ -212,14 +377,55 @@ _JobsRepo_decorators = [(0, import_mongodb.Repository)()], _jobs_dec = [(0, impo
|
|
|
212
377
|
})];
|
|
213
378
|
var JobsRepo = class {
|
|
214
379
|
constructor() {
|
|
215
|
-
__publicField(this, "jobs", __runInitializers(
|
|
380
|
+
__publicField(this, "jobs", __runInitializers(_init2, 8, this)), __runInitializers(_init2, 11, this);
|
|
381
|
+
__publicField(this, "sortedUpdateOneSupportPromise");
|
|
382
|
+
__publicField(this, "partitionsByJobName", /* @__PURE__ */ new Map());
|
|
383
|
+
}
|
|
384
|
+
configureJobPartitions(jobNames, nPartitions) {
|
|
385
|
+
for (const jobName of jobNames) {
|
|
386
|
+
this.partitionsByJobName.set(jobName, nPartitions);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
getJobPartitionsCount(jobName) {
|
|
390
|
+
return this.partitionsByJobName.get(jobName);
|
|
391
|
+
}
|
|
392
|
+
async supportsSortedUpdateOne() {
|
|
393
|
+
if (!this.sortedUpdateOneSupportPromise) {
|
|
394
|
+
this.sortedUpdateOneSupportPromise = this.detectSortedUpdateOneSupport();
|
|
395
|
+
}
|
|
396
|
+
return this.sortedUpdateOneSupportPromise;
|
|
397
|
+
}
|
|
398
|
+
async detectSortedUpdateOneSupport() {
|
|
399
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
400
|
+
await this.jobs.createIndexesPromise;
|
|
401
|
+
const hello = await rawCollection.db.admin().command({ hello: 1 });
|
|
402
|
+
const supportsSortedUpdateOne = typeof hello.maxWireVersion === "number" && hello.maxWireVersion >= SORTED_UPDATE_ONE_MIN_WIRE_VERSION;
|
|
403
|
+
if (!supportsSortedUpdateOne) {
|
|
404
|
+
import_logger.logger.info("MongoDB does not support sorted updateOne; using findOneAndUpdate for jobs.");
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
const hasLockIdIndex = await rawCollection.indexExists(LOCK_ID_INDEX_NAME);
|
|
408
|
+
if (!hasLockIdIndex) {
|
|
409
|
+
import_logger.logger.warn(
|
|
410
|
+
`MongoDB supports sorted updateOne but index "${LOCK_ID_INDEX_NAME}" is unavailable; using findOneAndUpdate for jobs.`
|
|
411
|
+
);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
import_logger.logger.info("MongoDB supports sorted updateOne; using it for job acquisition.");
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
async getJobAndLock(jobNames, lockTime, hint = INITIAL_JOB_ACQUISITION_HINT, partition = 0) {
|
|
418
|
+
if (await this.supportsSortedUpdateOne()) {
|
|
419
|
+
return this.getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition);
|
|
420
|
+
}
|
|
421
|
+
return this.getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition);
|
|
216
422
|
}
|
|
217
|
-
async
|
|
423
|
+
async getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition) {
|
|
218
424
|
const executionId = (0, import_helpers.generateId)();
|
|
219
425
|
const now = /* @__PURE__ */ new Date();
|
|
220
426
|
const lockedUntil = new Date(now.getTime() + lockTime);
|
|
221
427
|
const job = await this.jobs.findOneAndUpdate(
|
|
222
|
-
getJobAcquisitionSelector(jobNames, now),
|
|
428
|
+
getJobAcquisitionSelector(jobNames, now, partition),
|
|
223
429
|
{
|
|
224
430
|
$set: { lockId: executionId, lockedUntil, lastRunAt: now },
|
|
225
431
|
$inc: { tries: 1 }
|
|
@@ -235,6 +441,37 @@ var JobsRepo = class {
|
|
|
235
441
|
if (!job) return;
|
|
236
442
|
const tries = (job.tries || 0) + 1;
|
|
237
443
|
const wasStale = Boolean(job.lockedUntil);
|
|
444
|
+
return this.toJobToRun(job, executionId, lockTime, tries, wasStale);
|
|
445
|
+
}
|
|
446
|
+
async getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition) {
|
|
447
|
+
const executionId = (0, import_helpers.generateId)();
|
|
448
|
+
const now = /* @__PURE__ */ new Date();
|
|
449
|
+
const lockedUntil = new Date(now.getTime() + lockTime);
|
|
450
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
451
|
+
const result = await rawCollection.updateOne(
|
|
452
|
+
getJobAcquisitionSelector(jobNames, now, partition),
|
|
453
|
+
[
|
|
454
|
+
{
|
|
455
|
+
$set: {
|
|
456
|
+
lockId: { $literal: executionId },
|
|
457
|
+
lockedUntil: { $literal: lockedUntil },
|
|
458
|
+
lastRunAt: { $literal: now },
|
|
459
|
+
tries: { $add: [{ $ifNull: ["$tries", 0] }, 1] },
|
|
460
|
+
claimWasStale: { $eq: [{ $type: "$lockedUntil" }, "date"] }
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
],
|
|
464
|
+
{
|
|
465
|
+
hint: JOB_ACQUISITION_HINTS[hint],
|
|
466
|
+
sort: JOB_ACQUISITION_SORT
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
if (result.matchedCount === 0) return;
|
|
470
|
+
const job = await rawCollection.findOne({ lockId: executionId });
|
|
471
|
+
if (!job) return;
|
|
472
|
+
return this.toJobToRun(job, executionId, lockTime, job.tries || 0, Boolean(job.claimWasStale));
|
|
473
|
+
}
|
|
474
|
+
toJobToRun(job, executionId, lockTime, tries, wasStale) {
|
|
238
475
|
if (wasStale) {
|
|
239
476
|
import_logger.logger.info(`Running job "${job.jobName}" that was staled`);
|
|
240
477
|
}
|
|
@@ -252,9 +489,9 @@ var JobsRepo = class {
|
|
|
252
489
|
wasStale
|
|
253
490
|
};
|
|
254
491
|
}
|
|
255
|
-
async getJobAcquisitionExecutionTime(jobNames, hint) {
|
|
492
|
+
async getJobAcquisitionExecutionTime(jobNames, hint, partition = 0) {
|
|
256
493
|
var _a;
|
|
257
|
-
const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date()), {
|
|
494
|
+
const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date(), partition), {
|
|
258
495
|
hint: JOB_ACQUISITION_HINTS[hint],
|
|
259
496
|
maxTimeMS: 1e3,
|
|
260
497
|
readPreference: "primary"
|
|
@@ -278,7 +515,7 @@ var JobsRepo = class {
|
|
|
278
515
|
priority: options.priority,
|
|
279
516
|
...options.resetTries ? { tries: 0 } : {}
|
|
280
517
|
},
|
|
281
|
-
$unset: { lockId: "", lockedUntil: "" }
|
|
518
|
+
$unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
|
|
282
519
|
};
|
|
283
520
|
const result = await this.jobs.updateOne(
|
|
284
521
|
options.lockId ? { _id: options.jobId, lockId: options.lockId } : options.jobId,
|
|
@@ -312,6 +549,7 @@ var JobsRepo = class {
|
|
|
312
549
|
$unset: {
|
|
313
550
|
lockId: "",
|
|
314
551
|
lockedUntil: "",
|
|
552
|
+
claimWasStale: "",
|
|
315
553
|
...retentionMs === null ? { expiresAt: "" } : {}
|
|
316
554
|
}
|
|
317
555
|
}
|
|
@@ -368,12 +606,12 @@ var JobsRepo = class {
|
|
|
368
606
|
lockedUntil: { $exists: true }
|
|
369
607
|
},
|
|
370
608
|
{
|
|
371
|
-
$unset: { lockId: "", lockedUntil: "" }
|
|
609
|
+
$unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
|
|
372
610
|
}
|
|
373
611
|
);
|
|
374
612
|
return result.modifiedCount;
|
|
375
613
|
}
|
|
376
|
-
async ensureJobRecord(job) {
|
|
614
|
+
async ensureJobRecord(job, partition = 0) {
|
|
377
615
|
const result = await this.jobs.upsert(
|
|
378
616
|
{
|
|
379
617
|
jobName: job.name
|
|
@@ -387,7 +625,8 @@ var JobsRepo = class {
|
|
|
387
625
|
status: ""
|
|
388
626
|
},
|
|
389
627
|
$setOnInsert: {
|
|
390
|
-
nextRunAt: /* @__PURE__ */ new Date()
|
|
628
|
+
nextRunAt: /* @__PURE__ */ new Date(),
|
|
629
|
+
partition
|
|
391
630
|
}
|
|
392
631
|
}
|
|
393
632
|
);
|
|
@@ -405,7 +644,8 @@ var JobsRepo = class {
|
|
|
405
644
|
params: options.params,
|
|
406
645
|
nextRunAt: options.nextRunAt,
|
|
407
646
|
priority: options.priority,
|
|
408
|
-
type: "event"
|
|
647
|
+
type: "event",
|
|
648
|
+
...options.partition === void 0 ? {} : { partition: options.partition }
|
|
409
649
|
});
|
|
410
650
|
} catch (error) {
|
|
411
651
|
if (error.isValidationError && Object.values(error.validationErrors).includes("notUnique") && options.uniqueIdentifier) {
|
|
@@ -433,7 +673,8 @@ var JobsRepo = class {
|
|
|
433
673
|
params: job.params,
|
|
434
674
|
nextRunAt: job.nextRunAt,
|
|
435
675
|
priority: job.priority,
|
|
436
|
-
type: "event"
|
|
676
|
+
type: "event",
|
|
677
|
+
...job.partition === void 0 ? {} : { partition: job.partition }
|
|
437
678
|
});
|
|
438
679
|
scheduledCount++;
|
|
439
680
|
} catch (error) {
|
|
@@ -458,11 +699,55 @@ var JobsRepo = class {
|
|
|
458
699
|
errors
|
|
459
700
|
};
|
|
460
701
|
}
|
|
702
|
+
async reconcilePartitions(options) {
|
|
703
|
+
if (options.jobNames.length === 0) return 0;
|
|
704
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
705
|
+
await this.jobs.createIndexesPromise;
|
|
706
|
+
const now = /* @__PURE__ */ new Date();
|
|
707
|
+
const invalidPartition = {
|
|
708
|
+
$or: [
|
|
709
|
+
{ partition: { $exists: false } },
|
|
710
|
+
{ partition: { $lt: 0 } },
|
|
711
|
+
{ partition: { $gte: options.nPartitions } }
|
|
712
|
+
]
|
|
713
|
+
};
|
|
714
|
+
const availableLock = {
|
|
715
|
+
$or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: now } }]
|
|
716
|
+
};
|
|
717
|
+
const runnableJob = {
|
|
718
|
+
$or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }]
|
|
719
|
+
};
|
|
720
|
+
const jobs = await rawCollection.find({
|
|
721
|
+
jobName: { $in: options.jobNames },
|
|
722
|
+
$and: [invalidPartition, availableLock, runnableJob]
|
|
723
|
+
}).project({ _id: 1 }).limit(options.batchSize).toArray();
|
|
724
|
+
if (jobs.length === 0) return 0;
|
|
725
|
+
const result = await rawCollection.bulkWrite(
|
|
726
|
+
jobs.map((job) => ({
|
|
727
|
+
updateOne: {
|
|
728
|
+
filter: {
|
|
729
|
+
_id: job._id,
|
|
730
|
+
$and: [invalidPartition, availableLock, runnableJob]
|
|
731
|
+
},
|
|
732
|
+
update: { $set: { partition: getRandomPartition(options.nPartitions) } }
|
|
733
|
+
}
|
|
734
|
+
})),
|
|
735
|
+
{ ordered: false }
|
|
736
|
+
);
|
|
737
|
+
return result.modifiedCount;
|
|
738
|
+
}
|
|
461
739
|
};
|
|
462
|
-
|
|
463
|
-
__decorateElement(
|
|
464
|
-
JobsRepo = __decorateElement(
|
|
465
|
-
__runInitializers(
|
|
740
|
+
_init2 = __decoratorStart(null);
|
|
741
|
+
__decorateElement(_init2, 5, "jobs", _jobs_dec, JobsRepo);
|
|
742
|
+
JobsRepo = __decorateElement(_init2, 0, "JobsRepo", _JobsRepo_decorators, JobsRepo);
|
|
743
|
+
__runInitializers(_init2, 1, JobsRepo);
|
|
744
|
+
|
|
745
|
+
// src/schedule.ts
|
|
746
|
+
var import_services2 = require("@orion-js/services");
|
|
747
|
+
|
|
748
|
+
// src/services/EventsService.ts
|
|
749
|
+
var import_logger2 = require("@orion-js/logger");
|
|
750
|
+
var import_services = require("@orion-js/services");
|
|
466
751
|
|
|
467
752
|
// src/services/getNextRunDate.ts
|
|
468
753
|
var import_cron_parser = require("cron-parser");
|
|
@@ -492,176 +777,65 @@ var getNextRunDate = (options) => {
|
|
|
492
777
|
};
|
|
493
778
|
|
|
494
779
|
// src/services/EventsService.ts
|
|
495
|
-
var _jobsRepo_dec, _EventsService_decorators,
|
|
780
|
+
var _jobsRepo_dec, _EventsService_decorators, _init3;
|
|
496
781
|
_EventsService_decorators = [(0, import_services.Service)()], _jobsRepo_dec = [(0, import_services.Inject)(() => JobsRepo)];
|
|
497
782
|
var EventsService = class {
|
|
498
783
|
constructor() {
|
|
499
|
-
__publicField(this, "jobsRepo", __runInitializers(
|
|
784
|
+
__publicField(this, "jobsRepo", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
|
|
500
785
|
}
|
|
501
|
-
async scheduleJob(options) {
|
|
786
|
+
async scheduleJob(options, nPartitions) {
|
|
502
787
|
import_logger2.logger.debug("Scheduling job...", options);
|
|
788
|
+
const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
|
|
503
789
|
await this.jobsRepo.scheduleJob({
|
|
504
790
|
name: options.name,
|
|
505
791
|
priority: options.priority || 100,
|
|
506
792
|
nextRunAt: getNextRunDate(options),
|
|
507
793
|
params: options.params || null,
|
|
508
|
-
uniqueIdentifier: options.uniqueIdentifier
|
|
794
|
+
uniqueIdentifier: options.uniqueIdentifier,
|
|
795
|
+
partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
|
|
509
796
|
});
|
|
510
797
|
}
|
|
511
|
-
async scheduleJobs(jobs) {
|
|
798
|
+
async scheduleJobs(jobs, nPartitions) {
|
|
512
799
|
import_logger2.logger.debug(`Scheduling ${jobs.length} jobs...`);
|
|
513
|
-
const jobRecords = jobs.map((options) =>
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
800
|
+
const jobRecords = jobs.map((options) => {
|
|
801
|
+
const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
|
|
802
|
+
return {
|
|
803
|
+
name: options.name,
|
|
804
|
+
priority: options.priority || 100,
|
|
805
|
+
nextRunAt: getNextRunDate(options),
|
|
806
|
+
params: options.params || null,
|
|
807
|
+
uniqueIdentifier: options.uniqueIdentifier,
|
|
808
|
+
partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
|
|
809
|
+
};
|
|
810
|
+
});
|
|
520
811
|
return await this.jobsRepo.scheduleJobs(jobRecords);
|
|
521
812
|
}
|
|
522
813
|
};
|
|
523
|
-
|
|
524
|
-
__decorateElement(
|
|
525
|
-
EventsService = __decorateElement(
|
|
526
|
-
__runInitializers(
|
|
814
|
+
_init3 = __decoratorStart(null);
|
|
815
|
+
__decorateElement(_init3, 5, "jobsRepo", _jobsRepo_dec, EventsService);
|
|
816
|
+
EventsService = __decorateElement(_init3, 0, "EventsService", _EventsService_decorators, EventsService);
|
|
817
|
+
__runInitializers(_init3, 1, EventsService);
|
|
818
|
+
|
|
819
|
+
// src/schedule.ts
|
|
820
|
+
var eventsService = (0, import_services2.getInstance)(EventsService);
|
|
821
|
+
function scheduleJobInternal(options, nPartitions) {
|
|
822
|
+
return eventsService.scheduleJob(options, nPartitions);
|
|
823
|
+
}
|
|
824
|
+
function scheduleJobsInternal(jobs, nPartitions) {
|
|
825
|
+
return eventsService.scheduleJobs(jobs, nPartitions);
|
|
826
|
+
}
|
|
527
827
|
|
|
528
828
|
// src/services/WorkerService.ts
|
|
529
829
|
var import_helpers2 = require("@orion-js/helpers");
|
|
530
830
|
var import_logger4 = require("@orion-js/logger");
|
|
531
|
-
var
|
|
831
|
+
var import_services4 = require("@orion-js/services");
|
|
532
832
|
|
|
533
833
|
// src/services/Executor.ts
|
|
534
834
|
var import_api = require("@opentelemetry/api");
|
|
535
835
|
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
|
|
836
|
+
var import_services3 = require("@orion-js/services");
|
|
663
837
|
var _jobsHistoryRepo_dec, _jobsRepo_dec2, _Executor_decorators, _init4;
|
|
664
|
-
_Executor_decorators = [(0,
|
|
838
|
+
_Executor_decorators = [(0, import_services3.Service)()], _jobsRepo_dec2 = [(0, import_services3.Inject)(() => JobsRepo)], _jobsHistoryRepo_dec = [(0, import_services3.Inject)(() => JobsHistoryRepo)];
|
|
665
839
|
var Executor = class {
|
|
666
840
|
constructor() {
|
|
667
841
|
__publicField(this, "jobsRepo", __runInitializers(_init4, 8, this)), __runInitializers(_init4, 11, this);
|
|
@@ -999,11 +1173,16 @@ Executor = __decorateElement(_init4, 0, "Executor", _Executor_decorators, Execut
|
|
|
999
1173
|
__runInitializers(_init4, 1, Executor);
|
|
1000
1174
|
|
|
1001
1175
|
// src/services/WorkerService.ts
|
|
1176
|
+
var FILTERED_JOB_NAMES_ACQUISITION_HINT = "byJobName";
|
|
1002
1177
|
var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
|
|
1003
|
-
_WorkerService_decorators = [(0,
|
|
1178
|
+
_WorkerService_decorators = [(0, import_services4.Service)()], _jobsRepo_dec3 = [(0, import_services4.Inject)(() => JobsRepo)], _executor_dec = [(0, import_services4.Inject)(() => Executor)];
|
|
1004
1179
|
var WorkerService = class {
|
|
1005
1180
|
constructor() {
|
|
1006
1181
|
__publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
|
|
1182
|
+
__publicField(this, "partitionReconcilerIntervalMs", 5 * 60 * 1e3);
|
|
1183
|
+
__publicField(this, "partitionReconcilerJitterMs", 30 * 1e3);
|
|
1184
|
+
__publicField(this, "partitionReconcilerBatchPauseMs", 250);
|
|
1185
|
+
__publicField(this, "partitionReconcilerBatchSize", 500);
|
|
1007
1186
|
__publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
|
|
1008
1187
|
__publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
|
|
1009
1188
|
}
|
|
@@ -1088,6 +1267,15 @@ var WorkerService = class {
|
|
|
1088
1267
|
if (capacityPromises.length === 0) return;
|
|
1089
1268
|
await Promise.race([...capacityPromises, workersInstance.stopSignal]);
|
|
1090
1269
|
}
|
|
1270
|
+
getNextPartition(config, workersInstance) {
|
|
1271
|
+
const partition = workersInstance.partitionOrder[workersInstance.partitionCursor];
|
|
1272
|
+
workersInstance.partitionCursor++;
|
|
1273
|
+
if (workersInstance.partitionCursor >= config.nPartitions) {
|
|
1274
|
+
workersInstance.partitionOrder = createShuffledPartitions(config.nPartitions);
|
|
1275
|
+
workersInstance.partitionCursor = 0;
|
|
1276
|
+
}
|
|
1277
|
+
return partition;
|
|
1278
|
+
}
|
|
1091
1279
|
async runSchedulerLoop(config, workersInstance) {
|
|
1092
1280
|
const jobNames = this.getJobNames(config.jobs);
|
|
1093
1281
|
const executeConfig = {
|
|
@@ -1097,6 +1285,7 @@ var WorkerService = class {
|
|
|
1097
1285
|
onMaxTriesReached: config.onMaxTriesReached
|
|
1098
1286
|
};
|
|
1099
1287
|
import_logger4.logger.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
|
|
1288
|
+
let consecutivePartitionMisses = 0;
|
|
1100
1289
|
while (workersInstance.running) {
|
|
1101
1290
|
let didMiss = false;
|
|
1102
1291
|
let allJobNamesAtCapacity = false;
|
|
@@ -1107,21 +1296,31 @@ var WorkerService = class {
|
|
|
1107
1296
|
allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
|
|
1108
1297
|
break;
|
|
1109
1298
|
}
|
|
1299
|
+
const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
|
|
1300
|
+
const partition = this.getNextPartition(config, workersInstance);
|
|
1110
1301
|
const jobToRun = await this.jobsRepo.getJobAndLock(
|
|
1111
1302
|
availableJobNames,
|
|
1112
1303
|
config.defaultLockTime,
|
|
1113
|
-
|
|
1304
|
+
jobAcquisitionHint,
|
|
1305
|
+
partition
|
|
1114
1306
|
);
|
|
1115
1307
|
if (!jobToRun) {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1308
|
+
consecutivePartitionMisses++;
|
|
1309
|
+
import_logger4.logger.debug(`No job to run in partition ${partition}`);
|
|
1310
|
+
if (consecutivePartitionMisses >= config.nPartitions) {
|
|
1311
|
+
consecutivePartitionMisses = 0;
|
|
1312
|
+
didMiss = true;
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
continue;
|
|
1119
1316
|
}
|
|
1317
|
+
consecutivePartitionMisses = 0;
|
|
1120
1318
|
this.startExecution(executeConfig, workersInstance, jobToRun);
|
|
1121
1319
|
if (!workersInstance.running) break;
|
|
1122
1320
|
}
|
|
1123
1321
|
} catch (error) {
|
|
1124
1322
|
import_logger4.logger.error("Error in job scheduler.", { error });
|
|
1323
|
+
consecutivePartitionMisses = 0;
|
|
1125
1324
|
didMiss = true;
|
|
1126
1325
|
}
|
|
1127
1326
|
if (!workersInstance.running) return;
|
|
@@ -1157,11 +1356,13 @@ var WorkerService = class {
|
|
|
1157
1356
|
workersInstance.jobAcquisitionHintProbeCycle++;
|
|
1158
1357
|
try {
|
|
1159
1358
|
for (let sample = 0; sample < 3; sample++) {
|
|
1359
|
+
const partition = getRandomPartition(config.nPartitions);
|
|
1160
1360
|
for (const hint of [firstHint, secondHint]) {
|
|
1161
1361
|
if (!workersInstance.running) return;
|
|
1162
1362
|
const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
|
|
1163
1363
|
jobNames,
|
|
1164
|
-
hint
|
|
1364
|
+
hint,
|
|
1365
|
+
partition
|
|
1165
1366
|
);
|
|
1166
1367
|
if (!workersInstance.running) return;
|
|
1167
1368
|
byHint[hint].push(executionTimeMillis);
|
|
@@ -1233,6 +1434,44 @@ var WorkerService = class {
|
|
|
1233
1434
|
await this.waitForNextJobAcquisitionHintProbe(workersInstance);
|
|
1234
1435
|
}
|
|
1235
1436
|
}
|
|
1437
|
+
async reconcilePartitionBatch(config) {
|
|
1438
|
+
if (typeof this.jobsRepo.reconcilePartitions !== "function") return 0;
|
|
1439
|
+
try {
|
|
1440
|
+
return await this.jobsRepo.reconcilePartitions({
|
|
1441
|
+
jobNames: this.getJobNames(config.jobs),
|
|
1442
|
+
nPartitions: config.nPartitions,
|
|
1443
|
+
batchSize: this.partitionReconcilerBatchSize
|
|
1444
|
+
});
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
import_logger4.logger.warn("Job partition reconciliation failed; it will be retried.", { error });
|
|
1447
|
+
return 0;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
async waitForNextPartitionReconcile(workersInstance, delayMs) {
|
|
1451
|
+
await new Promise((resolve) => {
|
|
1452
|
+
let resolved = false;
|
|
1453
|
+
const finishWait = () => {
|
|
1454
|
+
if (resolved) return;
|
|
1455
|
+
resolved = true;
|
|
1456
|
+
clearTimeout(workersInstance.partitionReconcilerTimer);
|
|
1457
|
+
workersInstance.partitionReconcilerTimer = void 0;
|
|
1458
|
+
workersInstance.cancelPartitionReconcilerWait = void 0;
|
|
1459
|
+
resolve();
|
|
1460
|
+
};
|
|
1461
|
+
workersInstance.partitionReconcilerTimer = setTimeout(finishWait, delayMs);
|
|
1462
|
+
workersInstance.cancelPartitionReconcilerWait = finishWait;
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
async runPartitionReconcilerLoop(config, workersInstance, initialReconcilePromise) {
|
|
1466
|
+
let modifiedCount = await initialReconcilePromise;
|
|
1467
|
+
while (workersInstance.running) {
|
|
1468
|
+
const didFillBatch = modifiedCount >= this.partitionReconcilerBatchSize;
|
|
1469
|
+
const delayMs = didFillBatch ? this.partitionReconcilerBatchPauseMs : this.partitionReconcilerIntervalMs + Math.floor(Math.random() * (this.partitionReconcilerJitterMs + 1));
|
|
1470
|
+
await this.waitForNextPartitionReconcile(workersInstance, delayMs);
|
|
1471
|
+
if (!workersInstance.running) return;
|
|
1472
|
+
modifiedCount = await this.reconcilePartitionBatch(config);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1236
1475
|
createWorkersInstanceDefinition(config) {
|
|
1237
1476
|
let signalStop;
|
|
1238
1477
|
const stopSignal = new Promise((resolve) => {
|
|
@@ -1248,7 +1487,10 @@ var WorkerService = class {
|
|
|
1248
1487
|
jobAcquisitionHintProbeCycle: 0,
|
|
1249
1488
|
hasCompletedInitialJobAcquisitionHintProbe: false,
|
|
1250
1489
|
pendingJobAcquisitionHintWins: 0,
|
|
1490
|
+
partitionOrder: createShuffledPartitions(config.nPartitions),
|
|
1491
|
+
partitionCursor: 0,
|
|
1251
1492
|
jobAcquisitionHintProbePromise: Promise.resolve(),
|
|
1493
|
+
partitionReconcilerPromise: Promise.resolve(),
|
|
1252
1494
|
schedulerPromise: Promise.resolve(),
|
|
1253
1495
|
stopSignal,
|
|
1254
1496
|
signalStop,
|
|
@@ -1258,14 +1500,16 @@ var WorkerService = class {
|
|
|
1258
1500
|
stop: async () => {
|
|
1259
1501
|
if (workersInstance.stopPromise) return workersInstance.stopPromise;
|
|
1260
1502
|
workersInstance.stopPromise = (async () => {
|
|
1261
|
-
var _a;
|
|
1503
|
+
var _a, _b;
|
|
1262
1504
|
import_logger4.logger.info("Stopping job scheduler...");
|
|
1263
1505
|
workersInstance.running = false;
|
|
1264
1506
|
workersInstance.signalStop();
|
|
1265
1507
|
(_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
|
|
1508
|
+
(_b = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b.call(workersInstance);
|
|
1266
1509
|
await Promise.all([
|
|
1267
1510
|
workersInstance.schedulerPromise,
|
|
1268
|
-
workersInstance.jobAcquisitionHintProbePromise
|
|
1511
|
+
workersInstance.jobAcquisitionHintProbePromise,
|
|
1512
|
+
workersInstance.partitionReconcilerPromise
|
|
1269
1513
|
]);
|
|
1270
1514
|
const activeCapacity = Array.from(
|
|
1271
1515
|
workersInstance.activeExecutions.values(),
|
|
@@ -1283,7 +1527,7 @@ var WorkerService = class {
|
|
|
1283
1527
|
await Promise.all(
|
|
1284
1528
|
jobs.filter((job) => job.type === "recurrent").map(async (job) => {
|
|
1285
1529
|
import_logger4.logger.debug(`Ensuring records for job "${job.name}"...`);
|
|
1286
|
-
await this.jobsRepo.ensureJobRecord(job);
|
|
1530
|
+
await this.jobsRepo.ensureJobRecord(job, getRandomPartition(config.nPartitions));
|
|
1287
1531
|
})
|
|
1288
1532
|
);
|
|
1289
1533
|
}
|
|
@@ -1300,29 +1544,41 @@ var WorkerService = class {
|
|
|
1300
1544
|
* @param userConfig - Configuration for the scheduler. Required field: jobs
|
|
1301
1545
|
*/
|
|
1302
1546
|
startWorkers(userConfig) {
|
|
1547
|
+
var _a, _b;
|
|
1303
1548
|
const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
|
|
1304
1549
|
if (maxTriesReachedRetentionMs !== null && (!Number.isFinite(maxTriesReachedRetentionMs) || maxTriesReachedRetentionMs < 0)) {
|
|
1305
1550
|
throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
|
|
1306
1551
|
}
|
|
1552
|
+
const nPartitions = userConfig.nPartitions ?? 1;
|
|
1553
|
+
validatePartitionsCount(nPartitions);
|
|
1307
1554
|
const config = {
|
|
1308
1555
|
pollInterval: 3e3,
|
|
1309
1556
|
workersCount: 4,
|
|
1310
1557
|
defaultLockTime: 30 * 1e3,
|
|
1311
1558
|
...userConfig,
|
|
1559
|
+
nPartitions,
|
|
1312
1560
|
maxTriesReachedRetentionMs
|
|
1313
1561
|
};
|
|
1314
|
-
setNameToJobs(config.jobs);
|
|
1562
|
+
setNameToJobs(config.jobs, config.nPartitions);
|
|
1563
|
+
(_b = (_a = this.jobsRepo).configureJobPartitions) == null ? void 0 : _b.call(_a, this.getJobNames(config.jobs), config.nPartitions);
|
|
1315
1564
|
const workersInstance = this.createWorkersInstanceDefinition(config);
|
|
1316
1565
|
import_logger4.logger.debug("Starting job scheduler", config);
|
|
1317
1566
|
workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
|
|
1318
1567
|
config,
|
|
1319
1568
|
workersInstance
|
|
1320
1569
|
);
|
|
1570
|
+
const initialReconcilePromise = this.reconcilePartitionBatch(config);
|
|
1571
|
+
workersInstance.partitionReconcilerPromise = this.runPartitionReconcilerLoop(
|
|
1572
|
+
config,
|
|
1573
|
+
workersInstance,
|
|
1574
|
+
initialReconcilePromise
|
|
1575
|
+
);
|
|
1321
1576
|
workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
|
|
1322
|
-
var
|
|
1577
|
+
var _a2, _b2;
|
|
1323
1578
|
workersInstance.running = false;
|
|
1324
1579
|
workersInstance.signalStop();
|
|
1325
|
-
(
|
|
1580
|
+
(_a2 = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a2.call(workersInstance);
|
|
1581
|
+
(_b2 = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b2.call(workersInstance);
|
|
1326
1582
|
import_logger4.logger.error("Job scheduler stopped after an unexpected error.", { error });
|
|
1327
1583
|
});
|
|
1328
1584
|
return workersInstance;
|
|
@@ -1333,15 +1589,13 @@ __decorateElement(_init5, 5, "jobsRepo", _jobsRepo_dec3, WorkerService);
|
|
|
1333
1589
|
__decorateElement(_init5, 5, "executor", _executor_dec, WorkerService);
|
|
1334
1590
|
WorkerService = __decorateElement(_init5, 0, "WorkerService", _WorkerService_decorators, WorkerService);
|
|
1335
1591
|
__runInitializers(_init5, 1, WorkerService);
|
|
1336
|
-
function setNameToJobs(jobs) {
|
|
1592
|
+
function setNameToJobs(jobs, nPartitions) {
|
|
1337
1593
|
for (const name of Object.keys(jobs)) {
|
|
1338
1594
|
jobs[name].jobName = name;
|
|
1595
|
+
jobs[name].nPartitions = nPartitions;
|
|
1339
1596
|
}
|
|
1340
1597
|
}
|
|
1341
1598
|
|
|
1342
|
-
// src/service/index.ts
|
|
1343
|
-
var import_services4 = require("@orion-js/services");
|
|
1344
|
-
|
|
1345
1599
|
// src/defineJob/index.ts
|
|
1346
1600
|
var import_schema3 = require("@orion-js/schema");
|
|
1347
1601
|
var import_cron_parser2 = require("cron-parser");
|
|
@@ -1358,11 +1612,14 @@ function createEventJob(options) {
|
|
|
1358
1612
|
throw new Error("This job has not been registered in the workers");
|
|
1359
1613
|
}
|
|
1360
1614
|
const params = jobDefinition.params ? await (0, import_schema3.cleanAndValidate)(jobDefinition.params, scheduleOptions.params) : scheduleOptions.params;
|
|
1361
|
-
return await
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1615
|
+
return await scheduleJobInternal(
|
|
1616
|
+
{
|
|
1617
|
+
...scheduleOptions,
|
|
1618
|
+
name: jobDefinition.jobName,
|
|
1619
|
+
params
|
|
1620
|
+
},
|
|
1621
|
+
jobDefinition.nPartitions
|
|
1622
|
+
);
|
|
1366
1623
|
};
|
|
1367
1624
|
jobDefinition.scheduleJobs = async (jobs) => {
|
|
1368
1625
|
if (!jobDefinition.jobName) {
|
|
@@ -1378,7 +1635,7 @@ function createEventJob(options) {
|
|
|
1378
1635
|
};
|
|
1379
1636
|
})
|
|
1380
1637
|
);
|
|
1381
|
-
return await
|
|
1638
|
+
return await scheduleJobsInternal(processedJobs, jobDefinition.nPartitions);
|
|
1382
1639
|
};
|
|
1383
1640
|
return jobDefinition;
|
|
1384
1641
|
}
|
|
@@ -1407,13 +1664,14 @@ var defineJob = (options) => {
|
|
|
1407
1664
|
};
|
|
1408
1665
|
|
|
1409
1666
|
// src/service/index.ts
|
|
1667
|
+
var import_services5 = require("@orion-js/services");
|
|
1410
1668
|
var serviceMetadata = /* @__PURE__ */ new WeakMap();
|
|
1411
1669
|
var jobsMetadata = /* @__PURE__ */ new Map();
|
|
1412
1670
|
var jobEntriesByClass = /* @__PURE__ */ new Map();
|
|
1413
1671
|
var pendingJobEntries = {};
|
|
1414
1672
|
function Jobs() {
|
|
1415
1673
|
return (target, context) => {
|
|
1416
|
-
(0,
|
|
1674
|
+
(0, import_services5.Service)()(target, context);
|
|
1417
1675
|
serviceMetadata.set(target, { _serviceType: "jobs" });
|
|
1418
1676
|
if (Object.keys(pendingJobEntries).length > 0) {
|
|
1419
1677
|
jobEntriesByClass.set(target, pendingJobEntries);
|
|
@@ -1462,7 +1720,7 @@ function initializeJobsIfNeeded(instance) {
|
|
|
1462
1720
|
jobsMetadata.set(instance, jobs);
|
|
1463
1721
|
}
|
|
1464
1722
|
function getServiceJobs(target) {
|
|
1465
|
-
const instance = (0,
|
|
1723
|
+
const instance = (0, import_services5.getInstance)(target);
|
|
1466
1724
|
if (!serviceMetadata.has(instance.constructor)) {
|
|
1467
1725
|
throw new Error("You must pass a class decorated with @Jobs to getServiceJobs");
|
|
1468
1726
|
}
|
|
@@ -1476,18 +1734,17 @@ function getServiceJobs(target) {
|
|
|
1476
1734
|
}
|
|
1477
1735
|
|
|
1478
1736
|
// src/index.ts
|
|
1479
|
-
var workerService = (0,
|
|
1480
|
-
var
|
|
1481
|
-
var
|
|
1482
|
-
var jobsRepo = (0, import_services5.getInstance)(JobsRepo);
|
|
1737
|
+
var workerService = (0, import_services6.getInstance)(WorkerService);
|
|
1738
|
+
var jobsHistoryRepo = (0, import_services6.getInstance)(JobsHistoryRepo);
|
|
1739
|
+
var jobsRepo = (0, import_services6.getInstance)(JobsRepo);
|
|
1483
1740
|
var startWorkers = (config) => {
|
|
1484
1741
|
return workerService.startWorkers(config);
|
|
1485
1742
|
};
|
|
1486
1743
|
var scheduleJob = (options) => {
|
|
1487
|
-
return
|
|
1744
|
+
return scheduleJobInternal(options);
|
|
1488
1745
|
};
|
|
1489
1746
|
var scheduleJobs = (jobs) => {
|
|
1490
|
-
return
|
|
1747
|
+
return scheduleJobsInternal(jobs);
|
|
1491
1748
|
};
|
|
1492
1749
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1493
1750
|
0 && (module.exports = {
|