@orion-js/dogs 4.6.1 → 4.6.4

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 }
@@ -968,10 +999,12 @@ Executor = __decorateElement(_init4, 0, "Executor", _Executor_decorators, Execut
968
999
  __runInitializers(_init4, 1, Executor);
969
1000
 
970
1001
  // src/services/WorkerService.ts
1002
+ var FILTERED_JOB_NAMES_ACQUISITION_HINT = "byJobName";
971
1003
  var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
972
1004
  _WorkerService_decorators = [(0, import_services3.Service)()], _jobsRepo_dec3 = [(0, import_services3.Inject)(() => JobsRepo)], _executor_dec = [(0, import_services3.Inject)(() => Executor)];
973
1005
  var WorkerService = class {
974
1006
  constructor() {
1007
+ __publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
975
1008
  __publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
976
1009
  __publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
977
1010
  }
@@ -1075,9 +1108,11 @@ var WorkerService = class {
1075
1108
  allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
1076
1109
  break;
1077
1110
  }
1111
+ const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
1078
1112
  const jobToRun = await this.jobsRepo.getJobAndLock(
1079
1113
  availableJobNames,
1080
- config.defaultLockTime
1114
+ config.defaultLockTime,
1115
+ jobAcquisitionHint
1081
1116
  );
1082
1117
  if (!jobToRun) {
1083
1118
  import_logger4.logger.debug("No job to run");
@@ -1100,6 +1135,106 @@ var WorkerService = class {
1100
1135
  }
1101
1136
  }
1102
1137
  }
1138
+ getOtherJobAcquisitionHint(hint) {
1139
+ return hint === INITIAL_JOB_ACQUISITION_HINT ? CANDIDATE_JOB_ACQUISITION_HINT : INITIAL_JOB_ACQUISITION_HINT;
1140
+ }
1141
+ getMedian(values) {
1142
+ const sortedValues = [...values].sort((left, right) => left - right);
1143
+ return sortedValues[Math.floor(sortedValues.length / 2)];
1144
+ }
1145
+ resetPendingJobAcquisitionHint(workersInstance) {
1146
+ workersInstance.pendingJobAcquisitionHint = void 0;
1147
+ workersInstance.pendingJobAcquisitionHintWins = 0;
1148
+ }
1149
+ async runJobAcquisitionHintProbe(config, workersInstance) {
1150
+ const jobNames = this.getJobNames(config.jobs);
1151
+ if (jobNames.length === 0 || !workersInstance.running) return;
1152
+ const byHint = {
1153
+ [INITIAL_JOB_ACQUISITION_HINT]: [],
1154
+ [CANDIDATE_JOB_ACQUISITION_HINT]: []
1155
+ };
1156
+ const startsWithInitialHint = workersInstance.jobAcquisitionHintProbeCycle % 2 === 0;
1157
+ const firstHint = startsWithInitialHint ? INITIAL_JOB_ACQUISITION_HINT : CANDIDATE_JOB_ACQUISITION_HINT;
1158
+ const secondHint = this.getOtherJobAcquisitionHint(firstHint);
1159
+ workersInstance.jobAcquisitionHintProbeCycle++;
1160
+ try {
1161
+ for (let sample = 0; sample < 3; sample++) {
1162
+ for (const hint of [firstHint, secondHint]) {
1163
+ if (!workersInstance.running) return;
1164
+ const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
1165
+ jobNames,
1166
+ hint
1167
+ );
1168
+ if (!workersInstance.running) return;
1169
+ byHint[hint].push(executionTimeMillis);
1170
+ }
1171
+ }
1172
+ } catch (error) {
1173
+ this.resetPendingJobAcquisitionHint(workersInstance);
1174
+ if (workersInstance.running) {
1175
+ import_logger4.logger.warn("Job acquisition hint probe failed; keeping the current hint.", { error });
1176
+ }
1177
+ return;
1178
+ }
1179
+ const currentHint = workersInstance.jobAcquisitionHint;
1180
+ const otherHint = this.getOtherJobAcquisitionHint(currentHint);
1181
+ const currentMedian = this.getMedian(byHint[currentHint]);
1182
+ const otherMedian = this.getMedian(byHint[otherHint]);
1183
+ const winner = otherMedian < currentMedian ? otherHint : currentHint;
1184
+ if (!workersInstance.hasCompletedInitialJobAcquisitionHintProbe) {
1185
+ workersInstance.hasCompletedInitialJobAcquisitionHintProbe = true;
1186
+ workersInstance.jobAcquisitionHint = winner;
1187
+ this.resetPendingJobAcquisitionHint(workersInstance);
1188
+ import_logger4.logger.info(`Selected job acquisition hint "${winner}" after startup probe.`);
1189
+ return;
1190
+ }
1191
+ if (winner === currentHint) {
1192
+ this.resetPendingJobAcquisitionHint(workersInstance);
1193
+ import_logger4.logger.debug(`Keeping job acquisition hint "${currentHint}" after probe.`);
1194
+ return;
1195
+ }
1196
+ if (workersInstance.pendingJobAcquisitionHint === winner) {
1197
+ workersInstance.pendingJobAcquisitionHintWins++;
1198
+ } else {
1199
+ workersInstance.pendingJobAcquisitionHint = winner;
1200
+ workersInstance.pendingJobAcquisitionHintWins = 1;
1201
+ }
1202
+ if (workersInstance.pendingJobAcquisitionHintWins < 2) {
1203
+ import_logger4.logger.debug(
1204
+ `Keeping job acquisition hint "${currentHint}"; "${winner}" has one consecutive probe win.`
1205
+ );
1206
+ return;
1207
+ }
1208
+ workersInstance.jobAcquisitionHint = winner;
1209
+ this.resetPendingJobAcquisitionHint(workersInstance);
1210
+ import_logger4.logger.info(`Changed job acquisition hint from "${currentHint}" to "${winner}".`);
1211
+ }
1212
+ async waitForNextJobAcquisitionHintProbe(workersInstance) {
1213
+ await new Promise((resolve) => {
1214
+ let resolved = false;
1215
+ const finishWait = () => {
1216
+ if (resolved) return;
1217
+ resolved = true;
1218
+ clearTimeout(workersInstance.jobAcquisitionHintProbeTimer);
1219
+ workersInstance.jobAcquisitionHintProbeTimer = void 0;
1220
+ workersInstance.cancelJobAcquisitionHintProbeWait = void 0;
1221
+ resolve();
1222
+ };
1223
+ workersInstance.jobAcquisitionHintProbeTimer = setTimeout(
1224
+ finishWait,
1225
+ this.jobAcquisitionHintProbeIntervalMs
1226
+ );
1227
+ workersInstance.cancelJobAcquisitionHintProbeWait = finishWait;
1228
+ });
1229
+ }
1230
+ async runJobAcquisitionHintProbeLoop(config, workersInstance) {
1231
+ if (this.getJobNames(config.jobs).length === 0) return;
1232
+ while (workersInstance.running) {
1233
+ await this.runJobAcquisitionHintProbe(config, workersInstance);
1234
+ if (!workersInstance.running) return;
1235
+ await this.waitForNextJobAcquisitionHintProbe(workersInstance);
1236
+ }
1237
+ }
1103
1238
  createWorkersInstanceDefinition(config) {
1104
1239
  let signalStop;
1105
1240
  const stopSignal = new Promise((resolve) => {
@@ -1111,6 +1246,11 @@ var WorkerService = class {
1111
1246
  activeExecutions: /* @__PURE__ */ new Map(),
1112
1247
  staleExecutions: /* @__PURE__ */ new Map(),
1113
1248
  runningJobsByName: /* @__PURE__ */ new Map(),
1249
+ jobAcquisitionHint: INITIAL_JOB_ACQUISITION_HINT,
1250
+ jobAcquisitionHintProbeCycle: 0,
1251
+ hasCompletedInitialJobAcquisitionHintProbe: false,
1252
+ pendingJobAcquisitionHintWins: 0,
1253
+ jobAcquisitionHintProbePromise: Promise.resolve(),
1114
1254
  schedulerPromise: Promise.resolve(),
1115
1255
  stopSignal,
1116
1256
  signalStop,
@@ -1120,10 +1260,15 @@ var WorkerService = class {
1120
1260
  stop: async () => {
1121
1261
  if (workersInstance.stopPromise) return workersInstance.stopPromise;
1122
1262
  workersInstance.stopPromise = (async () => {
1263
+ var _a;
1123
1264
  import_logger4.logger.info("Stopping job scheduler...");
1124
1265
  workersInstance.running = false;
1125
1266
  workersInstance.signalStop();
1126
- await workersInstance.schedulerPromise;
1267
+ (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1268
+ await Promise.all([
1269
+ workersInstance.schedulerPromise,
1270
+ workersInstance.jobAcquisitionHintProbePromise
1271
+ ]);
1127
1272
  const activeCapacity = Array.from(
1128
1273
  workersInstance.activeExecutions.values(),
1129
1274
  (execution) => execution.capacityPromise
@@ -1171,8 +1316,15 @@ var WorkerService = class {
1171
1316
  setNameToJobs(config.jobs);
1172
1317
  const workersInstance = this.createWorkersInstanceDefinition(config);
1173
1318
  import_logger4.logger.debug("Starting job scheduler", config);
1319
+ workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
1320
+ config,
1321
+ workersInstance
1322
+ );
1174
1323
  workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
1324
+ var _a;
1175
1325
  workersInstance.running = false;
1326
+ workersInstance.signalStop();
1327
+ (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1176
1328
  import_logger4.logger.error("Job scheduler stopped after an unexpected error.", { error });
1177
1329
  });
1178
1330
  return workersInstance;