@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.js CHANGED
@@ -98,6 +98,35 @@ var JobRecordSchema = schemaWithName("JobRecord", {
98
98
  var DEFAULT_MAX_TRIES_REACHED_RETENTION_MS = 1e3 * 60 * 60 * 24 * 7;
99
99
 
100
100
  // src/repos/JobsRepo.ts
101
+ var JOB_ACQUISITION_HINTS = {
102
+ byJobName: {
103
+ jobName: 1,
104
+ priority: -1,
105
+ nextRunAt: 1
106
+ },
107
+ globalPriority: {
108
+ priority: -1,
109
+ nextRunAt: 1
110
+ }
111
+ };
112
+ var INITIAL_JOB_ACQUISITION_HINT = "byJobName";
113
+ var CANDIDATE_JOB_ACQUISITION_HINT = "globalPriority";
114
+ var JOB_ACQUISITION_SORT = {
115
+ priority: -1,
116
+ nextRunAt: 1
117
+ };
118
+ function getJobAcquisitionSelector(jobNames, now) {
119
+ return {
120
+ jobName: { $in: jobNames },
121
+ nextRunAt: { $lte: now },
122
+ $and: [
123
+ { $or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: now } }] },
124
+ // Exclude event jobs that have reached max tries. Recurrent jobs keep running even
125
+ // if old records still have this status from previous versions.
126
+ { $or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }] }
127
+ ]
128
+ };
129
+ }
101
130
  var _jobs_dec, _JobsRepo_decorators, _init;
102
131
  _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
103
132
  idGeneration: "uuid",
@@ -105,11 +134,10 @@ _JobsRepo_decorators = [Repository()], _jobs_dec = [MongoCollection({
105
134
  schema: JobRecordSchema,
106
135
  indexes: [
107
136
  {
108
- keys: {
109
- jobName: 1,
110
- priority: -1,
111
- nextRunAt: 1
112
- }
137
+ keys: JOB_ACQUISITION_HINTS.byJobName
138
+ },
139
+ {
140
+ keys: JOB_ACQUISITION_HINTS.globalPriority
113
141
  },
114
142
  {
115
143
  keys: {
@@ -142,30 +170,20 @@ var JobsRepo = class {
142
170
  constructor() {
143
171
  __publicField(this, "jobs", __runInitializers(_init, 8, this)), __runInitializers(_init, 11, this);
144
172
  }
145
- async getJobAndLock(jobNames, lockTime) {
173
+ async getJobAndLock(jobNames, lockTime, hint = INITIAL_JOB_ACQUISITION_HINT) {
146
174
  const executionId = generateId();
147
- const lockedUntil = new Date(Date.now() + lockTime);
175
+ const now = /* @__PURE__ */ new Date();
176
+ const lockedUntil = new Date(now.getTime() + lockTime);
148
177
  const job = await this.jobs.findOneAndUpdate(
178
+ getJobAcquisitionSelector(jobNames, now),
149
179
  {
150
- jobName: { $in: jobNames },
151
- nextRunAt: { $lte: /* @__PURE__ */ new Date() },
152
- $and: [
153
- { $or: [{ lockedUntil: { $exists: false } }, { lockedUntil: { $lte: /* @__PURE__ */ new Date() } }] },
154
- // Exclude event jobs that have reached max tries. Recurrent jobs keep running even
155
- // if old records still have this status from previous versions.
156
- { $or: [{ type: { $ne: "event" } }, { status: { $ne: "maxTriesReached" } }] }
157
- ]
158
- },
159
- {
160
- $set: { lockId: executionId, lockedUntil, lastRunAt: /* @__PURE__ */ new Date() },
180
+ $set: { lockId: executionId, lockedUntil, lastRunAt: now },
161
181
  $inc: { tries: 1 }
162
182
  },
163
183
  {
164
184
  mongoOptions: {
165
- sort: {
166
- priority: -1,
167
- nextRunAt: 1
168
- },
185
+ hint: JOB_ACQUISITION_HINTS[hint],
186
+ sort: JOB_ACQUISITION_SORT,
169
187
  returnDocument: "before"
170
188
  }
171
189
  }
@@ -190,6 +208,19 @@ var JobsRepo = class {
190
208
  wasStale
191
209
  };
192
210
  }
211
+ async getJobAcquisitionExecutionTime(jobNames, hint) {
212
+ var _a;
213
+ const explain = await this.jobs.find(getJobAcquisitionSelector(jobNames, /* @__PURE__ */ new Date()), {
214
+ hint: JOB_ACQUISITION_HINTS[hint],
215
+ maxTimeMS: 1e3,
216
+ readPreference: "primary"
217
+ }).sort(JOB_ACQUISITION_SORT).limit(1).explain("executionStats");
218
+ const executionTimeMillis = (_a = explain.executionStats) == null ? void 0 : _a.executionTimeMillis;
219
+ if (typeof executionTimeMillis !== "number" || !Number.isFinite(executionTimeMillis)) {
220
+ throw new Error(`Explain for acquisition hint "${hint}" returned no execution time`);
221
+ }
222
+ return executionTimeMillis;
223
+ }
193
224
  async setJobRecordPriority(jobId, priority, lockId) {
194
225
  const result = await this.jobs.updateOne(lockId ? { _id: jobId, lockId } : jobId, {
195
226
  $set: { priority }
@@ -924,10 +955,12 @@ Executor = __decorateElement(_init4, 0, "Executor", _Executor_decorators, Execut
924
955
  __runInitializers(_init4, 1, Executor);
925
956
 
926
957
  // src/services/WorkerService.ts
958
+ var FILTERED_JOB_NAMES_ACQUISITION_HINT = "byJobName";
927
959
  var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
928
960
  _WorkerService_decorators = [Service3()], _jobsRepo_dec3 = [Inject3(() => JobsRepo)], _executor_dec = [Inject3(() => Executor)];
929
961
  var WorkerService = class {
930
962
  constructor() {
963
+ __publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
931
964
  __publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
932
965
  __publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
933
966
  }
@@ -1031,9 +1064,11 @@ var WorkerService = class {
1031
1064
  allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
1032
1065
  break;
1033
1066
  }
1067
+ const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
1034
1068
  const jobToRun = await this.jobsRepo.getJobAndLock(
1035
1069
  availableJobNames,
1036
- config.defaultLockTime
1070
+ config.defaultLockTime,
1071
+ jobAcquisitionHint
1037
1072
  );
1038
1073
  if (!jobToRun) {
1039
1074
  logger4.debug("No job to run");
@@ -1056,6 +1091,106 @@ var WorkerService = class {
1056
1091
  }
1057
1092
  }
1058
1093
  }
1094
+ getOtherJobAcquisitionHint(hint) {
1095
+ return hint === INITIAL_JOB_ACQUISITION_HINT ? CANDIDATE_JOB_ACQUISITION_HINT : INITIAL_JOB_ACQUISITION_HINT;
1096
+ }
1097
+ getMedian(values) {
1098
+ const sortedValues = [...values].sort((left, right) => left - right);
1099
+ return sortedValues[Math.floor(sortedValues.length / 2)];
1100
+ }
1101
+ resetPendingJobAcquisitionHint(workersInstance) {
1102
+ workersInstance.pendingJobAcquisitionHint = void 0;
1103
+ workersInstance.pendingJobAcquisitionHintWins = 0;
1104
+ }
1105
+ async runJobAcquisitionHintProbe(config, workersInstance) {
1106
+ const jobNames = this.getJobNames(config.jobs);
1107
+ if (jobNames.length === 0 || !workersInstance.running) return;
1108
+ const byHint = {
1109
+ [INITIAL_JOB_ACQUISITION_HINT]: [],
1110
+ [CANDIDATE_JOB_ACQUISITION_HINT]: []
1111
+ };
1112
+ const startsWithInitialHint = workersInstance.jobAcquisitionHintProbeCycle % 2 === 0;
1113
+ const firstHint = startsWithInitialHint ? INITIAL_JOB_ACQUISITION_HINT : CANDIDATE_JOB_ACQUISITION_HINT;
1114
+ const secondHint = this.getOtherJobAcquisitionHint(firstHint);
1115
+ workersInstance.jobAcquisitionHintProbeCycle++;
1116
+ try {
1117
+ for (let sample = 0; sample < 3; sample++) {
1118
+ for (const hint of [firstHint, secondHint]) {
1119
+ if (!workersInstance.running) return;
1120
+ const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
1121
+ jobNames,
1122
+ hint
1123
+ );
1124
+ if (!workersInstance.running) return;
1125
+ byHint[hint].push(executionTimeMillis);
1126
+ }
1127
+ }
1128
+ } catch (error) {
1129
+ this.resetPendingJobAcquisitionHint(workersInstance);
1130
+ if (workersInstance.running) {
1131
+ logger4.warn("Job acquisition hint probe failed; keeping the current hint.", { error });
1132
+ }
1133
+ return;
1134
+ }
1135
+ const currentHint = workersInstance.jobAcquisitionHint;
1136
+ const otherHint = this.getOtherJobAcquisitionHint(currentHint);
1137
+ const currentMedian = this.getMedian(byHint[currentHint]);
1138
+ const otherMedian = this.getMedian(byHint[otherHint]);
1139
+ const winner = otherMedian < currentMedian ? otherHint : currentHint;
1140
+ if (!workersInstance.hasCompletedInitialJobAcquisitionHintProbe) {
1141
+ workersInstance.hasCompletedInitialJobAcquisitionHintProbe = true;
1142
+ workersInstance.jobAcquisitionHint = winner;
1143
+ this.resetPendingJobAcquisitionHint(workersInstance);
1144
+ logger4.info(`Selected job acquisition hint "${winner}" after startup probe.`);
1145
+ return;
1146
+ }
1147
+ if (winner === currentHint) {
1148
+ this.resetPendingJobAcquisitionHint(workersInstance);
1149
+ logger4.debug(`Keeping job acquisition hint "${currentHint}" after probe.`);
1150
+ return;
1151
+ }
1152
+ if (workersInstance.pendingJobAcquisitionHint === winner) {
1153
+ workersInstance.pendingJobAcquisitionHintWins++;
1154
+ } else {
1155
+ workersInstance.pendingJobAcquisitionHint = winner;
1156
+ workersInstance.pendingJobAcquisitionHintWins = 1;
1157
+ }
1158
+ if (workersInstance.pendingJobAcquisitionHintWins < 2) {
1159
+ logger4.debug(
1160
+ `Keeping job acquisition hint "${currentHint}"; "${winner}" has one consecutive probe win.`
1161
+ );
1162
+ return;
1163
+ }
1164
+ workersInstance.jobAcquisitionHint = winner;
1165
+ this.resetPendingJobAcquisitionHint(workersInstance);
1166
+ logger4.info(`Changed job acquisition hint from "${currentHint}" to "${winner}".`);
1167
+ }
1168
+ async waitForNextJobAcquisitionHintProbe(workersInstance) {
1169
+ await new Promise((resolve) => {
1170
+ let resolved = false;
1171
+ const finishWait = () => {
1172
+ if (resolved) return;
1173
+ resolved = true;
1174
+ clearTimeout(workersInstance.jobAcquisitionHintProbeTimer);
1175
+ workersInstance.jobAcquisitionHintProbeTimer = void 0;
1176
+ workersInstance.cancelJobAcquisitionHintProbeWait = void 0;
1177
+ resolve();
1178
+ };
1179
+ workersInstance.jobAcquisitionHintProbeTimer = setTimeout(
1180
+ finishWait,
1181
+ this.jobAcquisitionHintProbeIntervalMs
1182
+ );
1183
+ workersInstance.cancelJobAcquisitionHintProbeWait = finishWait;
1184
+ });
1185
+ }
1186
+ async runJobAcquisitionHintProbeLoop(config, workersInstance) {
1187
+ if (this.getJobNames(config.jobs).length === 0) return;
1188
+ while (workersInstance.running) {
1189
+ await this.runJobAcquisitionHintProbe(config, workersInstance);
1190
+ if (!workersInstance.running) return;
1191
+ await this.waitForNextJobAcquisitionHintProbe(workersInstance);
1192
+ }
1193
+ }
1059
1194
  createWorkersInstanceDefinition(config) {
1060
1195
  let signalStop;
1061
1196
  const stopSignal = new Promise((resolve) => {
@@ -1067,6 +1202,11 @@ var WorkerService = class {
1067
1202
  activeExecutions: /* @__PURE__ */ new Map(),
1068
1203
  staleExecutions: /* @__PURE__ */ new Map(),
1069
1204
  runningJobsByName: /* @__PURE__ */ new Map(),
1205
+ jobAcquisitionHint: INITIAL_JOB_ACQUISITION_HINT,
1206
+ jobAcquisitionHintProbeCycle: 0,
1207
+ hasCompletedInitialJobAcquisitionHintProbe: false,
1208
+ pendingJobAcquisitionHintWins: 0,
1209
+ jobAcquisitionHintProbePromise: Promise.resolve(),
1070
1210
  schedulerPromise: Promise.resolve(),
1071
1211
  stopSignal,
1072
1212
  signalStop,
@@ -1076,10 +1216,15 @@ var WorkerService = class {
1076
1216
  stop: async () => {
1077
1217
  if (workersInstance.stopPromise) return workersInstance.stopPromise;
1078
1218
  workersInstance.stopPromise = (async () => {
1219
+ var _a;
1079
1220
  logger4.info("Stopping job scheduler...");
1080
1221
  workersInstance.running = false;
1081
1222
  workersInstance.signalStop();
1082
- await workersInstance.schedulerPromise;
1223
+ (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1224
+ await Promise.all([
1225
+ workersInstance.schedulerPromise,
1226
+ workersInstance.jobAcquisitionHintProbePromise
1227
+ ]);
1083
1228
  const activeCapacity = Array.from(
1084
1229
  workersInstance.activeExecutions.values(),
1085
1230
  (execution) => execution.capacityPromise
@@ -1127,8 +1272,15 @@ var WorkerService = class {
1127
1272
  setNameToJobs(config.jobs);
1128
1273
  const workersInstance = this.createWorkersInstanceDefinition(config);
1129
1274
  logger4.debug("Starting job scheduler", config);
1275
+ workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
1276
+ config,
1277
+ workersInstance
1278
+ );
1130
1279
  workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
1280
+ var _a;
1131
1281
  workersInstance.running = false;
1282
+ workersInstance.signalStop();
1283
+ (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1132
1284
  logger4.error("Job scheduler stopped after an unexpected error.", { error });
1133
1285
  });
1134
1286
  return workersInstance;