@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.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 }
@@ -928,6 +959,7 @@ var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
928
959
  _WorkerService_decorators = [Service3()], _jobsRepo_dec3 = [Inject3(() => JobsRepo)], _executor_dec = [Inject3(() => Executor)];
929
960
  var WorkerService = class {
930
961
  constructor() {
962
+ __publicField(this, "jobAcquisitionHintProbeIntervalMs", 30 * 60 * 1e3);
931
963
  __publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
932
964
  __publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
933
965
  }
@@ -1033,7 +1065,8 @@ var WorkerService = class {
1033
1065
  }
1034
1066
  const jobToRun = await this.jobsRepo.getJobAndLock(
1035
1067
  availableJobNames,
1036
- config.defaultLockTime
1068
+ config.defaultLockTime,
1069
+ workersInstance.jobAcquisitionHint
1037
1070
  );
1038
1071
  if (!jobToRun) {
1039
1072
  logger4.debug("No job to run");
@@ -1056,6 +1089,106 @@ var WorkerService = class {
1056
1089
  }
1057
1090
  }
1058
1091
  }
1092
+ getOtherJobAcquisitionHint(hint) {
1093
+ return hint === INITIAL_JOB_ACQUISITION_HINT ? CANDIDATE_JOB_ACQUISITION_HINT : INITIAL_JOB_ACQUISITION_HINT;
1094
+ }
1095
+ getMedian(values) {
1096
+ const sortedValues = [...values].sort((left, right) => left - right);
1097
+ return sortedValues[Math.floor(sortedValues.length / 2)];
1098
+ }
1099
+ resetPendingJobAcquisitionHint(workersInstance) {
1100
+ workersInstance.pendingJobAcquisitionHint = void 0;
1101
+ workersInstance.pendingJobAcquisitionHintWins = 0;
1102
+ }
1103
+ async runJobAcquisitionHintProbe(config, workersInstance) {
1104
+ const jobNames = this.getJobNames(config.jobs);
1105
+ if (jobNames.length === 0 || !workersInstance.running) return;
1106
+ const byHint = {
1107
+ [INITIAL_JOB_ACQUISITION_HINT]: [],
1108
+ [CANDIDATE_JOB_ACQUISITION_HINT]: []
1109
+ };
1110
+ const startsWithInitialHint = workersInstance.jobAcquisitionHintProbeCycle % 2 === 0;
1111
+ const firstHint = startsWithInitialHint ? INITIAL_JOB_ACQUISITION_HINT : CANDIDATE_JOB_ACQUISITION_HINT;
1112
+ const secondHint = this.getOtherJobAcquisitionHint(firstHint);
1113
+ workersInstance.jobAcquisitionHintProbeCycle++;
1114
+ try {
1115
+ for (let sample = 0; sample < 3; sample++) {
1116
+ for (const hint of [firstHint, secondHint]) {
1117
+ if (!workersInstance.running) return;
1118
+ const executionTimeMillis = await this.jobsRepo.getJobAcquisitionExecutionTime(
1119
+ jobNames,
1120
+ hint
1121
+ );
1122
+ if (!workersInstance.running) return;
1123
+ byHint[hint].push(executionTimeMillis);
1124
+ }
1125
+ }
1126
+ } catch (error) {
1127
+ this.resetPendingJobAcquisitionHint(workersInstance);
1128
+ if (workersInstance.running) {
1129
+ logger4.warn("Job acquisition hint probe failed; keeping the current hint.", { error });
1130
+ }
1131
+ return;
1132
+ }
1133
+ const currentHint = workersInstance.jobAcquisitionHint;
1134
+ const otherHint = this.getOtherJobAcquisitionHint(currentHint);
1135
+ const currentMedian = this.getMedian(byHint[currentHint]);
1136
+ const otherMedian = this.getMedian(byHint[otherHint]);
1137
+ const winner = otherMedian < currentMedian ? otherHint : currentHint;
1138
+ if (!workersInstance.hasCompletedInitialJobAcquisitionHintProbe) {
1139
+ workersInstance.hasCompletedInitialJobAcquisitionHintProbe = true;
1140
+ workersInstance.jobAcquisitionHint = winner;
1141
+ this.resetPendingJobAcquisitionHint(workersInstance);
1142
+ logger4.info(`Selected job acquisition hint "${winner}" after startup probe.`);
1143
+ return;
1144
+ }
1145
+ if (winner === currentHint) {
1146
+ this.resetPendingJobAcquisitionHint(workersInstance);
1147
+ logger4.debug(`Keeping job acquisition hint "${currentHint}" after probe.`);
1148
+ return;
1149
+ }
1150
+ if (workersInstance.pendingJobAcquisitionHint === winner) {
1151
+ workersInstance.pendingJobAcquisitionHintWins++;
1152
+ } else {
1153
+ workersInstance.pendingJobAcquisitionHint = winner;
1154
+ workersInstance.pendingJobAcquisitionHintWins = 1;
1155
+ }
1156
+ if (workersInstance.pendingJobAcquisitionHintWins < 2) {
1157
+ logger4.debug(
1158
+ `Keeping job acquisition hint "${currentHint}"; "${winner}" has one consecutive probe win.`
1159
+ );
1160
+ return;
1161
+ }
1162
+ workersInstance.jobAcquisitionHint = winner;
1163
+ this.resetPendingJobAcquisitionHint(workersInstance);
1164
+ logger4.info(`Changed job acquisition hint from "${currentHint}" to "${winner}".`);
1165
+ }
1166
+ async waitForNextJobAcquisitionHintProbe(workersInstance) {
1167
+ await new Promise((resolve) => {
1168
+ let resolved = false;
1169
+ const finishWait = () => {
1170
+ if (resolved) return;
1171
+ resolved = true;
1172
+ clearTimeout(workersInstance.jobAcquisitionHintProbeTimer);
1173
+ workersInstance.jobAcquisitionHintProbeTimer = void 0;
1174
+ workersInstance.cancelJobAcquisitionHintProbeWait = void 0;
1175
+ resolve();
1176
+ };
1177
+ workersInstance.jobAcquisitionHintProbeTimer = setTimeout(
1178
+ finishWait,
1179
+ this.jobAcquisitionHintProbeIntervalMs
1180
+ );
1181
+ workersInstance.cancelJobAcquisitionHintProbeWait = finishWait;
1182
+ });
1183
+ }
1184
+ async runJobAcquisitionHintProbeLoop(config, workersInstance) {
1185
+ if (this.getJobNames(config.jobs).length === 0) return;
1186
+ while (workersInstance.running) {
1187
+ await this.runJobAcquisitionHintProbe(config, workersInstance);
1188
+ if (!workersInstance.running) return;
1189
+ await this.waitForNextJobAcquisitionHintProbe(workersInstance);
1190
+ }
1191
+ }
1059
1192
  createWorkersInstanceDefinition(config) {
1060
1193
  let signalStop;
1061
1194
  const stopSignal = new Promise((resolve) => {
@@ -1067,6 +1200,11 @@ var WorkerService = class {
1067
1200
  activeExecutions: /* @__PURE__ */ new Map(),
1068
1201
  staleExecutions: /* @__PURE__ */ new Map(),
1069
1202
  runningJobsByName: /* @__PURE__ */ new Map(),
1203
+ jobAcquisitionHint: INITIAL_JOB_ACQUISITION_HINT,
1204
+ jobAcquisitionHintProbeCycle: 0,
1205
+ hasCompletedInitialJobAcquisitionHintProbe: false,
1206
+ pendingJobAcquisitionHintWins: 0,
1207
+ jobAcquisitionHintProbePromise: Promise.resolve(),
1070
1208
  schedulerPromise: Promise.resolve(),
1071
1209
  stopSignal,
1072
1210
  signalStop,
@@ -1076,10 +1214,15 @@ var WorkerService = class {
1076
1214
  stop: async () => {
1077
1215
  if (workersInstance.stopPromise) return workersInstance.stopPromise;
1078
1216
  workersInstance.stopPromise = (async () => {
1217
+ var _a;
1079
1218
  logger4.info("Stopping job scheduler...");
1080
1219
  workersInstance.running = false;
1081
1220
  workersInstance.signalStop();
1082
- await workersInstance.schedulerPromise;
1221
+ (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1222
+ await Promise.all([
1223
+ workersInstance.schedulerPromise,
1224
+ workersInstance.jobAcquisitionHintProbePromise
1225
+ ]);
1083
1226
  const activeCapacity = Array.from(
1084
1227
  workersInstance.activeExecutions.values(),
1085
1228
  (execution) => execution.capacityPromise
@@ -1127,8 +1270,15 @@ var WorkerService = class {
1127
1270
  setNameToJobs(config.jobs);
1128
1271
  const workersInstance = this.createWorkersInstanceDefinition(config);
1129
1272
  logger4.debug("Starting job scheduler", config);
1273
+ workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
1274
+ config,
1275
+ workersInstance
1276
+ );
1130
1277
  workersInstance.schedulerPromise = this.runScheduler(config, workersInstance).catch((error) => {
1278
+ var _a;
1131
1279
  workersInstance.running = false;
1280
+ workersInstance.signalStop();
1281
+ (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
1132
1282
  logger4.error("Job scheduler stopped after an unexpected error.", { error });
1133
1283
  });
1134
1284
  return workersInstance;