@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Orionjs Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs CHANGED
@@ -524,6 +524,17 @@ var JobsRepo = class {
524
524
  );
525
525
  return result.matchedCount > 0;
526
526
  }
527
+ async rescheduleAfterExecutionGuardAcquisitionFailure(jobId, lockId, nextRunAt) {
528
+ const result = await this.jobs.updateOne(
529
+ { _id: jobId, lockId, tries: { $gte: 1 } },
530
+ {
531
+ $set: { nextRunAt },
532
+ $inc: { tries: -1 },
533
+ $unset: { lockId: "", lockedUntil: "", claimWasStale: "" }
534
+ }
535
+ );
536
+ return result.matchedCount > 0;
537
+ }
527
538
  async deleteEventJob(jobId, lockId) {
528
539
  const result = await this.jobs.deleteOne({
529
540
  _id: jobId,
@@ -872,7 +883,7 @@ function scheduleJobsInternal(jobs, nPartitions) {
872
883
 
873
884
  // src/services/WorkerService.ts
874
885
  var import_helpers2 = require("@orion-js/helpers");
875
- var import_logger4 = require("@orion-js/logger");
886
+ var import_logger5 = require("@orion-js/logger");
876
887
  var import_services4 = require("@orion-js/services");
877
888
 
878
889
  // src/services/Executor.ts
@@ -1124,6 +1135,42 @@ var Executor = class {
1124
1135
  }
1125
1136
  return false;
1126
1137
  }
1138
+ async handleExecutionGuardAcquisitionFailure(error, config, jobToRun, context) {
1139
+ context.logger.error(`Error acquiring execution guard for job "${jobToRun.name}"`, { error });
1140
+ const nextRunAt = new Date(Date.now() + (config.executionGuardRetryDelayMs ?? 1e3));
1141
+ try {
1142
+ const didReschedule = await this.jobsRepo.rescheduleAfterExecutionGuardAcquisitionFailure(
1143
+ jobToRun.jobId,
1144
+ jobToRun.lockId,
1145
+ nextRunAt
1146
+ );
1147
+ if (!didReschedule) {
1148
+ context.logger.warn(
1149
+ `Could not requeue job "${jobToRun.name}" after execution guard acquisition failed because its lock is no longer owned`
1150
+ );
1151
+ }
1152
+ } catch (rescheduleError) {
1153
+ context.logger.error(
1154
+ `Error requeueing job "${jobToRun.name}" after execution guard acquisition failed`,
1155
+ { error: rescheduleError }
1156
+ );
1157
+ }
1158
+ }
1159
+ async releaseExecutionGuardLease(lease, jobToRun, context) {
1160
+ try {
1161
+ await lease.release();
1162
+ } catch (error) {
1163
+ context.logger.error(`Error releasing execution guard for job "${jobToRun.name}"`, { error });
1164
+ }
1165
+ }
1166
+ async runGuardedResolver(job, jobToRun, context, lease) {
1167
+ try {
1168
+ return await job.resolve(jobToRun.params, context);
1169
+ } finally {
1170
+ context.clearStaleTimeout();
1171
+ await this.releaseExecutionGuardLease(lease, jobToRun, context);
1172
+ }
1173
+ }
1127
1174
  async executeJob(config, jobToRun, onExecutionStale) {
1128
1175
  const job = this.getJobDefinition(jobToRun, config.jobs);
1129
1176
  if (!job) return;
@@ -1205,13 +1252,36 @@ var Executor = class {
1205
1252
  params: jobToRun.params
1206
1253
  };
1207
1254
  await (0, import_logger3.runWithOrionAsyncContext)(extraContext, async () => {
1208
- try {
1255
+ let executionGuardLease;
1256
+ if (config.executionGuard) {
1209
1257
  (0, import_logger3.updateOrionAsyncContext)({
1210
1258
  jobName: jobToRun.name,
1211
1259
  jobId: jobToRun.jobId,
1212
1260
  params: jobToRun.params
1213
1261
  });
1214
- const result = await job.resolve(jobToRun.params, context);
1262
+ try {
1263
+ executionGuardLease = await config.executionGuard.acquire(jobToRun);
1264
+ } catch (error) {
1265
+ context.clearStaleTimeout();
1266
+ await this.handleExecutionGuardAcquisitionFailure(error, config, jobToRun, context);
1267
+ if (executionStatus === "stale") await staleHandlingPromise;
1268
+ return;
1269
+ }
1270
+ if (executionStatus === "stale") {
1271
+ await staleHandlingPromise;
1272
+ await this.releaseExecutionGuardLease(executionGuardLease, jobToRun, context);
1273
+ return;
1274
+ }
1275
+ }
1276
+ try {
1277
+ if (!config.executionGuard) {
1278
+ (0, import_logger3.updateOrionAsyncContext)({
1279
+ jobName: jobToRun.name,
1280
+ jobId: jobToRun.jobId,
1281
+ params: jobToRun.params
1282
+ });
1283
+ }
1284
+ const result = executionGuardLease ? await this.runGuardedResolver(job, jobToRun, context, executionGuardLease) : await job.resolve(jobToRun.params, context);
1215
1285
  context.clearStaleTimeout();
1216
1286
  if (executionStatus === "stale") {
1217
1287
  await staleHandlingPromise;
@@ -1280,6 +1350,76 @@ __decorateElement(_init4, 5, "jobsHistoryRepo", _jobsHistoryRepo_dec, Executor);
1280
1350
  Executor = __decorateElement(_init4, 0, "Executor", _Executor_decorators, Executor);
1281
1351
  __runInitializers(_init4, 1, Executor);
1282
1352
 
1353
+ // src/services/jobFilters.ts
1354
+ var import_logger4 = require("@orion-js/logger");
1355
+ var DEFAULT_JOBS_FILTER_TIMEOUT_MS = 5e3;
1356
+ var warnedUnknownJobNames = /* @__PURE__ */ new Set();
1357
+ async function withTimeout(promise, timeoutMs, name) {
1358
+ let timer;
1359
+ const timeout = new Promise((_, reject) => {
1360
+ timer = setTimeout(() => reject(new Error(`${name} timed out after ${timeoutMs}ms`)), timeoutMs);
1361
+ });
1362
+ try {
1363
+ return await Promise.race([promise, timeout]);
1364
+ } finally {
1365
+ clearTimeout(timer);
1366
+ }
1367
+ }
1368
+ async function runCheck(options, name, run) {
1369
+ if (options.pendingChecks.has(name)) {
1370
+ throw new Error(`${name} is still running from a previous claim`);
1371
+ }
1372
+ const promise = Promise.resolve().then(run);
1373
+ options.pendingChecks.set(name, promise);
1374
+ void promise.then(
1375
+ () => options.pendingChecks.delete(name),
1376
+ () => options.pendingChecks.delete(name)
1377
+ );
1378
+ return await withTimeout(promise, options.timeoutMs, name);
1379
+ }
1380
+ function warnUnknownJobNames(jobs, filter) {
1381
+ for (const jobName of [...filter.allow ?? [], ...filter.deny ?? []]) {
1382
+ if (jobs[jobName] || warnedUnknownJobNames.has(jobName)) continue;
1383
+ warnedUnknownJobNames.add(jobName);
1384
+ import_logger4.logger.warn(`getJobsFilter returned unknown job name "${jobName}"; it will be ignored.`);
1385
+ }
1386
+ }
1387
+ async function filterByJobsFilter(options, getJobsFilter) {
1388
+ try {
1389
+ const filter = await runCheck(options, "getJobsFilter", getJobsFilter) ?? {};
1390
+ warnUnknownJobNames(options.jobs, filter);
1391
+ return options.jobNames.filter((jobName) => {
1392
+ var _a;
1393
+ if (filter.allow && !filter.allow.includes(jobName)) return false;
1394
+ if ((_a = filter.deny) == null ? void 0 : _a.includes(jobName)) return false;
1395
+ return true;
1396
+ });
1397
+ } catch (error) {
1398
+ import_logger4.logger.error("Error in getJobsFilter; no jobs will be taken on this claim.", { error });
1399
+ return [];
1400
+ }
1401
+ }
1402
+ async function canTakeJob(options, jobName) {
1403
+ const canTakeJobs = options.jobs[jobName].canTakeJobs;
1404
+ if (!canTakeJobs) return true;
1405
+ try {
1406
+ return await runCheck(options, `canTakeJobs of "${jobName}"`, canTakeJobs) === true;
1407
+ } catch (error) {
1408
+ import_logger4.logger.error(`Error in canTakeJobs of job "${jobName}"; it will not be taken.`, { error });
1409
+ return false;
1410
+ }
1411
+ }
1412
+ async function getTakeableJobNames(inputOptions) {
1413
+ const options = {
1414
+ timeoutMs: DEFAULT_JOBS_FILTER_TIMEOUT_MS,
1415
+ pendingChecks: /* @__PURE__ */ new Map(),
1416
+ ...inputOptions
1417
+ };
1418
+ const jobNames = inputOptions.getJobsFilter ? await filterByJobsFilter(options, inputOptions.getJobsFilter) : options.jobNames;
1419
+ const checks = await Promise.all(jobNames.map((jobName) => canTakeJob(options, jobName)));
1420
+ return jobNames.filter((_, index) => checks[index]);
1421
+ }
1422
+
1283
1423
  // src/services/WorkerService.ts
1284
1424
  var FILTERED_JOB_NAMES_ACQUISITION_HINT = "byJobName";
1285
1425
  var _executor_dec, _jobsRepo_dec3, _WorkerService_decorators, _init5;
@@ -1291,6 +1431,7 @@ var WorkerService = class {
1291
1431
  __publicField(this, "partitionReconcilerJitterMs", 30 * 1e3);
1292
1432
  __publicField(this, "partitionReconcilerBatchPauseMs", 250);
1293
1433
  __publicField(this, "partitionReconcilerBatchSize", 500);
1434
+ __publicField(this, "jobsFilterTimeoutMs", DEFAULT_JOBS_FILTER_TIMEOUT_MS);
1294
1435
  __publicField(this, "jobsRepo", __runInitializers(_init5, 8, this)), __runInitializers(_init5, 11, this);
1295
1436
  __publicField(this, "executor", __runInitializers(_init5, 12, this)), __runInitializers(_init5, 15, this);
1296
1437
  }
@@ -1349,13 +1490,13 @@ var WorkerService = class {
1349
1490
  };
1350
1491
  this.reserveJobExecution(workersInstance, jobToRun.name);
1351
1492
  workersInstance.activeExecutions.set(jobToRun.executionId, execution);
1352
- import_logger4.logger.debug(`Starting job execution "${jobToRun.executionId}"`, jobToRun);
1493
+ import_logger5.logger.debug(`Starting job execution "${jobToRun.executionId}"`, jobToRun);
1353
1494
  const executionPromise = this.executor.executeJob(config, jobToRun, () => {
1354
1495
  this.releaseExecutionCapacity(workersInstance, execution, "stale");
1355
1496
  });
1356
1497
  execution.promise = executionPromise;
1357
1498
  void executionPromise.catch((error) => {
1358
- import_logger4.logger.error(`Unhandled error executing job "${jobToRun.name}"`, { error });
1499
+ import_logger5.logger.error(`Unhandled error executing job "${jobToRun.name}"`, { error });
1359
1500
  }).finally(() => {
1360
1501
  if (execution.status === "running") {
1361
1502
  this.releaseExecutionCapacity(workersInstance, execution, "finished");
@@ -1390,20 +1531,39 @@ var WorkerService = class {
1390
1531
  jobs: config.jobs,
1391
1532
  maxTries: config.maxTries,
1392
1533
  maxTriesReachedRetentionMs: config.maxTriesReachedRetentionMs,
1393
- onMaxTriesReached: config.onMaxTriesReached
1534
+ onMaxTriesReached: config.onMaxTriesReached,
1535
+ ...config.executionGuard ? {
1536
+ executionGuard: config.executionGuard,
1537
+ executionGuardRetryDelayMs: Math.max(config.pollInterval, 1e3)
1538
+ } : {}
1394
1539
  };
1395
- import_logger4.logger.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
1540
+ import_logger5.logger.debug(`Running scheduler loop for jobs "${jobNames.join(", ")}"...`);
1396
1541
  let consecutivePartitionMisses = 0;
1397
1542
  while (workersInstance.running) {
1398
1543
  let didMiss = false;
1399
1544
  let allJobNamesAtCapacity = false;
1400
1545
  try {
1401
1546
  while (workersInstance.activeExecutions.size < config.workersCount) {
1402
- const availableJobNames = this.getAvailableJobNames(config, workersInstance, jobNames);
1403
- if (availableJobNames.length === 0) {
1547
+ const jobNamesWithCapacity = this.getAvailableJobNames(config, workersInstance, jobNames);
1548
+ if (jobNamesWithCapacity.length === 0) {
1404
1549
  allJobNamesAtCapacity = workersInstance.activeExecutions.size > 0;
1405
1550
  break;
1406
1551
  }
1552
+ const availableJobNames = await Promise.race([
1553
+ getTakeableJobNames({
1554
+ jobs: config.jobs,
1555
+ getJobsFilter: config.getJobsFilter,
1556
+ jobNames: jobNamesWithCapacity,
1557
+ timeoutMs: this.jobsFilterTimeoutMs,
1558
+ pendingChecks: workersInstance.pendingFilterChecks
1559
+ }),
1560
+ workersInstance.stopSignal.then(() => [])
1561
+ ]);
1562
+ if (!workersInstance.running) break;
1563
+ if (availableJobNames.length === 0) {
1564
+ didMiss = true;
1565
+ break;
1566
+ }
1407
1567
  const jobAcquisitionHint = availableJobNames.length < jobNames.length ? FILTERED_JOB_NAMES_ACQUISITION_HINT : workersInstance.jobAcquisitionHint;
1408
1568
  const partition = this.getNextPartition(config, workersInstance);
1409
1569
  const jobToRun = await this.jobsRepo.getJobAndLock(
@@ -1414,7 +1574,7 @@ var WorkerService = class {
1414
1574
  );
1415
1575
  if (!jobToRun) {
1416
1576
  consecutivePartitionMisses++;
1417
- import_logger4.logger.debug(`No job to run in partition ${partition}`);
1577
+ import_logger5.logger.debug(`No job to run in partition ${partition}`);
1418
1578
  if (consecutivePartitionMisses >= config.nPartitions) {
1419
1579
  consecutivePartitionMisses = 0;
1420
1580
  didMiss = true;
@@ -1427,7 +1587,7 @@ var WorkerService = class {
1427
1587
  if (!workersInstance.running) break;
1428
1588
  }
1429
1589
  } catch (error) {
1430
- import_logger4.logger.error("Error in job scheduler.", { error });
1590
+ import_logger5.logger.error("Error in job scheduler.", { error });
1431
1591
  consecutivePartitionMisses = 0;
1432
1592
  didMiss = true;
1433
1593
  }
@@ -1479,7 +1639,7 @@ var WorkerService = class {
1479
1639
  } catch (error) {
1480
1640
  this.resetPendingJobAcquisitionHint(workersInstance);
1481
1641
  if (workersInstance.running) {
1482
- import_logger4.logger.warn("Job acquisition hint probe failed; keeping the current hint.", { error });
1642
+ import_logger5.logger.warn("Job acquisition hint probe failed; keeping the current hint.", { error });
1483
1643
  }
1484
1644
  return;
1485
1645
  }
@@ -1492,12 +1652,12 @@ var WorkerService = class {
1492
1652
  workersInstance.hasCompletedInitialJobAcquisitionHintProbe = true;
1493
1653
  workersInstance.jobAcquisitionHint = winner;
1494
1654
  this.resetPendingJobAcquisitionHint(workersInstance);
1495
- import_logger4.logger.info(`Selected job acquisition hint "${winner}" after startup probe.`);
1655
+ import_logger5.logger.info(`Selected job acquisition hint "${winner}" after startup probe.`);
1496
1656
  return;
1497
1657
  }
1498
1658
  if (winner === currentHint) {
1499
1659
  this.resetPendingJobAcquisitionHint(workersInstance);
1500
- import_logger4.logger.debug(`Keeping job acquisition hint "${currentHint}" after probe.`);
1660
+ import_logger5.logger.debug(`Keeping job acquisition hint "${currentHint}" after probe.`);
1501
1661
  return;
1502
1662
  }
1503
1663
  if (workersInstance.pendingJobAcquisitionHint === winner) {
@@ -1507,14 +1667,14 @@ var WorkerService = class {
1507
1667
  workersInstance.pendingJobAcquisitionHintWins = 1;
1508
1668
  }
1509
1669
  if (workersInstance.pendingJobAcquisitionHintWins < 2) {
1510
- import_logger4.logger.debug(
1670
+ import_logger5.logger.debug(
1511
1671
  `Keeping job acquisition hint "${currentHint}"; "${winner}" has one consecutive probe win.`
1512
1672
  );
1513
1673
  return;
1514
1674
  }
1515
1675
  workersInstance.jobAcquisitionHint = winner;
1516
1676
  this.resetPendingJobAcquisitionHint(workersInstance);
1517
- import_logger4.logger.info(`Changed job acquisition hint from "${currentHint}" to "${winner}".`);
1677
+ import_logger5.logger.info(`Changed job acquisition hint from "${currentHint}" to "${winner}".`);
1518
1678
  }
1519
1679
  async waitForNextJobAcquisitionHintProbe(workersInstance) {
1520
1680
  await new Promise((resolve) => {
@@ -1551,7 +1711,7 @@ var WorkerService = class {
1551
1711
  batchSize: this.partitionReconcilerBatchSize
1552
1712
  });
1553
1713
  } catch (error) {
1554
- import_logger4.logger.warn("Job partition reconciliation failed; it will be retried.", { error });
1714
+ import_logger5.logger.warn("Job partition reconciliation failed; it will be retried.", { error });
1555
1715
  return 0;
1556
1716
  }
1557
1717
  }
@@ -1591,6 +1751,7 @@ var WorkerService = class {
1591
1751
  activeExecutions: /* @__PURE__ */ new Map(),
1592
1752
  staleExecutions: /* @__PURE__ */ new Map(),
1593
1753
  runningJobsByName: /* @__PURE__ */ new Map(),
1754
+ pendingFilterChecks: /* @__PURE__ */ new Map(),
1594
1755
  jobAcquisitionHint: INITIAL_JOB_ACQUISITION_HINT,
1595
1756
  jobAcquisitionHintProbeCycle: 0,
1596
1757
  hasCompletedInitialJobAcquisitionHintProbe: false,
@@ -1609,7 +1770,7 @@ var WorkerService = class {
1609
1770
  if (workersInstance.stopPromise) return workersInstance.stopPromise;
1610
1771
  workersInstance.stopPromise = (async () => {
1611
1772
  var _a, _b;
1612
- import_logger4.logger.info("Stopping job scheduler...");
1773
+ import_logger5.logger.info("Stopping job scheduler...");
1613
1774
  workersInstance.running = false;
1614
1775
  workersInstance.signalStop();
1615
1776
  (_a = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a.call(workersInstance);
@@ -1634,17 +1795,17 @@ var WorkerService = class {
1634
1795
  const jobs = this.getJobs(config.jobs);
1635
1796
  await Promise.all(
1636
1797
  jobs.filter((job) => job.type === "recurrent").map(async (job) => {
1637
- import_logger4.logger.debug(`Ensuring records for job "${job.name}"...`);
1798
+ import_logger5.logger.debug(`Ensuring records for job "${job.name}"...`);
1638
1799
  await this.jobsRepo.ensureJobRecord(job, getRandomPartition(config.nPartitions));
1639
1800
  })
1640
1801
  );
1641
1802
  }
1642
1803
  async runScheduler(config, workersInstance) {
1643
- import_logger4.logger.debug("Will ensure maxTriesReached retention and TTL index");
1644
- void this.jobsRepo.ensureMaxTriesReachedRetention(config.maxTriesReachedRetentionMs).catch((error) => import_logger4.logger.error("Error ensuring maxTriesReached retention", { error }));
1645
- import_logger4.logger.debug("Will ensure records for recurrent jobs");
1804
+ import_logger5.logger.debug("Will ensure maxTriesReached retention and TTL index");
1805
+ void this.jobsRepo.ensureMaxTriesReachedRetention(config.maxTriesReachedRetentionMs).catch((error) => import_logger5.logger.error("Error ensuring maxTriesReached retention", { error }));
1806
+ import_logger5.logger.debug("Will ensure records for recurrent jobs");
1646
1807
  await this.ensureRecords(config);
1647
- import_logger4.logger.info(`Starting job scheduler with concurrency ${config.workersCount}`);
1808
+ import_logger5.logger.info(`Starting job scheduler with concurrency ${config.workersCount}`);
1648
1809
  await this.runSchedulerLoop(config, workersInstance);
1649
1810
  }
1650
1811
  /**
@@ -1670,7 +1831,7 @@ var WorkerService = class {
1670
1831
  setNameToJobs(config.jobs, config.nPartitions);
1671
1832
  (_b = (_a = this.jobsRepo).configureJobPartitions) == null ? void 0 : _b.call(_a, this.getJobNames(config.jobs), config.nPartitions);
1672
1833
  const workersInstance = this.createWorkersInstanceDefinition(config);
1673
- import_logger4.logger.debug("Starting job scheduler", config);
1834
+ import_logger5.logger.debug("Starting job scheduler", config);
1674
1835
  workersInstance.jobAcquisitionHintProbePromise = this.runJobAcquisitionHintProbeLoop(
1675
1836
  config,
1676
1837
  workersInstance
@@ -1687,7 +1848,7 @@ var WorkerService = class {
1687
1848
  workersInstance.signalStop();
1688
1849
  (_a2 = workersInstance.cancelJobAcquisitionHintProbeWait) == null ? void 0 : _a2.call(workersInstance);
1689
1850
  (_b2 = workersInstance.cancelPartitionReconcilerWait) == null ? void 0 : _b2.call(workersInstance);
1690
- import_logger4.logger.error("Job scheduler stopped after an unexpected error.", { error });
1851
+ import_logger5.logger.error("Job scheduler stopped after an unexpected error.", { error });
1691
1852
  });
1692
1853
  return workersInstance;
1693
1854
  }