@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.js
CHANGED
|
@@ -48,27 +48,169 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
|
|
|
48
48
|
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
49
49
|
|
|
50
50
|
// src/index.ts
|
|
51
|
-
import { getInstance as
|
|
51
|
+
import { getInstance as getInstance3 } from "@orion-js/services";
|
|
52
52
|
|
|
53
|
-
// src/
|
|
54
|
-
import {
|
|
55
|
-
|
|
53
|
+
// src/repos/JobsHistoryRepo.ts
|
|
54
|
+
import { MongoCollection, Repository } from "@orion-js/mongodb";
|
|
55
|
+
|
|
56
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/isInteger.js
|
|
57
|
+
function _isInteger(n) {
|
|
58
|
+
return n << 0 === n;
|
|
59
|
+
}
|
|
60
|
+
var isInteger = Number.isInteger || _isInteger;
|
|
61
|
+
|
|
62
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/createPath.js
|
|
63
|
+
function createPath(path, delimiter = ".") {
|
|
64
|
+
return typeof path === "string" ? path.split(delimiter).map((x) => isInteger(x) ? Number(x) : x) : path;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/compare.js
|
|
68
|
+
function compare(a, b) {
|
|
69
|
+
return String(a) === String(b);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/includes.js
|
|
73
|
+
function includes(a, list) {
|
|
74
|
+
let index = -1;
|
|
75
|
+
const { length } = list;
|
|
76
|
+
while (++index < length)
|
|
77
|
+
if (compare(list[index], a))
|
|
78
|
+
return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/omit.js
|
|
83
|
+
function omit(propsToOmit, obj) {
|
|
84
|
+
if (arguments.length === 1) return (_obj) => omit(propsToOmit, _obj);
|
|
85
|
+
if (obj === null || obj === void 0)
|
|
86
|
+
return void 0;
|
|
87
|
+
const propsToOmitValue = createPath(propsToOmit, ",");
|
|
88
|
+
const willReturn = {};
|
|
89
|
+
for (const key in obj)
|
|
90
|
+
if (!includes(key, propsToOmitValue))
|
|
91
|
+
willReturn[key] = obj[key];
|
|
92
|
+
return willReturn;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/types/HistoryRecord.ts
|
|
96
|
+
import { schemaWithName } from "@orion-js/schema";
|
|
97
|
+
var HistoryRecordSchema = schemaWithName("HistoryRecord", {
|
|
98
|
+
_id: { type: "string" },
|
|
99
|
+
jobId: { type: "string" },
|
|
100
|
+
executionId: { type: "string" },
|
|
101
|
+
jobName: { type: "string" },
|
|
102
|
+
type: { type: "string" },
|
|
103
|
+
priority: { type: "number" },
|
|
104
|
+
tries: { type: "number" },
|
|
105
|
+
uniqueIdentifier: { type: "string", optional: true },
|
|
106
|
+
startedAt: { type: "date" },
|
|
107
|
+
endedAt: { type: "date" },
|
|
108
|
+
duration: { type: "number" },
|
|
109
|
+
expiresAt: { type: "date", optional: true },
|
|
110
|
+
status: { type: "string", enum: ["success", "error", "stale"] },
|
|
111
|
+
errorMessage: { type: "string", optional: true },
|
|
112
|
+
params: { type: "blackbox", optional: true },
|
|
113
|
+
result: { type: "any", optional: true }
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// src/repos/JobsHistoryRepo.ts
|
|
117
|
+
var _history_dec, _JobsHistoryRepo_decorators, _init;
|
|
118
|
+
_JobsHistoryRepo_decorators = [Repository()], _history_dec = [MongoCollection({
|
|
119
|
+
name: "orionjs.jobs_dogs_history",
|
|
120
|
+
idGeneration: "uuid",
|
|
121
|
+
schema: HistoryRecordSchema,
|
|
122
|
+
indexes: [
|
|
123
|
+
{
|
|
124
|
+
keys: {
|
|
125
|
+
jobName: 1,
|
|
126
|
+
startedAt: 1
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
keys: {
|
|
131
|
+
executionId: 1
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
keys: {
|
|
136
|
+
expiresAt: 1
|
|
137
|
+
},
|
|
138
|
+
options: {
|
|
139
|
+
expireAfterSeconds: 0
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
})];
|
|
144
|
+
var JobsHistoryRepo = class {
|
|
145
|
+
constructor() {
|
|
146
|
+
__publicField(this, "history", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
|
|
147
|
+
}
|
|
148
|
+
async saveExecution(record) {
|
|
149
|
+
await this.history.upsert(
|
|
150
|
+
{ executionId: record.executionId },
|
|
151
|
+
{
|
|
152
|
+
$setOnInsert: {
|
|
153
|
+
status: record.status
|
|
154
|
+
},
|
|
155
|
+
$set: {
|
|
156
|
+
...omit(["status"], record)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
async getExecutions(jobName, limit, skip) {
|
|
162
|
+
const cursor = this.history.find({ jobName }).sort({ startedAt: -1 });
|
|
163
|
+
if (skip) {
|
|
164
|
+
cursor.skip(skip);
|
|
165
|
+
}
|
|
166
|
+
if (limit) {
|
|
167
|
+
cursor.limit(limit);
|
|
168
|
+
}
|
|
169
|
+
return await cursor.toArray();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
_init = __decoratorStart(null);
|
|
173
|
+
__decorateElement(_init, 5, "history", _history_dec, JobsHistoryRepo);
|
|
174
|
+
JobsHistoryRepo = __decorateElement(_init, 0, "JobsHistoryRepo", _JobsHistoryRepo_decorators, JobsHistoryRepo);
|
|
175
|
+
__runInitializers(_init, 1, JobsHistoryRepo);
|
|
56
176
|
|
|
57
177
|
// src/repos/JobsRepo.ts
|
|
58
178
|
import { generateId } from "@orion-js/helpers";
|
|
59
179
|
import { logger } from "@orion-js/logger";
|
|
60
|
-
import { MongoCollection, Repository } from "@orion-js/mongodb";
|
|
180
|
+
import { MongoCollection as MongoCollection2, Repository as Repository2 } from "@orion-js/mongodb";
|
|
181
|
+
|
|
182
|
+
// src/services/partitions.ts
|
|
183
|
+
function validatePartitionsCount(nPartitions) {
|
|
184
|
+
if (!Number.isInteger(nPartitions) || nPartitions <= 0) {
|
|
185
|
+
throw new Error("nPartitions must be a positive integer");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function getRandomPartition(nPartitions) {
|
|
189
|
+
return Math.floor(Math.random() * nPartitions);
|
|
190
|
+
}
|
|
191
|
+
function createShuffledPartitions(nPartitions) {
|
|
192
|
+
const partitions = Array.from({ length: nPartitions }, (_, partition) => partition);
|
|
193
|
+
for (let index = partitions.length - 1; index > 0; index--) {
|
|
194
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
195
|
+
[partitions[index], partitions[swapIndex]] = [partitions[swapIndex], partitions[index]];
|
|
196
|
+
}
|
|
197
|
+
return partitions;
|
|
198
|
+
}
|
|
61
199
|
|
|
62
200
|
// src/types/JobRecord.ts
|
|
63
|
-
import { createEnum, schemaWithName } from "@orion-js/schema";
|
|
201
|
+
import { createEnum, schemaWithName as schemaWithName2 } from "@orion-js/schema";
|
|
64
202
|
var JobStatusEnum = createEnum("JobStatus", ["pending", "maxTriesReached"]);
|
|
65
|
-
var JobRecordSchema =
|
|
203
|
+
var JobRecordSchema = schemaWithName2("JobRecord", {
|
|
66
204
|
_id: { type: "string" },
|
|
67
205
|
jobName: { type: "string" },
|
|
68
206
|
type: { type: createEnum("JobType", ["recurrent", "event"]) },
|
|
69
207
|
priority: { type: "number" },
|
|
70
208
|
uniqueIdentifier: { type: "string", optional: true },
|
|
71
209
|
nextRunAt: { type: "date" },
|
|
210
|
+
/**
|
|
211
|
+
* Acquisition partition. Optional so records created by previous versions can be reconciled.
|
|
212
|
+
*/
|
|
213
|
+
partition: { type: "number", optional: true },
|
|
72
214
|
lastRunAt: { type: "date", optional: true },
|
|
73
215
|
lockedUntil: { type: "date", optional: true },
|
|
74
216
|
/**
|
|
@@ -76,6 +218,11 @@ var JobRecordSchema = schemaWithName("JobRecord", {
|
|
|
76
218
|
* this value so a stale execution cannot modify a newer claim.
|
|
77
219
|
*/
|
|
78
220
|
lockId: { type: "string", optional: true },
|
|
221
|
+
/**
|
|
222
|
+
* Internal acquisition metadata used when MongoDB supports sorted updateOne. It preserves the
|
|
223
|
+
* pre-update stale state because updateOne intentionally does not return the previous document.
|
|
224
|
+
*/
|
|
225
|
+
claimWasStale: { type: "boolean", optional: true },
|
|
79
226
|
tries: { type: "number", optional: true },
|
|
80
227
|
params: { type: "blackbox", optional: true },
|
|
81
228
|
/**
|
|
@@ -100,23 +247,30 @@ var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
|
|
|
100
247
|
// src/repos/JobsRepo.ts
|
|
101
248
|
var JOB_ACQUISITION_HINTS = {
|
|
102
249
|
byJobName: {
|
|
250
|
+
partition: 1,
|
|
103
251
|
jobName: 1,
|
|
104
252
|
priority: -1,
|
|
105
253
|
nextRunAt: 1
|
|
106
254
|
},
|
|
107
255
|
globalPriority: {
|
|
256
|
+
partition: 1,
|
|
108
257
|
priority: -1,
|
|
109
258
|
nextRunAt: 1
|
|
110
259
|
}
|
|
111
260
|
};
|
|
112
261
|
var INITIAL_JOB_ACQUISITION_HINT = "byJobName";
|
|
113
262
|
var CANDIDATE_JOB_ACQUISITION_HINT = "globalPriority";
|
|
263
|
+
var SORTED_UPDATE_ONE_MIN_WIRE_VERSION = 25;
|
|
264
|
+
var LOCK_ID_INDEX_NAME = "jobs_dogs_records_lock_id";
|
|
265
|
+
var PARTITION_JOB_NAME_INDEX_NAME = "jobs_dogs_records_partition_job_name_priority_next_run";
|
|
266
|
+
var PARTITION_PRIORITY_INDEX_NAME = "jobs_dogs_records_partition_priority_next_run";
|
|
114
267
|
var JOB_ACQUISITION_SORT = {
|
|
115
268
|
priority: -1,
|
|
116
269
|
nextRunAt: 1
|
|
117
270
|
};
|
|
118
|
-
function getJobAcquisitionSelector(jobNames, now) {
|
|
271
|
+
function getJobAcquisitionSelector(jobNames, now, partition) {
|
|
119
272
|
return {
|
|
273
|
+
partition,
|
|
120
274
|
jobName: { $in: jobNames },
|
|
121
275
|
nextRunAt: { $lte: now },
|
|
122
276
|
$and: [
|
|
@@ -127,17 +281,19 @@ function getJobAcquisitionSelector(jobNames, now) {
|
|
|
127
281
|
]
|
|
128
282
|
};
|
|
129
283
|
}
|
|
130
|
-
var _jobs_dec, _JobsRepo_decorators,
|
|
131
|
-
_JobsRepo_decorators = [
|
|
284
|
+
var _jobs_dec, _JobsRepo_decorators, _init2;
|
|
285
|
+
_JobsRepo_decorators = [Repository2()], _jobs_dec = [MongoCollection2({
|
|
132
286
|
idGeneration: "uuid",
|
|
133
287
|
name: "orionjs.jobs_dogs_records",
|
|
134
288
|
schema: JobRecordSchema,
|
|
135
289
|
indexes: [
|
|
136
290
|
{
|
|
137
|
-
keys: JOB_ACQUISITION_HINTS.byJobName
|
|
291
|
+
keys: JOB_ACQUISITION_HINTS.byJobName,
|
|
292
|
+
options: { name: PARTITION_JOB_NAME_INDEX_NAME }
|
|
138
293
|
},
|
|
139
294
|
{
|
|
140
|
-
keys: JOB_ACQUISITION_HINTS.globalPriority
|
|
295
|
+
keys: JOB_ACQUISITION_HINTS.globalPriority,
|
|
296
|
+
options: { name: PARTITION_PRIORITY_INDEX_NAME }
|
|
141
297
|
},
|
|
142
298
|
{
|
|
143
299
|
keys: {
|
|
@@ -157,6 +313,15 @@ _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
|
|
|
157
313
|
sparse: true
|
|
158
314
|
}
|
|
159
315
|
},
|
|
316
|
+
{
|
|
317
|
+
keys: {
|
|
318
|
+
lockId: 1
|
|
319
|
+
},
|
|
320
|
+
options: {
|
|
321
|
+
name: LOCK_ID_INDEX_NAME,
|
|
322
|
+
sparse: true
|
|
323
|
+
}
|
|
324
|
+
},
|
|
160
325
|
{
|
|
161
326
|
keys: {
|
|
162
327
|
expiresAt: 1
|
|
@@ -168,14 +333,55 @@ _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
|
|
|
168
333
|
})];
|
|
169
334
|
var JobsRepo = class {
|
|
170
335
|
constructor() {
|
|
171
|
-
__publicField(this, "jobs", __runInitializers(
|
|
336
|
+
__publicField(this, "jobs", __runInitializers(_init2, 8, this)), __runInitializers(_init2, 11, this);
|
|
337
|
+
__publicField(this, "sortedUpdateOneSupportPromise");
|
|
338
|
+
__publicField(this, "partitionsByJobName", /* @__PURE__ */ new Map());
|
|
339
|
+
}
|
|
340
|
+
configureJobPartitions(jobNames, nPartitions) {
|
|
341
|
+
for (const jobName of jobNames) {
|
|
342
|
+
this.partitionsByJobName.set(jobName, nPartitions);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
getJobPartitionsCount(jobName) {
|
|
346
|
+
return this.partitionsByJobName.get(jobName);
|
|
347
|
+
}
|
|
348
|
+
async supportsSortedUpdateOne() {
|
|
349
|
+
if (!this.sortedUpdateOneSupportPromise) {
|
|
350
|
+
this.sortedUpdateOneSupportPromise = this.detectSortedUpdateOneSupport();
|
|
351
|
+
}
|
|
352
|
+
return this.sortedUpdateOneSupportPromise;
|
|
353
|
+
}
|
|
354
|
+
async detectSortedUpdateOneSupport() {
|
|
355
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
356
|
+
await this.jobs.createIndexesPromise;
|
|
357
|
+
const hello = await rawCollection.db.admin().command({ hello: 1 });
|
|
358
|
+
const supportsSortedUpdateOne = typeof hello.maxWireVersion === "number" && hello.maxWireVersion >= SORTED_UPDATE_ONE_MIN_WIRE_VERSION;
|
|
359
|
+
if (!supportsSortedUpdateOne) {
|
|
360
|
+
logger.info("MongoDB does not support sorted updateOne; using findOneAndUpdate for jobs.");
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
const hasLockIdIndex = await rawCollection.indexExists(LOCK_ID_INDEX_NAME);
|
|
364
|
+
if (!hasLockIdIndex) {
|
|
365
|
+
logger.warn(
|
|
366
|
+
`MongoDB supports sorted updateOne but index "${LOCK_ID_INDEX_NAME}" is unavailable; using findOneAndUpdate for jobs.`
|
|
367
|
+
);
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
logger.info("MongoDB supports sorted updateOne; using it for job acquisition.");
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
async getJobAndLock(jobNames, lockTime, hint = INITIAL_JOB_ACQUISITION_HINT, partition = 0) {
|
|
374
|
+
if (await this.supportsSortedUpdateOne()) {
|
|
375
|
+
return this.getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition);
|
|
376
|
+
}
|
|
377
|
+
return this.getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition);
|
|
172
378
|
}
|
|
173
|
-
async
|
|
379
|
+
async getJobAndLockWithFindOneAndUpdate(jobNames, lockTime, hint, partition) {
|
|
174
380
|
const executionId = generateId();
|
|
175
381
|
const now = /* @__PURE__ */ new Date();
|
|
176
382
|
const lockedUntil = new Date(now.getTime() + lockTime);
|
|
177
383
|
const job = await this.jobs.findOneAndUpdate(
|
|
178
|
-
getJobAcquisitionSelector(jobNames, now),
|
|
384
|
+
getJobAcquisitionSelector(jobNames, now, partition),
|
|
179
385
|
{
|
|
180
386
|
$set: { lockId: executionId, lockedUntil, lastRunAt: now },
|
|
181
387
|
$inc: { tries: 1 }
|
|
@@ -191,6 +397,37 @@ var JobsRepo = class {
|
|
|
191
397
|
if (!job) return;
|
|
192
398
|
const tries = (job.tries || 0) + 1;
|
|
193
399
|
const wasStale = Boolean(job.lockedUntil);
|
|
400
|
+
return this.toJobToRun(job, executionId, lockTime, tries, wasStale);
|
|
401
|
+
}
|
|
402
|
+
async getJobAndLockWithSortedUpdateOne(jobNames, lockTime, hint, partition) {
|
|
403
|
+
const executionId = generateId();
|
|
404
|
+
const now = /* @__PURE__ */ new Date();
|
|
405
|
+
const lockedUntil = new Date(now.getTime() + lockTime);
|
|
406
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
407
|
+
const result = await rawCollection.updateOne(
|
|
408
|
+
getJobAcquisitionSelector(jobNames, now, partition),
|
|
409
|
+
[
|
|
410
|
+
{
|
|
411
|
+
$set: {
|
|
412
|
+
lockId: { $literal: executionId },
|
|
413
|
+
lockedUntil: { $literal: lockedUntil },
|
|
414
|
+
lastRunAt: { $literal: now },
|
|
415
|
+
tries: { $add: [{ $ifNull: ["$tries", 0] }, 1] },
|
|
416
|
+
claimWasStale: { $eq: [{ $type: "$lockedUntil" }, "date"] }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
],
|
|
420
|
+
{
|
|
421
|
+
hint: JOB_ACQUISITION_HINTS[hint],
|
|
422
|
+
sort: JOB_ACQUISITION_SORT
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
if (result.matchedCount === 0) return;
|
|
426
|
+
const job = await rawCollection.findOne({ lockId: executionId });
|
|
427
|
+
if (!job) return;
|
|
428
|
+
return this.toJobToRun(job, executionId, lockTime, job.tries || 0, Boolean(job.claimWasStale));
|
|
429
|
+
}
|
|
430
|
+
toJobToRun(job, executionId, lockTime, tries, wasStale) {
|
|
194
431
|
if (wasStale) {
|
|
195
432
|
logger.info(`Running job "${job.jobName}" that was staled`);
|
|
196
433
|
}
|
|
@@ -208,9 +445,9 @@ var JobsRepo = class {
|
|
|
208
445
|
wasStale
|
|
209
446
|
};
|
|
210
447
|
}
|
|
211
|
-
async getJobAcquisitionExecutionTime(jobNames, hint) {
|
|
448
|
+
async getJobAcquisitionExecutionTime(jobNames, hint, partition = 0) {
|
|
212
449
|
var _a;
|
|
213
|
-
const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date()), {
|
|
450
|
+
const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date(), partition), {
|
|
214
451
|
hint: JOB_ACQUISITION_HINTS[hint],
|
|
215
452
|
maxTimeMS: 1e3,
|
|
216
453
|
readPreference: "primary"
|
|
@@ -234,7 +471,7 @@ var JobsRepo = class {
|
|
|
234
471
|
priority: options.priority,
|
|
235
472
|
...options.resetTries ? { tries: 0 } : {}
|
|
236
473
|
},
|
|
237
|
-
$unset: { lockId: "", lockedUntil: "" }
|
|
474
|
+
$unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
|
|
238
475
|
};
|
|
239
476
|
const result = await this.jobs.updateOne(
|
|
240
477
|
options.lockId ? { _id: options.jobId, lockId: options.lockId } : options.jobId,
|
|
@@ -268,6 +505,7 @@ var JobsRepo = class {
|
|
|
268
505
|
$unset: {
|
|
269
506
|
lockId: "",
|
|
270
507
|
lockedUntil: "",
|
|
508
|
+
claimWasStale: "",
|
|
271
509
|
...retentionMs === null ? { expiresAt: "" } : {}
|
|
272
510
|
}
|
|
273
511
|
}
|
|
@@ -324,12 +562,12 @@ var JobsRepo = class {
|
|
|
324
562
|
lockedUntil: { $exists: true }
|
|
325
563
|
},
|
|
326
564
|
{
|
|
327
|
-
$unset: { lockId: "", lockedUntil: "" }
|
|
565
|
+
$unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
|
|
328
566
|
}
|
|
329
567
|
);
|
|
330
568
|
return result.modifiedCount;
|
|
331
569
|
}
|
|
332
|
-
async ensureJobRecord(job) {
|
|
570
|
+
async ensureJobRecord(job, partition = 0) {
|
|
333
571
|
const result = await this.jobs.upsert(
|
|
334
572
|
{
|
|
335
573
|
jobName: job.name
|
|
@@ -343,7 +581,8 @@ var JobsRepo = class {
|
|
|
343
581
|
status: ""
|
|
344
582
|
},
|
|
345
583
|
$setOnInsert: {
|
|
346
|
-
nextRunAt: /* @__PURE__ */ new Date()
|
|
584
|
+
nextRunAt: /* @__PURE__ */ new Date(),
|
|
585
|
+
partition
|
|
347
586
|
}
|
|
348
587
|
}
|
|
349
588
|
);
|
|
@@ -361,7 +600,8 @@ var JobsRepo = class {
|
|
|
361
600
|
params: options.params,
|
|
362
601
|
nextRunAt: options.nextRunAt,
|
|
363
602
|
priority: options.priority,
|
|
364
|
-
type: "event"
|
|
603
|
+
type: "event",
|
|
604
|
+
...options.partition === void 0 ? {} : { partition: options.partition }
|
|
365
605
|
});
|
|
366
606
|
} catch (error) {
|
|
367
607
|
if (error.isValidationError && Object.values(error.validationErrors).includes("notUnique") && options.uniqueIdentifier) {
|
|
@@ -389,7 +629,8 @@ var JobsRepo = class {
|
|
|
389
629
|
params: job.params,
|
|
390
630
|
nextRunAt: job.nextRunAt,
|
|
391
631
|
priority: job.priority,
|
|
392
|
-
type: "event"
|
|
632
|
+
type: "event",
|
|
633
|
+
...job.partition === void 0 ? {} : { partition: job.partition }
|
|
393
634
|
});
|
|
394
635
|
scheduledCount++;
|
|
395
636
|
} catch (error) {
|
|
@@ -414,11 +655,55 @@ var JobsRepo = class {
|
|
|
414
655
|
errors
|
|
415
656
|
};
|
|
416
657
|
}
|
|
658
|
+
async reconcilePartitions(options) {
|
|
659
|
+
if (options.jobNames.length === 0) return 0;
|
|
660
|
+
const rawCollection = await this.jobs.getRawCollection();
|
|
661
|
+
await this.jobs.createIndexesPromise;
|
|
662
|
+
const now = /* @__PURE__ */ new Date();
|
|
663
|
+
const invalidPartition = {
|
|
664
|
+
$or: [
|
|
665
|
+
{ partition: { $exists: false } },
|
|
666
|
+
{ partition: { $lt: 0 } },
|
|
667
|
+
{ partition: { $gte: options.nPartitions } }
|
|
668
|
+
]
|
|
669
|
+
};
|
|
670
|
+
const availableLock = {
|
|
671
|
+
$or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: now } }]
|
|
672
|
+
};
|
|
673
|
+
const runnableJob = {
|
|
674
|
+
$or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }]
|
|
675
|
+
};
|
|
676
|
+
const jobs = await rawCollection.find({
|
|
677
|
+
jobName: { $in: options.jobNames },
|
|
678
|
+
$and: [invalidPartition, availableLock, runnableJob]
|
|
679
|
+
}).project({ _id: 1 }).limit(options.batchSize).toArray();
|
|
680
|
+
if (jobs.length === 0) return 0;
|
|
681
|
+
const result = await rawCollection.bulkWrite(
|
|
682
|
+
jobs.map((job) => ({
|
|
683
|
+
updateOne: {
|
|
684
|
+
filter: {
|
|
685
|
+
_id: job._id,
|
|
686
|
+
$and: [invalidPartition, availableLock, runnableJob]
|
|
687
|
+
},
|
|
688
|
+
update: { $set: { partition: getRandomPartition(options.nPartitions) } }
|
|
689
|
+
}
|
|
690
|
+
})),
|
|
691
|
+
{ ordered: false }
|
|
692
|
+
);
|
|
693
|
+
return result.modifiedCount;
|
|
694
|
+
}
|
|
417
695
|
};
|
|
418
|
-
|
|
419
|
-
__decorateElement(
|
|
420
|
-
JobsRepo = __decorateElement(
|
|
421
|
-
__runInitializers(
|
|
696
|
+
_init2 = __decoratorStart(null);
|
|
697
|
+
__decorateElement(_init2, 5, "jobs", _jobs_dec, JobsRepo);
|
|
698
|
+
JobsRepo = __decorateElement(_init2, 0, "JobsRepo", _JobsRepo_decorators, JobsRepo);
|
|
699
|
+
__runInitializers(_init2, 1, JobsRepo);
|
|
700
|
+
|
|
701
|
+
// src/schedule.ts
|
|
702
|
+
import { getInstance } from "@orion-js/services";
|
|
703
|
+
|
|
704
|
+
// src/services/EventsService.ts
|
|
705
|
+
import { logger as logger2 } from "@orion-js/logger";
|
|
706
|
+
import { Inject, Service } from "@orion-js/services";
|
|
422
707
|
|
|
423
708
|
// src/services/getNextRunDate.ts
|
|
424
709
|
import { CronExpressionParser } from "cron-parser";
|
|
@@ -448,38 +733,53 @@ var getNextRunDate = (options) => {
|
|
|
448
733
|
};
|
|
449
734
|
|
|
450
735
|
// src/services/EventsService.ts
|
|
451
|
-
var _jobsRepo_dec, _EventsService_decorators,
|
|
736
|
+
var _jobsRepo_dec, _EventsService_decorators, _init3;
|
|
452
737
|
_EventsService_decorators = [Service()], _jobsRepo_dec = [Inject(() => JobsRepo)];
|
|
453
738
|
var EventsService = class {
|
|
454
739
|
constructor() {
|
|
455
|
-
__publicField(this, "jobsRepo", __runInitializers(
|
|
740
|
+
__publicField(this, "jobsRepo", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
|
|
456
741
|
}
|
|
457
|
-
async scheduleJob(options) {
|
|
742
|
+
async scheduleJob(options, nPartitions) {
|
|
458
743
|
logger2.debug("Scheduling job...", options);
|
|
744
|
+
const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
|
|
459
745
|
await this.jobsRepo.scheduleJob({
|
|
460
746
|
name: options.name,
|
|
461
747
|
priority: options.priority || 100,
|
|
462
748
|
nextRunAt: getNextRunDate(options),
|
|
463
749
|
params: options.params || null,
|
|
464
|
-
uniqueIdentifier: options.uniqueIdentifier
|
|
750
|
+
uniqueIdentifier: options.uniqueIdentifier,
|
|
751
|
+
partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
|
|
465
752
|
});
|
|
466
753
|
}
|
|
467
|
-
async scheduleJobs(jobs) {
|
|
754
|
+
async scheduleJobs(jobs, nPartitions) {
|
|
468
755
|
logger2.debug(`Scheduling ${jobs.length} jobs...`);
|
|
469
|
-
const jobRecords = jobs.map((options) =>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
756
|
+
const jobRecords = jobs.map((options) => {
|
|
757
|
+
const configuredPartitions = nPartitions ?? this.jobsRepo.getJobPartitionsCount(options.name);
|
|
758
|
+
return {
|
|
759
|
+
name: options.name,
|
|
760
|
+
priority: options.priority || 100,
|
|
761
|
+
nextRunAt: getNextRunDate(options),
|
|
762
|
+
params: options.params || null,
|
|
763
|
+
uniqueIdentifier: options.uniqueIdentifier,
|
|
764
|
+
partition: configuredPartitions === void 0 ? void 0 : getRandomPartition(configuredPartitions)
|
|
765
|
+
};
|
|
766
|
+
});
|
|
476
767
|
return await this.jobsRepo.scheduleJobs(jobRecords);
|
|
477
768
|
}
|
|
478
769
|
};
|
|
479
|
-
|
|
480
|
-
__decorateElement(
|
|
481
|
-
EventsService = __decorateElement(
|
|
482
|
-
__runInitializers(
|
|
770
|
+
_init3 = __decoratorStart(null);
|
|
771
|
+
__decorateElement(_init3, 5, "jobsRepo", _jobsRepo_dec, EventsService);
|
|
772
|
+
EventsService = __decorateElement(_init3, 0, "EventsService", _EventsService_decorators, EventsService);
|
|
773
|
+
__runInitializers(_init3, 1, EventsService);
|
|
774
|
+
|
|
775
|
+
// src/schedule.ts
|
|
776
|
+
var eventsService = getInstance(EventsService);
|
|
777
|
+
function scheduleJobInternal(options, nPartitions) {
|
|
778
|
+
return eventsService.scheduleJob(options, nPartitions);
|
|
779
|
+
}
|
|
780
|
+
function scheduleJobsInternal(jobs, nPartitions) {
|
|
781
|
+
return eventsService.scheduleJobs(jobs, nPartitions);
|
|
782
|
+
}
|
|
483
783
|
|
|
484
784
|
// src/services/WorkerService.ts
|
|
485
785
|
import { sleep } from "@orion-js/helpers";
|
|
@@ -490,132 +790,6 @@ import { Inject as Inject3, Service as Service3 } from "@orion-js/services";
|
|
|
490
790
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
491
791
|
import { logger as logger3, runWithOrionAsyncContext, updateOrionAsyncContext } from "@orion-js/logger";
|
|
492
792
|
import { Inject as Inject2, Service as Service2 } from "@orion-js/services";
|
|
493
|
-
|
|
494
|
-
// src/repos/JobsHistoryRepo.ts
|
|
495
|
-
import { MongoCollection as MongoCollection2, Repository as Repository2 } from "@orion-js/mongodb";
|
|
496
|
-
|
|
497
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/isInteger.js
|
|
498
|
-
function _isInteger(n) {
|
|
499
|
-
return n << 0 === n;
|
|
500
|
-
}
|
|
501
|
-
var isInteger = Number.isInteger || _isInteger;
|
|
502
|
-
|
|
503
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/createPath.js
|
|
504
|
-
function createPath(path, delimiter = ".") {
|
|
505
|
-
return typeof path === "string" ? path.split(delimiter).map((x) => isInteger(x) ? Number(x) : x) : path;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/compare.js
|
|
509
|
-
function compare(a, b) {
|
|
510
|
-
return String(a) === String(b);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/_internals/includes.js
|
|
514
|
-
function includes(a, list) {
|
|
515
|
-
let index = -1;
|
|
516
|
-
const { length } = list;
|
|
517
|
-
while (++index < length)
|
|
518
|
-
if (compare(list[index], a))
|
|
519
|
-
return true;
|
|
520
|
-
return false;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ../../node_modules/.bun/rambdax@11.3.1/node_modules/rambdax/src/omit.js
|
|
524
|
-
function omit(propsToOmit, obj) {
|
|
525
|
-
if (arguments.length === 1) return (_obj) => omit(propsToOmit, _obj);
|
|
526
|
-
if (obj === null || obj === void 0)
|
|
527
|
-
return void 0;
|
|
528
|
-
const propsToOmitValue = createPath(propsToOmit, ",");
|
|
529
|
-
const willReturn = {};
|
|
530
|
-
for (const key in obj)
|
|
531
|
-
if (!includes(key, propsToOmitValue))
|
|
532
|
-
willReturn[key] = obj[key];
|
|
533
|
-
return willReturn;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// src/types/HistoryRecord.ts
|
|
537
|
-
import { schemaWithName as schemaWithName2 } from "@orion-js/schema";
|
|
538
|
-
var HistoryRecordSchema = schemaWithName2("HistoryRecord", {
|
|
539
|
-
_id: { type: "string" },
|
|
540
|
-
jobId: { type: "string" },
|
|
541
|
-
executionId: { type: "string" },
|
|
542
|
-
jobName: { type: "string" },
|
|
543
|
-
type: { type: "string" },
|
|
544
|
-
priority: { type: "number" },
|
|
545
|
-
tries: { type: "number" },
|
|
546
|
-
uniqueIdentifier: { type: "string", optional: true },
|
|
547
|
-
startedAt: { type: "date" },
|
|
548
|
-
endedAt: { type: "date" },
|
|
549
|
-
duration: { type: "number" },
|
|
550
|
-
expiresAt: { type: "date", optional: true },
|
|
551
|
-
status: { type: "string", enum: ["success", "error", "stale"] },
|
|
552
|
-
errorMessage: { type: "string", optional: true },
|
|
553
|
-
params: { type: "blackbox", optional: true },
|
|
554
|
-
result: { type: "any", optional: true }
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// src/repos/JobsHistoryRepo.ts
|
|
558
|
-
var _history_dec, _JobsHistoryRepo_decorators, _init3;
|
|
559
|
-
_JobsHistoryRepo_decorators = [Repository2()], _history_dec = [MongoCollection2({
|
|
560
|
-
name: "orionjs.jobs_dogs_history",
|
|
561
|
-
idGeneration: "uuid",
|
|
562
|
-
schema: HistoryRecordSchema,
|
|
563
|
-
indexes: [
|
|
564
|
-
{
|
|
565
|
-
keys: {
|
|
566
|
-
jobName: 1,
|
|
567
|
-
startedAt: 1
|
|
568
|
-
}
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
keys: {
|
|
572
|
-
executionId: 1
|
|
573
|
-
}
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
keys: {
|
|
577
|
-
expiresAt: 1
|
|
578
|
-
},
|
|
579
|
-
options: {
|
|
580
|
-
expireAfterSeconds: 0
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
]
|
|
584
|
-
})];
|
|
585
|
-
var JobsHistoryRepo = class {
|
|
586
|
-
constructor() {
|
|
587
|
-
__publicField(this, "history", __runInitializers(_init3, 8, this)), __runInitializers(_init3, 11, this);
|
|
588
|
-
}
|
|
589
|
-
async saveExecution(record) {
|
|
590
|
-
await this.history.upsert(
|
|
591
|
-
{ executionId: record.executionId },
|
|
592
|
-
{
|
|
593
|
-
$setOnInsert: {
|
|
594
|
-
status: record.status
|
|
595
|
-
},
|
|
596
|
-
$set: {
|
|
597
|
-
...omit(["status"], record)
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
async getExecutions(jobName, limit, skip) {
|
|
603
|
-
const cursor = this.history.find({ jobName }).sort({ startedAt: -1 });
|
|
604
|
-
if (skip) {
|
|
605
|
-
cursor.skip(skip);
|
|
606
|
-
}
|
|
607
|
-
if (limit) {
|
|
608
|
-
cursor.limit(limit);
|
|
609
|
-
}
|
|
610
|
-
return await cursor.toArray();
|
|
611
|
-
}
|
|
612
|
-
};
|
|
613
|
-
_init3 = __decoratorStart(null);
|
|
614
|
-
__decorateElement(_init3, 5, "history", _history_dec, JobsHistoryRepo);
|
|
615
|
-
JobsHistoryRepo = __decorateElement(_init3, 0, "JobsHistoryRepo", _JobsHistoryRepo_decorators, JobsHistoryRepo);
|
|
616
|
-
__runInitializers(_init3, 1, JobsHistoryRepo);
|
|
617
|
-
|
|
618
|
-
// src/services/Executor.ts
|
|
619
793
|
var _jobsHistoryRepo_dec, _jobsRepo_dec2, _Executor_decorators, _init4;
|
|
620
794
|
_Executor_decorators = [Service2()], _jobsRepo_dec2 = [Inject2(() => JobsRepo)], _jobsHistoryRepo_dec = [Inject2(() => JobsHistoryRepo)];
|
|
621
795
|
var Executor = class {
|
|
@@ -955,11 +1129,16 @@ Executor = __decorateElement(_init4, 0, "Executor", _Executor_decorators, Execut
|
|
|
955
1129
|
__runInitializers(_init4, 1, Executor);
|
|
956
1130
|
|
|
957
1131
|
// src/services/WorkerService.ts
|
|
1132
|
+
var FILTERED_JOB_NAMES_ACQUISITION_HINT = "byJobName";
|
|
958
1133
|
var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
|
|
959
1134
|
_WorkerService_decorators = [Service3()], _jobsRepo_dec3 = [Inject3(() => JobsRepo)], _executor_dec = [Inject3(() => Executor)];
|
|
960
1135
|
var WorkerService = class {
|
|
961
1136
|
constructor() {
|
|
962
1137
|
__publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
|
|
1138
|
+
__publicField(this, "partitionReconcilerIntervalMs", 5 * 60 * 1e3);
|
|
1139
|
+
__publicField(this, "partitionReconcilerJitterMs", 30 * 1e3);
|
|
1140
|
+
__publicField(this, "partitionReconcilerBatchPauseMs", 250);
|
|
1141
|
+
__publicField(this, "partitionReconcilerBatchSize", 500);
|
|
963
1142
|
__publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
|
|
964
1143
|
__publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
|
|
965
1144
|
}
|
|
@@ -1044,6 +1223,15 @@ var WorkerService = class {
|
|
|
1044
1223
|
if (capacityPromises.length === 0) return;
|
|
1045
1224
|
await Promise.race([...capacityPromises, workersInstance.stopSignal]);
|
|
1046
1225
|
}
|
|
1226
|
+
getNextPartition(config, workersInstance) {
|
|
1227
|
+
const partition = workersInstance.partitionOrder[workersInstance.partitionCursor];
|
|
1228
|
+
workersInstance.partitionCursor++;
|
|
1229
|
+
if (workersInstance.partitionCursor >= config.nPartitions) {
|
|
1230
|
+
workersInstance.partitionOrder = createShuffledPartitions(config.nPartitions);
|
|
1231
|
+
workersInstance.partitionCursor = 0;
|
|
1232
|
+
}
|
|
1233
|
+
return partition;
|
|
1234
|
+
}
|
|
1047
1235
|
async runSchedulerLoop(config, workersInstance) {
|
|
1048
1236
|
const jobNames = this.getJobNames(config.jobs);
|
|
1049
1237
|
const executeConfig = {
|
|
@@ -1053,6 +1241,7 @@ var WorkerService = class {
|
|
|
1053
1241
|
onMaxTriesReached: config.onMaxTriesReached
|
|
1054
1242
|
};
|
|
1055
1243
|
logger4.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
|
|
1244
|
+
let consecutivePartitionMisses = 0;
|
|
1056
1245
|
while (workersInstance.running) {
|
|
1057
1246
|
let didMiss = false;
|
|
1058
1247
|
let allJobNamesAtCapacity = false;
|
|
@@ -1063,21 +1252,31 @@ var WorkerService = class {
|
|
|
1063
1252
|
allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
|
|
1064
1253
|
break;
|
|
1065
1254
|
}
|
|
1255
|
+
const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
|
|
1256
|
+
const partition = this.getNextPartition(config, workersInstance);
|
|
1066
1257
|
const jobToRun = await this.jobsRepo.getJobAndLock(
|
|
1067
1258
|
availableJobNames,
|
|
1068
1259
|
config.defaultLockTime,
|
|
1069
|
-
|
|
1260
|
+
jobAcquisitionHint,
|
|
1261
|
+
partition
|
|
1070
1262
|
);
|
|
1071
1263
|
if (!jobToRun) {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1264
|
+
consecutivePartitionMisses++;
|
|
1265
|
+
logger4.debug(`No job to run in partition ${partition}`);
|
|
1266
|
+
if (consecutivePartitionMisses >= config.nPartitions) {
|
|
1267
|
+
consecutivePartitionMisses = 0;
|
|
1268
|
+
didMiss = true;
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
continue;
|
|
1075
1272
|
}
|
|
1273
|
+
consecutivePartitionMisses = 0;
|
|
1076
1274
|
this.startExecution(executeConfig, workersInstance, jobToRun);
|
|
1077
1275
|
if (!workersInstance.running) break;
|
|
1078
1276
|
}
|
|
1079
1277
|
} catch (error) {
|
|
1080
1278
|
logger4.error("Error in job scheduler.", { error });
|
|
1279
|
+
consecutivePartitionMisses = 0;
|
|
1081
1280
|
didMiss = true;
|
|
1082
1281
|
}
|
|
1083
1282
|
if (!workersInstance.running) return;
|
|
@@ -1113,11 +1312,13 @@ var WorkerService = class {
|
|
|
1113
1312
|
workersInstance.jobAcquisitionHintProbeCycle++;
|
|
1114
1313
|
try {
|
|
1115
1314
|
for (let sample = 0; sample < 3; sample++) {
|
|
1315
|
+
const partition = getRandomPartition(config.nPartitions);
|
|
1116
1316
|
for (const hint of [firstHint, secondHint]) {
|
|
1117
1317
|
if (!workersInstance.running) return;
|
|
1118
1318
|
const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
|
|
1119
1319
|
jobNames,
|
|
1120
|
-
hint
|
|
1320
|
+
hint,
|
|
1321
|
+
partition
|
|
1121
1322
|
);
|
|
1122
1323
|
if (!workersInstance.running) return;
|
|
1123
1324
|
byHint[hint].push(executionTimeMillis);
|
|
@@ -1189,6 +1390,44 @@ var WorkerService = class {
|
|
|
1189
1390
|
await this.waitForNextJobAcquisitionHintProbe(workersInstance);
|
|
1190
1391
|
}
|
|
1191
1392
|
}
|
|
1393
|
+
async reconcilePartitionBatch(config) {
|
|
1394
|
+
if (typeof this.jobsRepo.reconcilePartitions !== "function") return 0;
|
|
1395
|
+
try {
|
|
1396
|
+
return await this.jobsRepo.reconcilePartitions({
|
|
1397
|
+
jobNames: this.getJobNames(config.jobs),
|
|
1398
|
+
nPartitions: config.nPartitions,
|
|
1399
|
+
batchSize: this.partitionReconcilerBatchSize
|
|
1400
|
+
});
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
logger4.warn("Job partition reconciliation failed; it will be retried.", { error });
|
|
1403
|
+
return 0;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
async waitForNextPartitionReconcile(workersInstance, delayMs) {
|
|
1407
|
+
await new Promise((resolve) => {
|
|
1408
|
+
let resolved = false;
|
|
1409
|
+
const finishWait = () => {
|
|
1410
|
+
if (resolved) return;
|
|
1411
|
+
resolved = true;
|
|
1412
|
+
clearTimeout(workersInstance.partitionReconcilerTimer);
|
|
1413
|
+
workersInstance.partitionReconcilerTimer = void 0;
|
|
1414
|
+
workersInstance.cancelPartitionReconcilerWait = void 0;
|
|
1415
|
+
resolve();
|
|
1416
|
+
};
|
|
1417
|
+
workersInstance.partitionReconcilerTimer = setTimeout(finishWait, delayMs);
|
|
1418
|
+
workersInstance.cancelPartitionReconcilerWait = finishWait;
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
async runPartitionReconcilerLoop(config, workersInstance, initialReconcilePromise) {
|
|
1422
|
+
let modifiedCount = await initialReconcilePromise;
|
|
1423
|
+
while (workersInstance.running) {
|
|
1424
|
+
const didFillBatch = modifiedCount >= this.partitionReconcilerBatchSize;
|
|
1425
|
+
const delayMs = didFillBatch ? this.partitionReconcilerBatchPauseMs : this.partitionReconcilerIntervalMs + Math.floor(Math.random() * (this.partitionReconcilerJitterMs + 1));
|
|
1426
|
+
await this.waitForNextPartitionReconcile(workersInstance, delayMs);
|
|
1427
|
+
if (!workersInstance.running) return;
|
|
1428
|
+
modifiedCount = await this.reconcilePartitionBatch(config);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1192
1431
|
createWorkersInstanceDefinition(config) {
|
|
1193
1432
|
let signalStop;
|
|
1194
1433
|
const stopSignal = new Promise((resolve) => {
|
|
@@ -1204,7 +1443,10 @@ var WorkerService = class {
|
|
|
1204
1443
|
jobAcquisitionHintProbeCycle: 0,
|
|
1205
1444
|
hasCompletedInitialJobAcquisitionHintProbe: false,
|
|
1206
1445
|
pendingJobAcquisitionHintWins: 0,
|
|
1446
|
+
partitionOrder: createShuffledPartitions(config.nPartitions),
|
|
1447
|
+
partitionCursor: 0,
|
|
1207
1448
|
jobAcquisitionHintProbePromise: Promise.resolve(),
|
|
1449
|
+
partitionReconcilerPromise: Promise.resolve(),
|
|
1208
1450
|
schedulerPromise: Promise.resolve(),
|
|
1209
1451
|
stopSignal,
|
|
1210
1452
|
signalStop,
|
|
@@ -1214,14 +1456,16 @@ var WorkerService = class {
|
|
|
1214
1456
|
stop: async () => {
|
|
1215
1457
|
if (workersInstance.stopPromise) return workersInstance.stopPromise;
|
|
1216
1458
|
workersInstance.stopPromise = (async () => {
|
|
1217
|
-
var _a;
|
|
1459
|
+
var _a, _b;
|
|
1218
1460
|
logger4.info("Stopping job scheduler...");
|
|
1219
1461
|
workersInstance.running = false;
|
|
1220
1462
|
workersInstance.signalStop();
|
|
1221
1463
|
(_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
|
|
1464
|
+
(_b = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b.call(workersInstance);
|
|
1222
1465
|
await Promise.all([
|
|
1223
1466
|
workersInstance.schedulerPromise,
|
|
1224
|
-
workersInstance.jobAcquisitionHintProbePromise
|
|
1467
|
+
workersInstance.jobAcquisitionHintProbePromise,
|
|
1468
|
+
workersInstance.partitionReconcilerPromise
|
|
1225
1469
|
]);
|
|
1226
1470
|
const activeCapacity = Array.from(
|
|
1227
1471
|
workersInstance.activeExecutions.values(),
|
|
@@ -1239,7 +1483,7 @@ var WorkerService = class {
|
|
|
1239
1483
|
await Promise.all(
|
|
1240
1484
|
jobs.filter((job) => job.type === "recurrent").map(async (job) => {
|
|
1241
1485
|
logger4.debug(`Ensuring records for job "${job.name}"...`);
|
|
1242
|
-
await this.jobsRepo.ensureJobRecord(job);
|
|
1486
|
+
await this.jobsRepo.ensureJobRecord(job, getRandomPartition(config.nPartitions));
|
|
1243
1487
|
})
|
|
1244
1488
|
);
|
|
1245
1489
|
}
|
|
@@ -1256,29 +1500,41 @@ var WorkerService = class {
|
|
|
1256
1500
|
* @param userConfig - Configuration for the scheduler. Required field: jobs
|
|
1257
1501
|
*/
|
|
1258
1502
|
startWorkers(userConfig) {
|
|
1503
|
+
var _a, _b;
|
|
1259
1504
|
const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
|
|
1260
1505
|
if (maxTriesReachedRetentionMs !== null && (!Number.isFinite(maxTriesReachedRetentionMs) || maxTriesReachedRetentionMs < 0)) {
|
|
1261
1506
|
throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
|
|
1262
1507
|
}
|
|
1508
|
+
const nPartitions = userConfig.nPartitions ?? 1;
|
|
1509
|
+
validatePartitionsCount(nPartitions);
|
|
1263
1510
|
const config = {
|
|
1264
1511
|
pollInterval: 3e3,
|
|
1265
1512
|
workersCount: 4,
|
|
1266
1513
|
defaultLockTime: 30 * 1e3,
|
|
1267
1514
|
...userConfig,
|
|
1515
|
+
nPartitions,
|
|
1268
1516
|
maxTriesReachedRetentionMs
|
|
1269
1517
|
};
|
|
1270
|
-
setNameToJobs(config.jobs);
|
|
1518
|
+
setNameToJobs(config.jobs, config.nPartitions);
|
|
1519
|
+
(_b = (_a = this.jobsRepo).configureJobPartitions) == null ? void 0 : _b.call(_a, this.getJobNames(config.jobs), config.nPartitions);
|
|
1271
1520
|
const workersInstance = this.createWorkersInstanceDefinition(config);
|
|
1272
1521
|
logger4.debug("Starting job scheduler", config);
|
|
1273
1522
|
workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
|
|
1274
1523
|
config,
|
|
1275
1524
|
workersInstance
|
|
1276
1525
|
);
|
|
1526
|
+
const initialReconcilePromise = this.reconcilePartitionBatch(config);
|
|
1527
|
+
workersInstance.partitionReconcilerPromise = this.runPartitionReconcilerLoop(
|
|
1528
|
+
config,
|
|
1529
|
+
workersInstance,
|
|
1530
|
+
initialReconcilePromise
|
|
1531
|
+
);
|
|
1277
1532
|
workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
|
|
1278
|
-
var
|
|
1533
|
+
var _a2, _b2;
|
|
1279
1534
|
workersInstance.running = false;
|
|
1280
1535
|
workersInstance.signalStop();
|
|
1281
|
-
(
|
|
1536
|
+
(_a2 = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a2.call(workersInstance);
|
|
1537
|
+
(_b2 = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b2.call(workersInstance);
|
|
1282
1538
|
logger4.error("Job scheduler stopped after an unexpected error.", { error });
|
|
1283
1539
|
});
|
|
1284
1540
|
return workersInstance;
|
|
@@ -1289,15 +1545,13 @@ __decorateElement(_init5, 5, "jobsRepo", _jobsRepo_dec3, WorkerService);
|
|
|
1289
1545
|
__decorateElement(_init5, 5, "executor", _executor_dec, WorkerService);
|
|
1290
1546
|
WorkerService = __decorateElement(_init5, 0, "WorkerService", _WorkerService_decorators, WorkerService);
|
|
1291
1547
|
__runInitializers(_init5, 1, WorkerService);
|
|
1292
|
-
function setNameToJobs(jobs) {
|
|
1548
|
+
function setNameToJobs(jobs, nPartitions) {
|
|
1293
1549
|
for (const name of Object.keys(jobs)) {
|
|
1294
1550
|
jobs[name].jobName = name;
|
|
1551
|
+
jobs[name].nPartitions = nPartitions;
|
|
1295
1552
|
}
|
|
1296
1553
|
}
|
|
1297
1554
|
|
|
1298
|
-
// src/service/index.ts
|
|
1299
|
-
import { getInstance, Service as Service4 } from "@orion-js/services";
|
|
1300
|
-
|
|
1301
1555
|
// src/defineJob/index.ts
|
|
1302
1556
|
import { cleanAndValidate } from "@orion-js/schema";
|
|
1303
1557
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
@@ -1314,11 +1568,14 @@ function createEventJob(options) {
|
|
|
1314
1568
|
throw new Error("This job has not been registered in the workers");
|
|
1315
1569
|
}
|
|
1316
1570
|
const params = jobDefinition.params ? await cleanAndValidate(jobDefinition.params, scheduleOptions.params) : scheduleOptions.params;
|
|
1317
|
-
return await
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1571
|
+
return await scheduleJobInternal(
|
|
1572
|
+
{
|
|
1573
|
+
...scheduleOptions,
|
|
1574
|
+
name: jobDefinition.jobName,
|
|
1575
|
+
params
|
|
1576
|
+
},
|
|
1577
|
+
jobDefinition.nPartitions
|
|
1578
|
+
);
|
|
1322
1579
|
};
|
|
1323
1580
|
jobDefinition.scheduleJobs = async (jobs) => {
|
|
1324
1581
|
if (!jobDefinition.jobName) {
|
|
@@ -1334,7 +1591,7 @@ function createEventJob(options) {
|
|
|
1334
1591
|
};
|
|
1335
1592
|
})
|
|
1336
1593
|
);
|
|
1337
|
-
return await
|
|
1594
|
+
return await scheduleJobsInternal(processedJobs, jobDefinition.nPartitions);
|
|
1338
1595
|
};
|
|
1339
1596
|
return jobDefinition;
|
|
1340
1597
|
}
|
|
@@ -1363,6 +1620,7 @@ var defineJob = (options) => {
|
|
|
1363
1620
|
};
|
|
1364
1621
|
|
|
1365
1622
|
// src/service/index.ts
|
|
1623
|
+
import { getInstance as getInstance2, Service as Service4 } from "@orion-js/services";
|
|
1366
1624
|
var serviceMetadata = /* @__PURE__ */ new WeakMap();
|
|
1367
1625
|
var jobsMetadata = /* @__PURE__ */ new Map();
|
|
1368
1626
|
var jobEntriesByClass = /* @__PURE__ */ new Map();
|
|
@@ -1418,7 +1676,7 @@ function initializeJobsIfNeeded(instance) {
|
|
|
1418
1676
|
jobsMetadata.set(instance, jobs);
|
|
1419
1677
|
}
|
|
1420
1678
|
function getServiceJobs(target) {
|
|
1421
|
-
const instance =
|
|
1679
|
+
const instance = getInstance2(target);
|
|
1422
1680
|
if (!serviceMetadata.has(instance.constructor)) {
|
|
1423
1681
|
throw new Error("You must pass a class decorated with @Jobs to getServiceJobs");
|
|
1424
1682
|
}
|
|
@@ -1432,18 +1690,17 @@ function getServiceJobs(target) {
|
|
|
1432
1690
|
}
|
|
1433
1691
|
|
|
1434
1692
|
// src/index.ts
|
|
1435
|
-
var workerService =
|
|
1436
|
-
var
|
|
1437
|
-
var
|
|
1438
|
-
var jobsRepo = getInstance2(JobsRepo);
|
|
1693
|
+
var workerService = getInstance3(WorkerService);
|
|
1694
|
+
var jobsHistoryRepo = getInstance3(JobsHistoryRepo);
|
|
1695
|
+
var jobsRepo = getInstance3(JobsRepo);
|
|
1439
1696
|
var startWorkers = (config) => {
|
|
1440
1697
|
return workerService.startWorkers(config);
|
|
1441
1698
|
};
|
|
1442
1699
|
var scheduleJob = (options) => {
|
|
1443
|
-
return
|
|
1700
|
+
return scheduleJobInternal(options);
|
|
1444
1701
|
};
|
|
1445
1702
|
var scheduleJobs = (jobs) => {
|
|
1446
|
-
return
|
|
1703
|
+
return scheduleJobsInternal(jobs);
|
|
1447
1704
|
};
|
|
1448
1705
|
export {
|
|
1449
1706
|
DEFAULT_MAX_TRIES_REACHED_RETENTION_MS,
|