@orion-js/dogs 4.6.1 → 4.6.2

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 CHANGED
@@ -142,6 +142,35 @@ var JobRecordSchema = (0, import_schema.schemaWithName)("JobRecord", {
142
142
  var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
143
143
 
144
144
  // src/repos/JobsRepo.ts
145
+ var JOB_ACQUISITION_HINTS = {
146
+ byJobName: {
147
+ jobName: 1,
148
+ priority: -1,
149
+ nextRunAt: 1
150
+ },
151
+ globalPriority: {
152
+ priority: -1,
153
+ nextRunAt: 1
154
+ }
155
+ };
156
+ var INITIAL_JOB_ACQUISITION_HINT = "byJobName";
157
+ var CANDIDATE_JOB_ACQUISITION_HINT = "globalPriority";
158
+ var JOB_ACQUISITION_SORT = {
159
+ priority: -1,
160
+ nextRunAt: 1
161
+ };
162
+ function getJobAcquisitionSelector(jobNames, now) {
163
+ return {
164
+ jobName: { $in: jobNames },
165
+ nextRunAt: { $lte: now },
166
+ $and: [
167
+ { $or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: now } }] },
168
+ // Exclude event jobs that have reached max tries. Recurrent jobs keep running even
169
+ // if old records still have this status from previous versions.
170
+ { $or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }] }
171
+ ]
172
+ };
173
+ }
145
174
  var _jobs_dec, _JobsRepo_decorators, _init;
146
175
  _JobsRepo_decorators = [(0, import_mongodb.Repository)()], _jobs_dec = [(0, import_mongodb.MongoCollection)({
147
176
  idGeneration: "uuid",
@@ -149,11 +178,10 @@ _JobsRepo_decorators = [(0, import_mongodb.Repository)()], _jobs_dec = [(0, impo
149
178
  schema: JobRecordSchema,
150
179
  indexes: [
151
180
  {
152
- keys: {
153
- jobName: 1,
154
- priority: -1,
155
- nextRunAt: 1
156
- }
181
+ keys: JOB_ACQUISITION_HINTS.byJobName
182
+ },
183
+ {
184
+ keys: JOB_ACQUISITION_HINTS.globalPriority
157
185
  },
158
186
  {
159
187
  keys: {
@@ -186,30 +214,20 @@ var JobsRepo = class {
186
214
  constructor() {
187
215
  __publicField(this, "jobs", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
188
216
  }
189
- async getJobAndLock(jobNames, lockTime) {
217
+ async getJobAndLock(jobNames, lockTime, hint = INITIAL_JOB_ACQUISITION_HINT) {
190
218
  const executionId = (0, import_helpers.generateId)();
191
- const lockedUntil = new Date(Date.now() + lockTime);
219
+ const now = /* @__PURE__ */ new Date();
220
+ const lockedUntil = new Date(now.getTime() + lockTime);
192
221
  const job = await this.jobs.findOneAndUpdate(
222
+ getJobAcquisitionSelector(jobNames, now),
193
223
  {
194
- jobName: { $in: jobNames },
195
- nextRunAt: { $lte: /* @__PURE__ */ new Date() },
196
- $and: [
197
- { $or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: /* @__PURE__ */ new Date() } }] },
198
- // Exclude event jobs that have reached max tries. Recurrent jobs keep running even
199
- // if old records still have this status from previous versions.
200
- { $or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }] }
201
- ]
202
- },
203
- {
204
- $set: { lockId: executionId, lockedUntil, lastRunAt: /* @__PURE__ */ new Date() },
224
+ $set: { lockId: executionId, lockedUntil, lastRunAt: now },
205
225
  $inc: { tries: 1 }
206
226
  },
207
227
  {
208
228
  mongoOptions: {
209
- sort: {
210
- priority: -1,
211
- nextRunAt: 1
212
- },
229
+ hint: JOB_ACQUISITION_HINTS[hint],
230
+ sort: JOB_ACQUISITION_SORT,
213
231
  returnDocument: "before"
214
232
  }
215
233
  }
@@ -234,6 +252,19 @@ var JobsRepo = class {
234
252
  wasStale
235
253
  };
236
254
  }
255
+ async getJobAcquisitionExecutionTime(jobNames, hint) {
256
+ var _a;
257
+ const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date()), {
258
+ hint: JOB_ACQUISITION_HINTS[hint],
259
+ maxTimeMS: 1e3,
260
+ readPreference: "primary"
261
+ }).sort(JOB_ACQUISITION_SORT).limit(1).explain("executionStats");
262
+ const executionTimeMillis = (_a = explain.executionStats) == null ? void 0 : _a.executionTimeMillis;
263
+ if (typeof executionTimeMillis !== "number" || !Number.isFinite(executionTimeMillis)) {
264
+ throw new Error(`Explain for acquisition hint "${hint}" returned no execution time`);
265
+ }
266
+ return executionTimeMillis;
267
+ }
237
268
  async setJobRecordPriority(jobId, priority, lockId) {
238
269
  const result = await this.jobs.updateOne(lockId ? { _id: jobId, lockId } : jobId, {
239
270
  $set: { priority }
@@ -972,6 +1003,7 @@ var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
972
1003
  _WorkerService_decorators = [(0, import_services3.Service)()], _jobsRepo_dec3 = [(0, import_services3.Inject)(() => JobsRepo)], _executor_dec = [(0, import_services3.Inject)(() => Executor)];
973
1004
  var WorkerService = class {
974
1005
  constructor() {
1006
+ __publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
975
1007
  __publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
976
1008
  __publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
977
1009
  }
@@ -1077,7 +1109,8 @@ var WorkerService = class {
1077
1109
  }
1078
1110
  const jobToRun = await this.jobsRepo.getJobAndLock(
1079
1111
  availableJobNames,
1080
- config.defaultLockTime
1112
+ config.defaultLockTime,
1113
+ workersInstance.jobAcquisitionHint
1081
1114
  );
1082
1115
  if (!jobToRun) {
1083
1116
  import_logger4.logger.debug("No job to run");
@@ -1100,6 +1133,106 @@ var WorkerService = class {
1100
1133
  }
1101
1134
  }
1102
1135
  }
1136
+ getOtherJobAcquisitionHint(hint) {
1137
+ return hint === INITIAL_JOB_ACQUISITION_HINT ? CANDIDATE_JOB_ACQUISITION_HINT : INITIAL_JOB_ACQUISITION_HINT;
1138
+ }
1139
+ getMedian(values) {
1140
+ const sortedValues = [...values].sort((left, right) => left - right);
1141
+ return sortedValues[Math.floor(sortedValues.length / 2)];
1142
+ }
1143
+ resetPendingJobAcquisitionHint(workersInstance) {
1144
+ workersInstance.pendingJobAcquisitionHint = void 0;
1145
+ workersInstance.pendingJobAcquisitionHintWins = 0;
1146
+ }
1147
+ async runJobAcquisitionHintProbe(config, workersInstance) {
1148
+ const jobNames = this.getJobNames(config.jobs);
1149
+ if (jobNames.length === 0 || !workersInstance.running) return;
1150
+ const byHint = {
1151
+ [INITIAL_JOB_ACQUISITION_HINT]: [],
1152
+ [CANDIDATE_JOB_ACQUISITION_HINT]: []
1153
+ };
1154
+ const startsWithInitialHint = workersInstance.jobAcquisitionHintProbeCycle % 2 === 0;
1155
+ const firstHint = startsWithInitialHint ? INITIAL_JOB_ACQUISITION_HINT : CANDIDATE_JOB_ACQUISITION_HINT;
1156
+ const secondHint = this.getOtherJobAcquisitionHint(firstHint);
1157
+ workersInstance.jobAcquisitionHintProbeCycle++;
1158
+ try {
1159
+ for (let sample = 0; sample < 3; sample++) {
1160
+ for (const hint of [firstHint, secondHint]) {
1161
+ if (!workersInstance.running) return;
1162
+ const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
1163
+ jobNames,
1164
+ hint
1165
+ );
1166
+ if (!workersInstance.running) return;
1167
+ byHint[hint].push(executionTimeMillis);
1168
+ }
1169
+ }
1170
+ } catch (error) {
1171
+ this.resetPendingJobAcquisitionHint(workersInstance);
1172
+ if (workersInstance.running) {
1173
+ import_logger4.logger.warn("Job acquisition hint probe failed; keeping the current hint.", { error });
1174
+ }
1175
+ return;
1176
+ }
1177
+ const currentHint = workersInstance.jobAcquisitionHint;
1178
+ const otherHint = this.getOtherJobAcquisitionHint(currentHint);
1179
+ const currentMedian = this.getMedian(byHint[currentHint]);
1180
+ const otherMedian = this.getMedian(byHint[otherHint]);
1181
+ const winner = otherMedian < currentMedian ? otherHint : currentHint;
1182
+ if (!workersInstance.hasCompletedInitialJobAcquisitionHintProbe) {
1183
+ workersInstance.hasCompletedInitialJobAcquisitionHintProbe = true;
1184
+ workersInstance.jobAcquisitionHint = winner;
1185
+ this.resetPendingJobAcquisitionHint(workersInstance);
1186
+ import_logger4.logger.info(`Selected job acquisition hint "${winner}" after startup probe.`);
1187
+ return;
1188
+ }
1189
+ if (winner === currentHint) {
1190
+ this.resetPendingJobAcquisitionHint(workersInstance);
1191
+ import_logger4.logger.debug(`Keeping job acquisition hint "${currentHint}" after probe.`);
1192
+ return;
1193
+ }
1194
+ if (workersInstance.pendingJobAcquisitionHint === winner) {
1195
+ workersInstance.pendingJobAcquisitionHintWins++;
1196
+ } else {
1197
+ workersInstance.pendingJobAcquisitionHint = winner;
1198
+ workersInstance.pendingJobAcquisitionHintWins = 1;
1199
+ }
1200
+ if (workersInstance.pendingJobAcquisitionHintWins < 2) {
1201
+ import_logger4.logger.debug(
1202
+ `Keeping job acquisition hint "${currentHint}"; "${winner}" has one consecutive probe win.`
1203
+ );
1204
+ return;
1205
+ }
1206
+ workersInstance.jobAcquisitionHint = winner;
1207
+ this.resetPendingJobAcquisitionHint(workersInstance);
1208
+ import_logger4.logger.info(`Changed job acquisition hint from "${currentHint}" to "${winner}".`);
1209
+ }
1210
+ async waitForNextJobAcquisitionHintProbe(workersInstance) {
1211
+ await new Promise((resolve) => {
1212
+ let resolved = false;
1213
+ const finishWait = () => {
1214
+ if (resolved) return;
1215
+ resolved = true;
1216
+ clearTimeout(workersInstance.jobAcquisitionHintProbeTimer);
1217
+ workersInstance.jobAcquisitionHintProbeTimer = void 0;
1218
+ workersInstance.cancelJobAcquisitionHintProbeWait = void 0;
1219
+ resolve();
1220
+ };
1221
+ workersInstance.jobAcquisitionHintProbeTimer = setTimeout(
1222
+ finishWait,
1223
+ this.jobAcquisitionHintProbeIntervalMs
1224
+ );
1225
+ workersInstance.cancelJobAcquisitionHintProbeWait = finishWait;
1226
+ });
1227
+ }
1228
+ async runJobAcquisitionHintProbeLoop(config, workersInstance) {
1229
+ if (this.getJobNames(config.jobs).length === 0) return;
1230
+ while (workersInstance.running) {
1231
+ await this.runJobAcquisitionHintProbe(config, workersInstance);
1232
+ if (!workersInstance.running) return;
1233
+ await this.waitForNextJobAcquisitionHintProbe(workersInstance);
1234
+ }
1235
+ }
1103
1236
  createWorkersInstanceDefinition(config) {
1104
1237
  let signalStop;
1105
1238
  const stopSignal = new Promise((resolve) => {
@@ -1111,6 +1244,11 @@ var WorkerService = class {
1111
1244
  activeExecutions: /* @__PURE__ */ new Map(),
1112
1245
  staleExecutions: /* @__PURE__ */ new Map(),
1113
1246
  runningJobsByName: /* @__PURE__ */ new Map(),
1247
+ jobAcquisitionHint: INITIAL_JOB_ACQUISITION_HINT,
1248
+ jobAcquisitionHintProbeCycle: 0,
1249
+ hasCompletedInitialJobAcquisitionHintProbe: false,
1250
+ pendingJobAcquisitionHintWins: 0,
1251
+ jobAcquisitionHintProbePromise: Promise.resolve(),
1114
1252
  schedulerPromise: Promise.resolve(),
1115
1253
  stopSignal,
1116
1254
  signalStop,
@@ -1120,10 +1258,15 @@ var WorkerService = class {
1120
1258
  stop: async () => {
1121
1259
  if (workersInstance.stopPromise) return workersInstance.stopPromise;
1122
1260
  workersInstance.stopPromise = (async () => {
1261
+ var _a;
1123
1262
  import_logger4.logger.info("Stopping job scheduler...");
1124
1263
  workersInstance.running = false;
1125
1264
  workersInstance.signalStop();
1126
- await workersInstance.schedulerPromise;
1265
+ (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1266
+ await Promise.all([
1267
+ workersInstance.schedulerPromise,
1268
+ workersInstance.jobAcquisitionHintProbePromise
1269
+ ]);
1127
1270
  const activeCapacity = Array.from(
1128
1271
  workersInstance.activeExecutions.values(),
1129
1272
  (execution) => execution.capacityPromise
@@ -1171,8 +1314,15 @@ var WorkerService = class {
1171
1314
  setNameToJobs(config.jobs);
1172
1315
  const workersInstance = this.createWorkersInstanceDefinition(config);
1173
1316
  import_logger4.logger.debug("Starting job scheduler", config);
1317
+ workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
1318
+ config,
1319
+ workersInstance
1320
+ );
1174
1321
  workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
1322
+ var _a;
1175
1323
  workersInstance.running = false;
1324
+ workersInstance.signalStop();
1325
+ (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1176
1326
  import_logger4.logger.error("Job scheduler stopped after an unexpected error.", { error });
1177
1327
  });
1178
1328
  return workersInstance;