@orion-js/dogs 4.6.7 → 4.6.9

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
@@ -480,6 +480,17 @@ var JobsRepo = class {
480
480
  );
481
481
  return result.matchedCount > 0;
482
482
  }
483
+ async rescheduleAfterExecutionGuardAcquisitionFailure(jobId, lockId, nextRunAt) {
484
+ const result = await this.jobs.updateOne(
485
+ { _id: jobId, lockId, tries: { $gte: 1 } },
486
+ {
487
+ $set: { nextRunAt },
488
+ $inc: { tries: -1 },
489
+ $unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
490
+ }
491
+ );
492
+ return result.matchedCount > 0;
493
+ }
483
494
  async deleteEventJob(jobId, lockId) {
484
495
  const result = await this.jobs.deleteOne({
485
496
  _id: jobId,
@@ -828,7 +839,7 @@ function scheduleJobsInternal(jobs, nPartitions) {
828
839
 
829
840
  // src/services/WorkerService.ts
830
841
  import { sleep } from "@orion-js/helpers";
831
- import { logger as logger4 } from "@orion-js/logger";
842
+ import { logger as logger5 } from "@orion-js/logger";
832
843
  import { Inject as Inject3, Service as Service3 } from "@orion-js/services";
833
844
 
834
845
  // src/services/Executor.ts
@@ -1080,6 +1091,42 @@ var Executor = class {
1080
1091
  }
1081
1092
  return false;
1082
1093
  }
1094
+ async handleExecutionGuardAcquisitionFailure(error, config, jobToRun, context) {
1095
+ context.logger.error(`Error acquiring execution guard for job "${jobToRun.name}"`, { error });
1096
+ const nextRunAt = new Date(Date.now() + (config.executionGuardRetryDelayMs ?? 1e3));
1097
+ try {
1098
+ const didReschedule = await this.jobsRepo.rescheduleAfterExecutionGuardAcquisitionFailure(
1099
+ jobToRun.jobId,
1100
+ jobToRun.lockId,
1101
+ nextRunAt
1102
+ );
1103
+ if (!didReschedule) {
1104
+ context.logger.warn(
1105
+ `Could not requeue job "${jobToRun.name}" after execution guard acquisition failed because its lock is no longer owned`
1106
+ );
1107
+ }
1108
+ } catch (rescheduleError) {
1109
+ context.logger.error(
1110
+ `Error requeueing job "${jobToRun.name}" after execution guard acquisition failed`,
1111
+ { error: rescheduleError }
1112
+ );
1113
+ }
1114
+ }
1115
+ async releaseExecutionGuardLease(lease, jobToRun, context) {
1116
+ try {
1117
+ await lease.release();
1118
+ } catch (error) {
1119
+ context.logger.error(`Error releasing execution guard for job "${jobToRun.name}"`, { error });
1120
+ }
1121
+ }
1122
+ async runGuardedResolver(job, jobToRun, context, lease) {
1123
+ try {
1124
+ return await job.resolve(jobToRun.params, context);
1125
+ } finally {
1126
+ context.clearStaleTimeout();
1127
+ await this.releaseExecutionGuardLease(lease, jobToRun, context);
1128
+ }
1129
+ }
1083
1130
  async executeJob(config, jobToRun, onExecutionStale) {
1084
1131
  const job = this.getJobDefinition(jobToRun, config.jobs);
1085
1132
  if (!job) return;
@@ -1161,13 +1208,36 @@ var Executor = class {
1161
1208
  params: jobToRun.params
1162
1209
  };
1163
1210
  await runWithOrionAsyncContext(extraContext, async () => {
1164
- try {
1211
+ let executionGuardLease;
1212
+ if (config.executionGuard) {
1165
1213
  updateOrionAsyncContext({
1166
1214
  jobName: jobToRun.name,
1167
1215
  jobId: jobToRun.jobId,
1168
1216
  params: jobToRun.params
1169
1217
  });
1170
- const result = await job.resolve(jobToRun.params, context);
1218
+ try {
1219
+ executionGuardLease = await config.executionGuard.acquire(jobToRun);
1220
+ } catch (error) {
1221
+ context.clearStaleTimeout();
1222
+ await this.handleExecutionGuardAcquisitionFailure(error, config, jobToRun, context);
1223
+ if (executionStatus === "stale") await staleHandlingPromise;
1224
+ return;
1225
+ }
1226
+ if (executionStatus === "stale") {
1227
+ await staleHandlingPromise;
1228
+ await this.releaseExecutionGuardLease(executionGuardLease, jobToRun, context);
1229
+ return;
1230
+ }
1231
+ }
1232
+ try {
1233
+ if (!config.executionGuard) {
1234
+ updateOrionAsyncContext({
1235
+ jobName: jobToRun.name,
1236
+ jobId: jobToRun.jobId,
1237
+ params: jobToRun.params
1238
+ });
1239
+ }
1240
+ const result = executionGuardLease ? await this.runGuardedResolver(job, jobToRun, context, executionGuardLease) : await job.resolve(jobToRun.params, context);
1171
1241
  context.clearStaleTimeout();
1172
1242
  if (executionStatus === "stale") {
1173
1243
  await staleHandlingPromise;
@@ -1236,6 +1306,76 @@ __decorateElement(_init4, 5, "jobsHistoryRepo", _jobsHistoryRepo_dec, Executor);
1236
1306
  Executor = __decorateElement(_init4, 0, "Executor", _Executor_decorators, Executor);
1237
1307
  __runInitializers(_init4, 1, Executor);
1238
1308
 
1309
+ // src/services/jobFilters.ts
1310
+ import { logger as logger4 } from "@orion-js/logger";
1311
+ var DEFAULT_JOBS_FILTER_TIMEOUT_MS = 5e3;
1312
+ var warnedUnknownJobNames = /* @__PURE__ */ new Set();
1313
+ async function withTimeout(promise, timeoutMs, name) {
1314
+ let timer;
1315
+ const timeout = new Promise((_, reject) => {
1316
+ timer = setTimeout(() => reject(new Error(`${name} timed out after ${timeoutMs}ms`)), timeoutMs);
1317
+ });
1318
+ try {
1319
+ return await Promise.race([promise, timeout]);
1320
+ } finally {
1321
+ clearTimeout(timer);
1322
+ }
1323
+ }
1324
+ async function runCheck(options, name, run) {
1325
+ if (options.pendingChecks.has(name)) {
1326
+ throw new Error(`${name} is still running from a previous claim`);
1327
+ }
1328
+ const promise = Promise.resolve().then(run);
1329
+ options.pendingChecks.set(name, promise);
1330
+ void promise.then(
1331
+ () => options.pendingChecks.delete(name),
1332
+ () => options.pendingChecks.delete(name)
1333
+ );
1334
+ return await withTimeout(promise, options.timeoutMs, name);
1335
+ }
1336
+ function warnUnknownJobNames(jobs, filter) {
1337
+ for (const jobName of [...filter.allow ?? [], ...filter.deny ?? []]) {
1338
+ if (jobs[jobName] || warnedUnknownJobNames.has(jobName)) continue;
1339
+ warnedUnknownJobNames.add(jobName);
1340
+ logger4.warn(`getJobsFilter returned unknown job name "${jobName}"; it will be ignored.`);
1341
+ }
1342
+ }
1343
+ async function filterByJobsFilter(options, getJobsFilter) {
1344
+ try {
1345
+ const filter = await runCheck(options, "getJobsFilter", getJobsFilter) ?? {};
1346
+ warnUnknownJobNames(options.jobs, filter);
1347
+ return options.jobNames.filter((jobName) => {
1348
+ var _a;
1349
+ if (filter.allow && !filter.allow.includes(jobName)) return false;
1350
+ if ((_a = filter.deny) == null ? void 0 : _a.includes(jobName)) return false;
1351
+ return true;
1352
+ });
1353
+ } catch (error) {
1354
+ logger4.error("Error in getJobsFilter; no jobs will be taken on this claim.", { error });
1355
+ return [];
1356
+ }
1357
+ }
1358
+ async function canTakeJob(options, jobName) {
1359
+ const canTakeJobs = options.jobs[jobName].canTakeJobs;
1360
+ if (!canTakeJobs) return true;
1361
+ try {
1362
+ return await runCheck(options, `canTakeJobs of "${jobName}"`, canTakeJobs) === true;
1363
+ } catch (error) {
1364
+ logger4.error(`Error in canTakeJobs of job "${jobName}"; it will not be taken.`, { error });
1365
+ return false;
1366
+ }
1367
+ }
1368
+ async function getTakeableJobNames(inputOptions) {
1369
+ const options = {
1370
+ timeoutMs: DEFAULT_JOBS_FILTER_TIMEOUT_MS,
1371
+ pendingChecks: /* @__PURE__ */ new Map(),
1372
+ ...inputOptions
1373
+ };
1374
+ const jobNames = inputOptions.getJobsFilter ? await filterByJobsFilter(options, inputOptions.getJobsFilter) : options.jobNames;
1375
+ const checks = await Promise.all(jobNames.map((jobName) => canTakeJob(options, jobName)));
1376
+ return jobNames.filter((_, index) => checks[index]);
1377
+ }
1378
+
1239
1379
  // src/services/WorkerService.ts
1240
1380
  var FILTERED_JOB_NAMES_ACQUISITION_HINT = "byJobName";
1241
1381
  var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
@@ -1247,6 +1387,7 @@ var WorkerService = class {
1247
1387
  __publicField(this, "partitionReconcilerJitterMs", 30 * 1e3);
1248
1388
  __publicField(this, "partitionReconcilerBatchPauseMs", 250);
1249
1389
  __publicField(this, "partitionReconcilerBatchSize", 500);
1390
+ __publicField(this, "jobsFilterTimeoutMs", DEFAULT_JOBS_FILTER_TIMEOUT_MS);
1250
1391
  __publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
1251
1392
  __publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
1252
1393
  }
@@ -1305,13 +1446,13 @@ var WorkerService = class {
1305
1446
  };
1306
1447
  this.reserveJobExecution(workersInstance, jobToRun.name);
1307
1448
  workersInstance.activeExecutions.set(jobToRun.executionId, execution);
1308
- logger4.debug(`Starting job execution "${jobToRun.executionId}"`, jobToRun);
1449
+ logger5.debug(`Starting job execution "${jobToRun.executionId}"`, jobToRun);
1309
1450
  const executionPromise = this.executor.executeJob(config, jobToRun, () => {
1310
1451
  this.releaseExecutionCapacity(workersInstance, execution, "stale");
1311
1452
  });
1312
1453
  execution.promise = executionPromise;
1313
1454
  void executionPromise.catch((error) => {
1314
- logger4.error(`Unhandled error executing job "${jobToRun.name}"`, { error });
1455
+ logger5.error(`Unhandled error executing job "${jobToRun.name}"`, { error });
1315
1456
  }).finally(() => {
1316
1457
  if (execution.status === "running") {
1317
1458
  this.releaseExecutionCapacity(workersInstance, execution, "finished");
@@ -1346,20 +1487,39 @@ var WorkerService = class {
1346
1487
  jobs: config.jobs,
1347
1488
  maxTries: config.maxTries,
1348
1489
  maxTriesReachedRetentionMs: config.maxTriesReachedRetentionMs,
1349
- onMaxTriesReached: config.onMaxTriesReached
1490
+ onMaxTriesReached: config.onMaxTriesReached,
1491
+ ...config.executionGuard ? {
1492
+ executionGuard: config.executionGuard,
1493
+ executionGuardRetryDelayMs: Math.max(config.pollInterval, 1e3)
1494
+ } : {}
1350
1495
  };
1351
- logger4.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
1496
+ logger5.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
1352
1497
  let consecutivePartitionMisses = 0;
1353
1498
  while (workersInstance.running) {
1354
1499
  let didMiss = false;
1355
1500
  let allJobNamesAtCapacity = false;
1356
1501
  try {
1357
1502
  while (workersInstance.activeExecutions.size < config.workersCount) {
1358
- const availableJobNames = this.getAvailableJobNames(config, workersInstance, jobNames);
1359
- if (availableJobNames.length === 0) {
1503
+ const jobNamesWithCapacity = this.getAvailableJobNames(config, workersInstance, jobNames);
1504
+ if (jobNamesWithCapacity.length === 0) {
1360
1505
  allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
1361
1506
  break;
1362
1507
  }
1508
+ const availableJobNames = await Promise.race([
1509
+ getTakeableJobNames({
1510
+ jobs: config.jobs,
1511
+ getJobsFilter: config.getJobsFilter,
1512
+ jobNames: jobNamesWithCapacity,
1513
+ timeoutMs: this.jobsFilterTimeoutMs,
1514
+ pendingChecks: workersInstance.pendingFilterChecks
1515
+ }),
1516
+ workersInstance.stopSignal.then(() => [])
1517
+ ]);
1518
+ if (!workersInstance.running) break;
1519
+ if (availableJobNames.length === 0) {
1520
+ didMiss = true;
1521
+ break;
1522
+ }
1363
1523
  const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
1364
1524
  const partition = this.getNextPartition(config, workersInstance);
1365
1525
  const jobToRun = await this.jobsRepo.getJobAndLock(
@@ -1370,7 +1530,7 @@ var WorkerService = class {
1370
1530
  );
1371
1531
  if (!jobToRun) {
1372
1532
  consecutivePartitionMisses++;
1373
- logger4.debug(`No job to run in partition ${partition}`);
1533
+ logger5.debug(`No job to run in partition ${partition}`);
1374
1534
  if (consecutivePartitionMisses >= config.nPartitions) {
1375
1535
  consecutivePartitionMisses = 0;
1376
1536
  didMiss = true;
@@ -1383,7 +1543,7 @@ var WorkerService = class {
1383
1543
  if (!workersInstance.running) break;
1384
1544
  }
1385
1545
  } catch (error) {
1386
- logger4.error("Error in job scheduler.", { error });
1546
+ logger5.error("Error in job scheduler.", { error });
1387
1547
  consecutivePartitionMisses = 0;
1388
1548
  didMiss = true;
1389
1549
  }
@@ -1435,7 +1595,7 @@ var WorkerService = class {
1435
1595
  } catch (error) {
1436
1596
  this.resetPendingJobAcquisitionHint(workersInstance);
1437
1597
  if (workersInstance.running) {
1438
- logger4.warn("Job acquisition hint probe failed; keeping the current hint.", { error });
1598
+ logger5.warn("Job acquisition hint probe failed; keeping the current hint.", { error });
1439
1599
  }
1440
1600
  return;
1441
1601
  }
@@ -1448,12 +1608,12 @@ var WorkerService = class {
1448
1608
  workersInstance.hasCompletedInitialJobAcquisitionHintProbe = true;
1449
1609
  workersInstance.jobAcquisitionHint = winner;
1450
1610
  this.resetPendingJobAcquisitionHint(workersInstance);
1451
- logger4.info(`Selected job acquisition hint "${winner}" after startup probe.`);
1611
+ logger5.info(`Selected job acquisition hint "${winner}" after startup probe.`);
1452
1612
  return;
1453
1613
  }
1454
1614
  if (winner === currentHint) {
1455
1615
  this.resetPendingJobAcquisitionHint(workersInstance);
1456
- logger4.debug(`Keeping job acquisition hint "${currentHint}" after probe.`);
1616
+ logger5.debug(`Keeping job acquisition hint "${currentHint}" after probe.`);
1457
1617
  return;
1458
1618
  }
1459
1619
  if (workersInstance.pendingJobAcquisitionHint === winner) {
@@ -1463,14 +1623,14 @@ var WorkerService = class {
1463
1623
  workersInstance.pendingJobAcquisitionHintWins = 1;
1464
1624
  }
1465
1625
  if (workersInstance.pendingJobAcquisitionHintWins < 2) {
1466
- logger4.debug(
1626
+ logger5.debug(
1467
1627
  `Keeping job acquisition hint "${currentHint}"; "${winner}" has one consecutive probe win.`
1468
1628
  );
1469
1629
  return;
1470
1630
  }
1471
1631
  workersInstance.jobAcquisitionHint = winner;
1472
1632
  this.resetPendingJobAcquisitionHint(workersInstance);
1473
- logger4.info(`Changed job acquisition hint from "${currentHint}" to "${winner}".`);
1633
+ logger5.info(`Changed job acquisition hint from "${currentHint}" to "${winner}".`);
1474
1634
  }
1475
1635
  async waitForNextJobAcquisitionHintProbe(workersInstance) {
1476
1636
  await new Promise((resolve) => {
@@ -1507,7 +1667,7 @@ var WorkerService = class {
1507
1667
  batchSize: this.partitionReconcilerBatchSize
1508
1668
  });
1509
1669
  } catch (error) {
1510
- logger4.warn("Job partition reconciliation failed; it will be retried.", { error });
1670
+ logger5.warn("Job partition reconciliation failed; it will be retried.", { error });
1511
1671
  return 0;
1512
1672
  }
1513
1673
  }
@@ -1547,6 +1707,7 @@ var WorkerService = class {
1547
1707
  activeExecutions: /* @__PURE__ */ new Map(),
1548
1708
  staleExecutions: /* @__PURE__ */ new Map(),
1549
1709
  runningJobsByName: /* @__PURE__ */ new Map(),
1710
+ pendingFilterChecks: /* @__PURE__ */ new Map(),
1550
1711
  jobAcquisitionHint: INITIAL_JOB_ACQUISITION_HINT,
1551
1712
  jobAcquisitionHintProbeCycle: 0,
1552
1713
  hasCompletedInitialJobAcquisitionHintProbe: false,
@@ -1565,7 +1726,7 @@ var WorkerService = class {
1565
1726
  if (workersInstance.stopPromise) return workersInstance.stopPromise;
1566
1727
  workersInstance.stopPromise = (async () => {
1567
1728
  var _a, _b;
1568
- logger4.info("Stopping job scheduler...");
1729
+ logger5.info("Stopping job scheduler...");
1569
1730
  workersInstance.running = false;
1570
1731
  workersInstance.signalStop();
1571
1732
  (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
@@ -1590,17 +1751,17 @@ var WorkerService = class {
1590
1751
  const jobs = this.getJobs(config.jobs);
1591
1752
  await Promise.all(
1592
1753
  jobs.filter((job) => job.type === "recurrent").map(async (job) => {
1593
- logger4.debug(`Ensuring records for job "${job.name}"...`);
1754
+ logger5.debug(`Ensuring records for job "${job.name}"...`);
1594
1755
  await this.jobsRepo.ensureJobRecord(job, getRandomPartition(config.nPartitions));
1595
1756
  })
1596
1757
  );
1597
1758
  }
1598
1759
  async runScheduler(config, workersInstance) {
1599
- logger4.debug("Will ensure maxTriesReached retention and TTL index");
1600
- void this.jobsRepo.ensureMaxTriesReachedRetention(config.maxTriesReachedRetentionMs).catch((error) => logger4.error("Error ensuring maxTriesReached retention", { error }));
1601
- logger4.debug("Will ensure records for recurrent jobs");
1760
+ logger5.debug("Will ensure maxTriesReached retention and TTL index");
1761
+ void this.jobsRepo.ensureMaxTriesReachedRetention(config.maxTriesReachedRetentionMs).catch((error) => logger5.error("Error ensuring maxTriesReached retention", { error }));
1762
+ logger5.debug("Will ensure records for recurrent jobs");
1602
1763
  await this.ensureRecords(config);
1603
- logger4.info(`Starting job scheduler with concurrency ${config.workersCount}`);
1764
+ logger5.info(`Starting job scheduler with concurrency ${config.workersCount}`);
1604
1765
  await this.runSchedulerLoop(config, workersInstance);
1605
1766
  }
1606
1767
  /**
@@ -1626,7 +1787,7 @@ var WorkerService = class {
1626
1787
  setNameToJobs(config.jobs, config.nPartitions);
1627
1788
  (_b = (_a = this.jobsRepo).configureJobPartitions) == null ? void 0 : _b.call(_a, this.getJobNames(config.jobs), config.nPartitions);
1628
1789
  const workersInstance = this.createWorkersInstanceDefinition(config);
1629
- logger4.debug("Starting job scheduler", config);
1790
+ logger5.debug("Starting job scheduler", config);
1630
1791
  workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
1631
1792
  config,
1632
1793
  workersInstance
@@ -1643,7 +1804,7 @@ var WorkerService = class {
1643
1804
  workersInstance.signalStop();
1644
1805
  (_a2 = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a2.call(workersInstance);
1645
1806
  (_b2 = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b2.call(workersInstance);
1646
- logger4.error("Job scheduler stopped after an unexpected error.", { error });
1807
+ logger5.error("Job scheduler stopped after an unexpected error.", { error });
1647
1808
  });
1648
1809
  return workersInstance;
1649
1810
  }