@orion-js/dogs 4.6.4 → 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 +459 -204
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +452 -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 {
|
|
@@ -961,6 +1135,10 @@ _WorkerService_decorators = [Service3()], _jobsRepo_dec3 = [Inject3(() => JobsRe
|
|
|
961
1135
|
var WorkerService = class {
|
|
962
1136
|
constructor() {
|
|
963
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);
|
|
964
1142
|
__publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
|
|
965
1143
|
__publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
|
|
966
1144
|
}
|
|
@@ -1045,6 +1223,15 @@ var WorkerService = class {
|
|
|
1045
1223
|
if (capacityPromises.length === 0) return;
|
|
1046
1224
|
await Promise.race([...capacityPromises, workersInstance.stopSignal]);
|
|
1047
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
|
+
}
|
|
1048
1235
|
async runSchedulerLoop(config, workersInstance) {
|
|
1049
1236
|
const jobNames = this.getJobNames(config.jobs);
|
|
1050
1237
|
const executeConfig = {
|
|
@@ -1054,6 +1241,7 @@ var WorkerService = class {
|
|
|
1054
1241
|
onMaxTriesReached: config.onMaxTriesReached
|
|
1055
1242
|
};
|
|
1056
1243
|
logger4.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
|
|
1244
|
+
let consecutivePartitionMisses = 0;
|
|
1057
1245
|
while (workersInstance.running) {
|
|
1058
1246
|
let didMiss = false;
|
|
1059
1247
|
let allJobNamesAtCapacity = false;
|
|
@@ -1065,21 +1253,30 @@ var WorkerService = class {
|
|
|
1065
1253
|
break;
|
|
1066
1254
|
}
|
|
1067
1255
|
const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
|
|
1256
|
+
const partition = this.getNextPartition(config, workersInstance);
|
|
1068
1257
|
const jobToRun = await this.jobsRepo.getJobAndLock(
|
|
1069
1258
|
availableJobNames,
|
|
1070
1259
|
config.defaultLockTime,
|
|
1071
|
-
jobAcquisitionHint
|
|
1260
|
+
jobAcquisitionHint,
|
|
1261
|
+
partition
|
|
1072
1262
|
);
|
|
1073
1263
|
if (!jobToRun) {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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;
|
|
1077
1272
|
}
|
|
1273
|
+
consecutivePartitionMisses = 0;
|
|
1078
1274
|
this.startExecution(executeConfig, workersInstance, jobToRun);
|
|
1079
1275
|
if (!workersInstance.running) break;
|
|
1080
1276
|
}
|
|
1081
1277
|
} catch (error) {
|
|
1082
1278
|
logger4.error("Error in job scheduler.", { error });
|
|
1279
|
+
consecutivePartitionMisses = 0;
|
|
1083
1280
|
didMiss = true;
|
|
1084
1281
|
}
|
|
1085
1282
|
if (!workersInstance.running) return;
|
|
@@ -1115,11 +1312,13 @@ var WorkerService = class {
|
|
|
1115
1312
|
workersInstance.jobAcquisitionHintProbeCycle++;
|
|
1116
1313
|
try {
|
|
1117
1314
|
for (let sample = 0; sample < 3; sample++) {
|
|
1315
|
+
const partition = getRandomPartition(config.nPartitions);
|
|
1118
1316
|
for (const hint of [firstHint, secondHint]) {
|
|
1119
1317
|
if (!workersInstance.running) return;
|
|
1120
1318
|
const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
|
|
1121
1319
|
jobNames,
|
|
1122
|
-
hint
|
|
1320
|
+
hint,
|
|
1321
|
+
partition
|
|
1123
1322
|
);
|
|
1124
1323
|
if (!workersInstance.running) return;
|
|
1125
1324
|
byHint[hint].push(executionTimeMillis);
|
|
@@ -1191,6 +1390,44 @@ var WorkerService = class {
|
|
|
1191
1390
|
await this.waitForNextJobAcquisitionHintProbe(workersInstance);
|
|
1192
1391
|
}
|
|
1193
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
|
+
}
|
|
1194
1431
|
createWorkersInstanceDefinition(config) {
|
|
1195
1432
|
let signalStop;
|
|
1196
1433
|
const stopSignal = new Promise((resolve) => {
|
|
@@ -1206,7 +1443,10 @@ var WorkerService = class {
|
|
|
1206
1443
|
jobAcquisitionHintProbeCycle: 0,
|
|
1207
1444
|
hasCompletedInitialJobAcquisitionHintProbe: false,
|
|
1208
1445
|
pendingJobAcquisitionHintWins: 0,
|
|
1446
|
+
partitionOrder: createShuffledPartitions(config.nPartitions),
|
|
1447
|
+
partitionCursor: 0,
|
|
1209
1448
|
jobAcquisitionHintProbePromise: Promise.resolve(),
|
|
1449
|
+
partitionReconcilerPromise: Promise.resolve(),
|
|
1210
1450
|
schedulerPromise: Promise.resolve(),
|
|
1211
1451
|
stopSignal,
|
|
1212
1452
|
signalStop,
|
|
@@ -1216,14 +1456,16 @@ var WorkerService = class {
|
|
|
1216
1456
|
stop: async () => {
|
|
1217
1457
|
if (workersInstance.stopPromise) return workersInstance.stopPromise;
|
|
1218
1458
|
workersInstance.stopPromise = (async () => {
|
|
1219
|
-
var _a;
|
|
1459
|
+
var _a, _b;
|
|
1220
1460
|
logger4.info("Stopping job scheduler...");
|
|
1221
1461
|
workersInstance.running = false;
|
|
1222
1462
|
workersInstance.signalStop();
|
|
1223
1463
|
(_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
|
|
1464
|
+
(_b = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b.call(workersInstance);
|
|
1224
1465
|
await Promise.all([
|
|
1225
1466
|
workersInstance.schedulerPromise,
|
|
1226
|
-
workersInstance.jobAcquisitionHintProbePromise
|
|
1467
|
+
workersInstance.jobAcquisitionHintProbePromise,
|
|
1468
|
+
workersInstance.partitionReconcilerPromise
|
|
1227
1469
|
]);
|
|
1228
1470
|
const activeCapacity = Array.from(
|
|
1229
1471
|
workersInstance.activeExecutions.values(),
|
|
@@ -1241,7 +1483,7 @@ var WorkerService = class {
|
|
|
1241
1483
|
await Promise.all(
|
|
1242
1484
|
jobs.filter((job) => job.type === "recurrent").map(async (job) => {
|
|
1243
1485
|
logger4.debug(`Ensuring records for job "${job.name}"...`);
|
|
1244
|
-
await this.jobsRepo.ensureJobRecord(job);
|
|
1486
|
+
await this.jobsRepo.ensureJobRecord(job, getRandomPartition(config.nPartitions));
|
|
1245
1487
|
})
|
|
1246
1488
|
);
|
|
1247
1489
|
}
|
|
@@ -1258,29 +1500,41 @@ var WorkerService = class {
|
|
|
1258
1500
|
* @param userConfig - Configuration for the scheduler. Required field: jobs
|
|
1259
1501
|
*/
|
|
1260
1502
|
startWorkers(userConfig) {
|
|
1503
|
+
var _a, _b;
|
|
1261
1504
|
const maxTriesReachedRetentionMs = userConfig.maxTriesReachedRetentionMs === void 0 ? DEFAULT_MAX_TRIES_REACHED_RETENTION_MS : userConfig.maxTriesReachedRetentionMs;
|
|
1262
1505
|
if (maxTriesReachedRetentionMs !== null && (!Number.isFinite(maxTriesReachedRetentionMs) || maxTriesReachedRetentionMs < 0)) {
|
|
1263
1506
|
throw new Error("maxTriesReachedRetentionMs must be null, zero, or a positive number");
|
|
1264
1507
|
}
|
|
1508
|
+
const nPartitions = userConfig.nPartitions ?? 1;
|
|
1509
|
+
validatePartitionsCount(nPartitions);
|
|
1265
1510
|
const config = {
|
|
1266
1511
|
pollInterval: 3e3,
|
|
1267
1512
|
workersCount: 4,
|
|
1268
1513
|
defaultLockTime: 30 * 1e3,
|
|
1269
1514
|
...userConfig,
|
|
1515
|
+
nPartitions,
|
|
1270
1516
|
maxTriesReachedRetentionMs
|
|
1271
1517
|
};
|
|
1272
|
-
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);
|
|
1273
1520
|
const workersInstance = this.createWorkersInstanceDefinition(config);
|
|
1274
1521
|
logger4.debug("Starting job scheduler", config);
|
|
1275
1522
|
workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
|
|
1276
1523
|
config,
|
|
1277
1524
|
workersInstance
|
|
1278
1525
|
);
|
|
1526
|
+
const initialReconcilePromise = this.reconcilePartitionBatch(config);
|
|
1527
|
+
workersInstance.partitionReconcilerPromise = this.runPartitionReconcilerLoop(
|
|
1528
|
+
config,
|
|
1529
|
+
workersInstance,
|
|
1530
|
+
initialReconcilePromise
|
|
1531
|
+
);
|
|
1279
1532
|
workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
|
|
1280
|
-
var
|
|
1533
|
+
var _a2, _b2;
|
|
1281
1534
|
workersInstance.running = false;
|
|
1282
1535
|
workersInstance.signalStop();
|
|
1283
|
-
(
|
|
1536
|
+
(_a2 = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a2.call(workersInstance);
|
|
1537
|
+
(_b2 = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b2.call(workersInstance);
|
|
1284
1538
|
logger4.error("Job scheduler stopped after an unexpected error.", { error });
|
|
1285
1539
|
});
|
|
1286
1540
|
return workersInstance;
|
|
@@ -1291,15 +1545,13 @@ __decorateElement(_init5, 5, "jobsRepo", _jobsRepo_dec3, WorkerService);
|
|
|
1291
1545
|
__decorateElement(_init5, 5, "executor", _executor_dec, WorkerService);
|
|
1292
1546
|
WorkerService = __decorateElement(_init5, 0, "WorkerService", _WorkerService_decorators, WorkerService);
|
|
1293
1547
|
__runInitializers(_init5, 1, WorkerService);
|
|
1294
|
-
function setNameToJobs(jobs) {
|
|
1548
|
+
function setNameToJobs(jobs, nPartitions) {
|
|
1295
1549
|
for (const name of Object.keys(jobs)) {
|
|
1296
1550
|
jobs[name].jobName = name;
|
|
1551
|
+
jobs[name].nPartitions = nPartitions;
|
|
1297
1552
|
}
|
|
1298
1553
|
}
|
|
1299
1554
|
|
|
1300
|
-
// src/service/index.ts
|
|
1301
|
-
import { getInstance, Service as Service4 } from "@orion-js/services";
|
|
1302
|
-
|
|
1303
1555
|
// src/defineJob/index.ts
|
|
1304
1556
|
import { cleanAndValidate } from "@orion-js/schema";
|
|
1305
1557
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
@@ -1316,11 +1568,14 @@ function createEventJob(options) {
|
|
|
1316
1568
|
throw new Error("This job has not been registered in the workers");
|
|
1317
1569
|
}
|
|
1318
1570
|
const params = jobDefinition.params ? await cleanAndValidate(jobDefinition.params, scheduleOptions.params) : scheduleOptions.params;
|
|
1319
|
-
return await
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1571
|
+
return await scheduleJobInternal(
|
|
1572
|
+
{
|
|
1573
|
+
...scheduleOptions,
|
|
1574
|
+
name: jobDefinition.jobName,
|
|
1575
|
+
params
|
|
1576
|
+
},
|
|
1577
|
+
jobDefinition.nPartitions
|
|
1578
|
+
);
|
|
1324
1579
|
};
|
|
1325
1580
|
jobDefinition.scheduleJobs = async (jobs) => {
|
|
1326
1581
|
if (!jobDefinition.jobName) {
|
|
@@ -1336,7 +1591,7 @@ function createEventJob(options) {
|
|
|
1336
1591
|
};
|
|
1337
1592
|
})
|
|
1338
1593
|
);
|
|
1339
|
-
return await
|
|
1594
|
+
return await scheduleJobsInternal(processedJobs, jobDefinition.nPartitions);
|
|
1340
1595
|
};
|
|
1341
1596
|
return jobDefinition;
|
|
1342
1597
|
}
|
|
@@ -1365,6 +1620,7 @@ var defineJob = (options) => {
|
|
|
1365
1620
|
};
|
|
1366
1621
|
|
|
1367
1622
|
// src/service/index.ts
|
|
1623
|
+
import { getInstance as getInstance2, Service as Service4 } from "@orion-js/services";
|
|
1368
1624
|
var serviceMetadata = /* @__PURE__ */ new WeakMap();
|
|
1369
1625
|
var jobsMetadata = /* @__PURE__ */ new Map();
|
|
1370
1626
|
var jobEntriesByClass = /* @__PURE__ */ new Map();
|
|
@@ -1420,7 +1676,7 @@ function initializeJobsIfNeeded(instance) {
|
|
|
1420
1676
|
jobsMetadata.set(instance, jobs);
|
|
1421
1677
|
}
|
|
1422
1678
|
function getServiceJobs(target) {
|
|
1423
|
-
const instance =
|
|
1679
|
+
const instance = getInstance2(target);
|
|
1424
1680
|
if (!serviceMetadata.has(instance.constructor)) {
|
|
1425
1681
|
throw new Error("You must pass a class decorated with @Jobs to getServiceJobs");
|
|
1426
1682
|
}
|
|
@@ -1434,18 +1690,17 @@ function getServiceJobs(target) {
|
|
|
1434
1690
|
}
|
|
1435
1691
|
|
|
1436
1692
|
// src/index.ts
|
|
1437
|
-
var workerService =
|
|
1438
|
-
var
|
|
1439
|
-
var
|
|
1440
|
-
var jobsRepo = getInstance2(JobsRepo);
|
|
1693
|
+
var workerService = getInstance3(WorkerService);
|
|
1694
|
+
var jobsHistoryRepo = getInstance3(JobsHistoryRepo);
|
|
1695
|
+
var jobsRepo = getInstance3(JobsRepo);
|
|
1441
1696
|
var startWorkers = (config) => {
|
|
1442
1697
|
return workerService.startWorkers(config);
|
|
1443
1698
|
};
|
|
1444
1699
|
var scheduleJob = (options) => {
|
|
1445
|
-
return
|
|
1700
|
+
return scheduleJobInternal(options);
|
|
1446
1701
|
};
|
|
1447
1702
|
var scheduleJobs = (jobs) => {
|
|
1448
|
-
return
|
|
1703
|
+
return scheduleJobsInternal(jobs);
|
|
1449
1704
|
};
|
|
1450
1705
|
export {
|
|
1451
1706
|
DEFAULT_MAX_TRIES_REACHED_RETENTION_MS,
|