@monque/core 1.4.0 → 1.5.0
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/CHANGELOG.md +13 -0
- package/dist/index.cjs +369 -195
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -22
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +99 -22
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +370 -197
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/events/types.ts +2 -2
- package/src/index.ts +1 -0
- package/src/jobs/document-to-persisted-job.ts +16 -16
- package/src/scheduler/monque.ts +98 -95
- package/src/scheduler/services/index.ts +1 -0
- package/src/scheduler/services/job-manager.ts +100 -116
- package/src/scheduler/services/job-query.ts +81 -36
- package/src/scheduler/services/job-scheduler.ts +42 -2
- package/src/scheduler/services/lifecycle-manager.ts +154 -0
- package/src/scheduler/services/types.ts +5 -1
- package/src/scheduler/types.ts +23 -0
- package/src/shared/errors.ts +31 -0
- package/src/shared/index.ts +1 -0
package/dist/index.cjs
CHANGED
|
@@ -480,6 +480,32 @@ var AggregationTimeoutError = class AggregationTimeoutError extends MonqueError
|
|
|
480
480
|
if (Error.captureStackTrace) Error.captureStackTrace(this, AggregationTimeoutError);
|
|
481
481
|
}
|
|
482
482
|
};
|
|
483
|
+
/**
|
|
484
|
+
* Error thrown when a job payload exceeds the configured maximum BSON byte size.
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```typescript
|
|
488
|
+
* const monque = new Monque(db, { maxPayloadSize: 1_000_000 }); // 1 MB
|
|
489
|
+
*
|
|
490
|
+
* try {
|
|
491
|
+
* await monque.enqueue('job', hugePayload);
|
|
492
|
+
* } catch (error) {
|
|
493
|
+
* if (error instanceof PayloadTooLargeError) {
|
|
494
|
+
* console.error(`Payload ${error.actualSize} bytes exceeds limit ${error.maxSize} bytes`);
|
|
495
|
+
* }
|
|
496
|
+
* }
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
var PayloadTooLargeError = class PayloadTooLargeError extends MonqueError {
|
|
500
|
+
constructor(message, actualSize, maxSize) {
|
|
501
|
+
super(message);
|
|
502
|
+
this.actualSize = actualSize;
|
|
503
|
+
this.maxSize = maxSize;
|
|
504
|
+
this.name = "PayloadTooLargeError";
|
|
505
|
+
/* istanbul ignore next -- @preserve captureStackTrace is always available in Node.js */
|
|
506
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, PayloadTooLargeError);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
483
509
|
|
|
484
510
|
//#endregion
|
|
485
511
|
//#region src/shared/utils/backoff.ts
|
|
@@ -1011,14 +1037,15 @@ var JobManager = class {
|
|
|
1011
1037
|
return false;
|
|
1012
1038
|
}
|
|
1013
1039
|
/**
|
|
1014
|
-
* Cancel multiple jobs matching the given filter.
|
|
1040
|
+
* Cancel multiple jobs matching the given filter via a single updateMany call.
|
|
1015
1041
|
*
|
|
1016
|
-
* Only cancels jobs in 'pending' status
|
|
1017
|
-
*
|
|
1042
|
+
* Only cancels jobs in 'pending' status — the status guard is applied regardless
|
|
1043
|
+
* of what the filter specifies. Jobs in other states are silently skipped (not
|
|
1044
|
+
* matched by the query). Emits a 'jobs:cancelled' event with the count of
|
|
1018
1045
|
* successfully cancelled jobs.
|
|
1019
1046
|
*
|
|
1020
1047
|
* @param filter - Selector for which jobs to cancel (name, status, date range)
|
|
1021
|
-
* @returns Result with count of cancelled jobs
|
|
1048
|
+
* @returns Result with count of cancelled jobs (errors array always empty for bulk ops)
|
|
1022
1049
|
*
|
|
1023
1050
|
* @example Cancel all pending jobs for a queue
|
|
1024
1051
|
* ```typescript
|
|
@@ -1030,53 +1057,39 @@ var JobManager = class {
|
|
|
1030
1057
|
* ```
|
|
1031
1058
|
*/
|
|
1032
1059
|
async cancelJobs(filter) {
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
});
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
if (doc["status"] === JobStatus.CANCELLED) {
|
|
1047
|
-
cancelledIds.push(jobId);
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
if (await this.ctx.collection.findOneAndUpdate({
|
|
1051
|
-
_id: doc._id,
|
|
1052
|
-
status: JobStatus.PENDING
|
|
1053
|
-
}, { $set: {
|
|
1060
|
+
const query = buildSelectorQuery(filter);
|
|
1061
|
+
if (filter.status !== void 0) {
|
|
1062
|
+
if (!(Array.isArray(filter.status) ? filter.status : [filter.status]).includes(JobStatus.PENDING)) return {
|
|
1063
|
+
count: 0,
|
|
1064
|
+
errors: []
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
query["status"] = JobStatus.PENDING;
|
|
1068
|
+
try {
|
|
1069
|
+
const count = (await this.ctx.collection.updateMany(query, { $set: {
|
|
1054
1070
|
status: JobStatus.CANCELLED,
|
|
1055
1071
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1056
|
-
} }
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1072
|
+
} })).modifiedCount;
|
|
1073
|
+
if (count > 0) this.ctx.emit("jobs:cancelled", { count });
|
|
1074
|
+
return {
|
|
1075
|
+
count,
|
|
1076
|
+
errors: []
|
|
1077
|
+
};
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
if (error instanceof MonqueError) throw error;
|
|
1080
|
+
throw new ConnectionError(`Failed to cancel jobs: ${error instanceof Error ? error.message : "Unknown error during cancelJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1061
1081
|
}
|
|
1062
|
-
if (cancelledIds.length > 0) this.ctx.emit("jobs:cancelled", {
|
|
1063
|
-
jobIds: cancelledIds,
|
|
1064
|
-
count: cancelledIds.length
|
|
1065
|
-
});
|
|
1066
|
-
return {
|
|
1067
|
-
count: cancelledIds.length,
|
|
1068
|
-
errors
|
|
1069
|
-
};
|
|
1070
1082
|
}
|
|
1071
1083
|
/**
|
|
1072
|
-
* Retry multiple jobs matching the given filter.
|
|
1084
|
+
* Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.
|
|
1073
1085
|
*
|
|
1074
|
-
* Only retries jobs in 'failed' or 'cancelled' status
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1086
|
+
* Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied
|
|
1087
|
+
* regardless of what the filter specifies. Jobs in other states are silently skipped.
|
|
1088
|
+
* Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.
|
|
1089
|
+
* Emits a 'jobs:retried' event with the count of successfully retried jobs.
|
|
1077
1090
|
*
|
|
1078
1091
|
* @param filter - Selector for which jobs to retry (name, status, date range)
|
|
1079
|
-
* @returns Result with count of retried jobs
|
|
1092
|
+
* @returns Result with count of retried jobs (errors array always empty for bulk ops)
|
|
1080
1093
|
*
|
|
1081
1094
|
* @example Retry all failed jobs
|
|
1082
1095
|
* ```typescript
|
|
@@ -1087,50 +1100,39 @@ var JobManager = class {
|
|
|
1087
1100
|
* ```
|
|
1088
1101
|
*/
|
|
1089
1102
|
async retryJobs(filter) {
|
|
1090
|
-
const
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
jobId,
|
|
1123
|
-
error: "Job status changed during retry attempt"
|
|
1124
|
-
});
|
|
1103
|
+
const query = buildSelectorQuery(filter);
|
|
1104
|
+
const retryable = [JobStatus.FAILED, JobStatus.CANCELLED];
|
|
1105
|
+
if (filter.status !== void 0) {
|
|
1106
|
+
const allowed = (Array.isArray(filter.status) ? filter.status : [filter.status]).filter((status) => status === JobStatus.FAILED || status === JobStatus.CANCELLED);
|
|
1107
|
+
if (allowed.length === 0) return {
|
|
1108
|
+
count: 0,
|
|
1109
|
+
errors: []
|
|
1110
|
+
};
|
|
1111
|
+
query["status"] = allowed.length === 1 ? allowed[0] : { $in: allowed };
|
|
1112
|
+
} else query["status"] = { $in: retryable };
|
|
1113
|
+
const spreadWindowMs = 3e4;
|
|
1114
|
+
try {
|
|
1115
|
+
const count = (await this.ctx.collection.updateMany(query, [{ $set: {
|
|
1116
|
+
status: JobStatus.PENDING,
|
|
1117
|
+
failCount: 0,
|
|
1118
|
+
nextRunAt: { $add: [/* @__PURE__ */ new Date(), { $multiply: [{ $rand: {} }, spreadWindowMs] }] },
|
|
1119
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1120
|
+
} }, { $unset: [
|
|
1121
|
+
"failReason",
|
|
1122
|
+
"lockedAt",
|
|
1123
|
+
"claimedBy",
|
|
1124
|
+
"lastHeartbeat",
|
|
1125
|
+
"heartbeatInterval"
|
|
1126
|
+
] }])).modifiedCount;
|
|
1127
|
+
if (count > 0) this.ctx.emit("jobs:retried", { count });
|
|
1128
|
+
return {
|
|
1129
|
+
count,
|
|
1130
|
+
errors: []
|
|
1131
|
+
};
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
if (error instanceof MonqueError) throw error;
|
|
1134
|
+
throw new ConnectionError(`Failed to retry jobs: ${error instanceof Error ? error.message : "Unknown error during retryJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1125
1135
|
}
|
|
1126
|
-
if (retriedIds.length > 0) this.ctx.emit("jobs:retried", {
|
|
1127
|
-
jobIds: retriedIds,
|
|
1128
|
-
count: retriedIds.length
|
|
1129
|
-
});
|
|
1130
|
-
return {
|
|
1131
|
-
count: retriedIds.length,
|
|
1132
|
-
errors
|
|
1133
|
-
};
|
|
1134
1136
|
}
|
|
1135
1137
|
/**
|
|
1136
1138
|
* Delete multiple jobs matching the given filter.
|
|
@@ -1154,12 +1156,17 @@ var JobManager = class {
|
|
|
1154
1156
|
*/
|
|
1155
1157
|
async deleteJobs(filter) {
|
|
1156
1158
|
const query = buildSelectorQuery(filter);
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1159
|
+
try {
|
|
1160
|
+
const result = await this.ctx.collection.deleteMany(query);
|
|
1161
|
+
if (result.deletedCount > 0) this.ctx.emit("jobs:deleted", { count: result.deletedCount });
|
|
1162
|
+
return {
|
|
1163
|
+
count: result.deletedCount,
|
|
1164
|
+
errors: []
|
|
1165
|
+
};
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
if (error instanceof MonqueError) throw error;
|
|
1168
|
+
throw new ConnectionError(`Failed to delete jobs: ${error instanceof Error ? error.message : "Unknown error during deleteJobs"}`, error instanceof Error ? { cause: error } : void 0);
|
|
1169
|
+
}
|
|
1163
1170
|
}
|
|
1164
1171
|
};
|
|
1165
1172
|
|
|
@@ -1478,7 +1485,9 @@ var JobProcessor = class {
|
|
|
1478
1485
|
*
|
|
1479
1486
|
* @internal Not part of public API - use Monque class methods instead.
|
|
1480
1487
|
*/
|
|
1481
|
-
var JobQueryService = class {
|
|
1488
|
+
var JobQueryService = class JobQueryService {
|
|
1489
|
+
statsCache = /* @__PURE__ */ new Map();
|
|
1490
|
+
static MAX_CACHE_SIZE = 100;
|
|
1482
1491
|
constructor(ctx) {
|
|
1483
1492
|
this.ctx = ctx;
|
|
1484
1493
|
}
|
|
@@ -1657,11 +1666,22 @@ var JobQueryService = class {
|
|
|
1657
1666
|
};
|
|
1658
1667
|
}
|
|
1659
1668
|
/**
|
|
1669
|
+
* Clear all cached getQueueStats() results.
|
|
1670
|
+
* Called on scheduler stop() for clean state on restart.
|
|
1671
|
+
* @internal
|
|
1672
|
+
*/
|
|
1673
|
+
clearStatsCache() {
|
|
1674
|
+
this.statsCache.clear();
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1660
1677
|
* Get aggregate statistics for the job queue.
|
|
1661
1678
|
*
|
|
1662
1679
|
* Uses MongoDB aggregation pipeline for efficient server-side calculation.
|
|
1663
1680
|
* Returns counts per status and optional average processing duration for completed jobs.
|
|
1664
1681
|
*
|
|
1682
|
+
* Results are cached per unique filter with a configurable TTL (default 5s).
|
|
1683
|
+
* Set `statsCacheTtlMs: 0` to disable caching.
|
|
1684
|
+
*
|
|
1665
1685
|
* @param filter - Optional filter to scope statistics by job name
|
|
1666
1686
|
* @returns Promise resolving to queue statistics
|
|
1667
1687
|
* @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout
|
|
@@ -1680,6 +1700,12 @@ var JobQueryService = class {
|
|
|
1680
1700
|
* ```
|
|
1681
1701
|
*/
|
|
1682
1702
|
async getQueueStats(filter) {
|
|
1703
|
+
const ttl = this.ctx.options.statsCacheTtlMs;
|
|
1704
|
+
const cacheKey = filter?.name ?? "";
|
|
1705
|
+
if (ttl > 0) {
|
|
1706
|
+
const cached = this.statsCache.get(cacheKey);
|
|
1707
|
+
if (cached && cached.expiresAt > Date.now()) return { ...cached.data };
|
|
1708
|
+
}
|
|
1683
1709
|
const matchStage = {};
|
|
1684
1710
|
if (filter?.name) matchStage["name"] = filter.name;
|
|
1685
1711
|
const pipeline = [...Object.keys(matchStage).length > 0 ? [{ $match: matchStage }] : [], { $facet: {
|
|
@@ -1703,35 +1729,47 @@ var JobQueryService = class {
|
|
|
1703
1729
|
cancelled: 0,
|
|
1704
1730
|
total: 0
|
|
1705
1731
|
};
|
|
1706
|
-
if (
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1732
|
+
if (result) {
|
|
1733
|
+
const statusCounts = result["statusCounts"];
|
|
1734
|
+
for (const entry of statusCounts) {
|
|
1735
|
+
const status = entry._id;
|
|
1736
|
+
const count = entry.count;
|
|
1737
|
+
switch (status) {
|
|
1738
|
+
case JobStatus.PENDING:
|
|
1739
|
+
stats.pending = count;
|
|
1740
|
+
break;
|
|
1741
|
+
case JobStatus.PROCESSING:
|
|
1742
|
+
stats.processing = count;
|
|
1743
|
+
break;
|
|
1744
|
+
case JobStatus.COMPLETED:
|
|
1745
|
+
stats.completed = count;
|
|
1746
|
+
break;
|
|
1747
|
+
case JobStatus.FAILED:
|
|
1748
|
+
stats.failed = count;
|
|
1749
|
+
break;
|
|
1750
|
+
case JobStatus.CANCELLED:
|
|
1751
|
+
stats.cancelled = count;
|
|
1752
|
+
break;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const totalResult = result["total"];
|
|
1756
|
+
if (totalResult.length > 0 && totalResult[0]) stats.total = totalResult[0].count;
|
|
1757
|
+
const avgDurationResult = result["avgDuration"];
|
|
1758
|
+
if (avgDurationResult.length > 0 && avgDurationResult[0]) {
|
|
1759
|
+
const avgMs = avgDurationResult[0].avgMs;
|
|
1760
|
+
if (typeof avgMs === "number" && !Number.isNaN(avgMs)) stats.avgProcessingDurationMs = Math.round(avgMs);
|
|
1727
1761
|
}
|
|
1728
1762
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1763
|
+
if (ttl > 0) {
|
|
1764
|
+
this.statsCache.delete(cacheKey);
|
|
1765
|
+
if (this.statsCache.size >= JobQueryService.MAX_CACHE_SIZE) {
|
|
1766
|
+
const oldestKey = this.statsCache.keys().next().value;
|
|
1767
|
+
if (oldestKey !== void 0) this.statsCache.delete(oldestKey);
|
|
1768
|
+
}
|
|
1769
|
+
this.statsCache.set(cacheKey, {
|
|
1770
|
+
data: { ...stats },
|
|
1771
|
+
expiresAt: Date.now() + ttl
|
|
1772
|
+
});
|
|
1735
1773
|
}
|
|
1736
1774
|
return stats;
|
|
1737
1775
|
} catch (error) {
|
|
@@ -1756,6 +1794,26 @@ var JobScheduler = class {
|
|
|
1756
1794
|
this.ctx = ctx;
|
|
1757
1795
|
}
|
|
1758
1796
|
/**
|
|
1797
|
+
* Validate that the job data payload does not exceed the configured maximum BSON byte size.
|
|
1798
|
+
*
|
|
1799
|
+
* @param data - The job data payload to validate
|
|
1800
|
+
* @throws {PayloadTooLargeError} If the payload exceeds `maxPayloadSize`
|
|
1801
|
+
*/
|
|
1802
|
+
validatePayloadSize(data) {
|
|
1803
|
+
const maxSize = this.ctx.options.maxPayloadSize;
|
|
1804
|
+
if (maxSize === void 0) return;
|
|
1805
|
+
let size;
|
|
1806
|
+
try {
|
|
1807
|
+
size = mongodb.BSON.calculateObjectSize({ data });
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
1810
|
+
const sizeError = new PayloadTooLargeError(`Failed to calculate job payload size: ${cause.message}`, -1, maxSize);
|
|
1811
|
+
sizeError.cause = cause;
|
|
1812
|
+
throw sizeError;
|
|
1813
|
+
}
|
|
1814
|
+
if (size > maxSize) throw new PayloadTooLargeError(`Job payload exceeds maximum size: ${size} bytes > ${maxSize} bytes`, size, maxSize);
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1759
1817
|
* Enqueue a job for processing.
|
|
1760
1818
|
*
|
|
1761
1819
|
* Jobs are stored in MongoDB and processed by registered workers. Supports
|
|
@@ -1773,6 +1831,7 @@ var JobScheduler = class {
|
|
|
1773
1831
|
* @param options - Scheduling and deduplication options
|
|
1774
1832
|
* @returns Promise resolving to the created or existing job document
|
|
1775
1833
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
1834
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
1776
1835
|
*
|
|
1777
1836
|
* @example Basic job enqueueing
|
|
1778
1837
|
* ```typescript
|
|
@@ -1800,6 +1859,7 @@ var JobScheduler = class {
|
|
|
1800
1859
|
* ```
|
|
1801
1860
|
*/
|
|
1802
1861
|
async enqueue(name, data, options = {}) {
|
|
1862
|
+
this.validatePayloadSize(data);
|
|
1803
1863
|
const now = /* @__PURE__ */ new Date();
|
|
1804
1864
|
const job = {
|
|
1805
1865
|
name,
|
|
@@ -1885,6 +1945,7 @@ var JobScheduler = class {
|
|
|
1885
1945
|
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
1886
1946
|
* @throws {InvalidCronError} If cron expression is invalid
|
|
1887
1947
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
1948
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
1888
1949
|
*
|
|
1889
1950
|
* @example Hourly cleanup job
|
|
1890
1951
|
* ```typescript
|
|
@@ -1910,6 +1971,7 @@ var JobScheduler = class {
|
|
|
1910
1971
|
* ```
|
|
1911
1972
|
*/
|
|
1912
1973
|
async schedule(cron, name, data, options = {}) {
|
|
1974
|
+
this.validatePayloadSize(data);
|
|
1913
1975
|
const nextRunAt = getNextCronDate(cron);
|
|
1914
1976
|
const now = /* @__PURE__ */ new Date();
|
|
1915
1977
|
const job = {
|
|
@@ -1948,6 +2010,114 @@ var JobScheduler = class {
|
|
|
1948
2010
|
}
|
|
1949
2011
|
};
|
|
1950
2012
|
|
|
2013
|
+
//#endregion
|
|
2014
|
+
//#region src/scheduler/services/lifecycle-manager.ts
|
|
2015
|
+
/**
|
|
2016
|
+
* Default retention check interval (1 hour).
|
|
2017
|
+
*/
|
|
2018
|
+
const DEFAULT_RETENTION_INTERVAL = 36e5;
|
|
2019
|
+
/**
|
|
2020
|
+
* Manages scheduler lifecycle timers and job cleanup.
|
|
2021
|
+
*
|
|
2022
|
+
* Owns poll interval, heartbeat interval, cleanup interval, and the
|
|
2023
|
+
* cleanupJobs logic. Extracted from Monque to keep the facade thin.
|
|
2024
|
+
*
|
|
2025
|
+
* @internal Not part of public API.
|
|
2026
|
+
*/
|
|
2027
|
+
var LifecycleManager = class {
|
|
2028
|
+
ctx;
|
|
2029
|
+
pollIntervalId = null;
|
|
2030
|
+
heartbeatIntervalId = null;
|
|
2031
|
+
cleanupIntervalId = null;
|
|
2032
|
+
constructor(ctx) {
|
|
2033
|
+
this.ctx = ctx;
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Start all lifecycle timers.
|
|
2037
|
+
*
|
|
2038
|
+
* Sets up poll interval, heartbeat interval, and (if configured)
|
|
2039
|
+
* cleanup interval. Runs an initial poll immediately.
|
|
2040
|
+
*
|
|
2041
|
+
* @param callbacks - Functions to invoke on each timer tick
|
|
2042
|
+
*/
|
|
2043
|
+
startTimers(callbacks) {
|
|
2044
|
+
this.pollIntervalId = setInterval(() => {
|
|
2045
|
+
callbacks.poll().catch((error) => {
|
|
2046
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2047
|
+
});
|
|
2048
|
+
}, this.ctx.options.pollInterval);
|
|
2049
|
+
this.heartbeatIntervalId = setInterval(() => {
|
|
2050
|
+
callbacks.updateHeartbeats().catch((error) => {
|
|
2051
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2052
|
+
});
|
|
2053
|
+
}, this.ctx.options.heartbeatInterval);
|
|
2054
|
+
if (this.ctx.options.jobRetention) {
|
|
2055
|
+
const interval = this.ctx.options.jobRetention.interval ?? DEFAULT_RETENTION_INTERVAL;
|
|
2056
|
+
this.cleanupJobs().catch((error) => {
|
|
2057
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2058
|
+
});
|
|
2059
|
+
this.cleanupIntervalId = setInterval(() => {
|
|
2060
|
+
this.cleanupJobs().catch((error) => {
|
|
2061
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2062
|
+
});
|
|
2063
|
+
}, interval);
|
|
2064
|
+
}
|
|
2065
|
+
callbacks.poll().catch((error) => {
|
|
2066
|
+
this.ctx.emit("job:error", { error: toError(error) });
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Stop all lifecycle timers.
|
|
2071
|
+
*
|
|
2072
|
+
* Clears poll, heartbeat, and cleanup intervals.
|
|
2073
|
+
*/
|
|
2074
|
+
stopTimers() {
|
|
2075
|
+
if (this.cleanupIntervalId) {
|
|
2076
|
+
clearInterval(this.cleanupIntervalId);
|
|
2077
|
+
this.cleanupIntervalId = null;
|
|
2078
|
+
}
|
|
2079
|
+
if (this.pollIntervalId) {
|
|
2080
|
+
clearInterval(this.pollIntervalId);
|
|
2081
|
+
this.pollIntervalId = null;
|
|
2082
|
+
}
|
|
2083
|
+
if (this.heartbeatIntervalId) {
|
|
2084
|
+
clearInterval(this.heartbeatIntervalId);
|
|
2085
|
+
this.heartbeatIntervalId = null;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Clean up old completed and failed jobs based on retention policy.
|
|
2090
|
+
*
|
|
2091
|
+
* - Removes completed jobs older than `jobRetention.completed`
|
|
2092
|
+
* - Removes failed jobs older than `jobRetention.failed`
|
|
2093
|
+
*
|
|
2094
|
+
* The cleanup runs concurrently for both statuses if configured.
|
|
2095
|
+
*
|
|
2096
|
+
* @returns Promise resolving when all deletion operations complete
|
|
2097
|
+
*/
|
|
2098
|
+
async cleanupJobs() {
|
|
2099
|
+
if (!this.ctx.options.jobRetention) return;
|
|
2100
|
+
const { completed, failed } = this.ctx.options.jobRetention;
|
|
2101
|
+
const now = Date.now();
|
|
2102
|
+
const deletions = [];
|
|
2103
|
+
if (completed != null) {
|
|
2104
|
+
const cutoff = new Date(now - completed);
|
|
2105
|
+
deletions.push(this.ctx.collection.deleteMany({
|
|
2106
|
+
status: JobStatus.COMPLETED,
|
|
2107
|
+
updatedAt: { $lt: cutoff }
|
|
2108
|
+
}));
|
|
2109
|
+
}
|
|
2110
|
+
if (failed != null) {
|
|
2111
|
+
const cutoff = new Date(now - failed);
|
|
2112
|
+
deletions.push(this.ctx.collection.deleteMany({
|
|
2113
|
+
status: JobStatus.FAILED,
|
|
2114
|
+
updatedAt: { $lt: cutoff }
|
|
2115
|
+
}));
|
|
2116
|
+
}
|
|
2117
|
+
if (deletions.length > 0) await Promise.all(deletions);
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
|
|
1951
2121
|
//#endregion
|
|
1952
2122
|
//#region src/scheduler/monque.ts
|
|
1953
2123
|
/**
|
|
@@ -2034,9 +2204,6 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2034
2204
|
options;
|
|
2035
2205
|
collection = null;
|
|
2036
2206
|
workers = /* @__PURE__ */ new Map();
|
|
2037
|
-
pollIntervalId = null;
|
|
2038
|
-
heartbeatIntervalId = null;
|
|
2039
|
-
cleanupIntervalId = null;
|
|
2040
2207
|
isRunning = false;
|
|
2041
2208
|
isInitialized = false;
|
|
2042
2209
|
_scheduler = null;
|
|
@@ -2044,6 +2211,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2044
2211
|
_query = null;
|
|
2045
2212
|
_processor = null;
|
|
2046
2213
|
_changeStreamHandler = null;
|
|
2214
|
+
_lifecycleManager = null;
|
|
2047
2215
|
constructor(db, options = {}) {
|
|
2048
2216
|
super();
|
|
2049
2217
|
this.db = db;
|
|
@@ -2061,7 +2229,9 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2061
2229
|
schedulerInstanceId: options.schedulerInstanceId ?? (0, node_crypto.randomUUID)(),
|
|
2062
2230
|
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
2063
2231
|
jobRetention: options.jobRetention,
|
|
2064
|
-
skipIndexCreation: options.skipIndexCreation ?? false
|
|
2232
|
+
skipIndexCreation: options.skipIndexCreation ?? false,
|
|
2233
|
+
maxPayloadSize: options.maxPayloadSize,
|
|
2234
|
+
statsCacheTtlMs: options.statsCacheTtlMs ?? 5e3
|
|
2065
2235
|
};
|
|
2066
2236
|
}
|
|
2067
2237
|
/**
|
|
@@ -2076,12 +2246,14 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2076
2246
|
this.collection = this.db.collection(this.options.collectionName);
|
|
2077
2247
|
if (!this.options.skipIndexCreation) await this.createIndexes();
|
|
2078
2248
|
if (this.options.recoverStaleJobs) await this.recoverStaleJobs();
|
|
2249
|
+
await this.checkInstanceCollision();
|
|
2079
2250
|
const ctx = this.buildContext();
|
|
2080
2251
|
this._scheduler = new JobScheduler(ctx);
|
|
2081
2252
|
this._manager = new JobManager(ctx);
|
|
2082
2253
|
this._query = new JobQueryService(ctx);
|
|
2083
2254
|
this._processor = new JobProcessor(ctx);
|
|
2084
2255
|
this._changeStreamHandler = new ChangeStreamHandler(ctx, () => this.processor.poll());
|
|
2256
|
+
this._lifecycleManager = new LifecycleManager(ctx);
|
|
2085
2257
|
this.isInitialized = true;
|
|
2086
2258
|
} catch (error) {
|
|
2087
2259
|
throw new ConnectionError(`Failed to initialize Monque: ${error instanceof Error ? error.message : "Unknown error during initialization"}`);
|
|
@@ -2112,6 +2284,11 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2112
2284
|
if (!this._changeStreamHandler) throw new ConnectionError("Monque not initialized. Call initialize() first.");
|
|
2113
2285
|
return this._changeStreamHandler;
|
|
2114
2286
|
}
|
|
2287
|
+
/** @throws {ConnectionError} if not initialized */
|
|
2288
|
+
get lifecycleManager() {
|
|
2289
|
+
if (!this._lifecycleManager) throw new ConnectionError("Monque not initialized. Call initialize() first.");
|
|
2290
|
+
return this._lifecycleManager;
|
|
2291
|
+
}
|
|
2115
2292
|
/**
|
|
2116
2293
|
* Build the shared context for internal services.
|
|
2117
2294
|
*/
|
|
@@ -2226,35 +2403,23 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2226
2403
|
if (result.modifiedCount > 0) this.emit("stale:recovered", { count: result.modifiedCount });
|
|
2227
2404
|
}
|
|
2228
2405
|
/**
|
|
2229
|
-
*
|
|
2406
|
+
* Check if another active instance is using the same schedulerInstanceId.
|
|
2407
|
+
* Uses heartbeat staleness to distinguish active instances from crashed ones.
|
|
2230
2408
|
*
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2409
|
+
* Called after stale recovery to avoid false positives: stale recovery resets
|
|
2410
|
+
* jobs with old `lockedAt`, so only jobs with recent heartbeats remain.
|
|
2233
2411
|
*
|
|
2234
|
-
*
|
|
2235
|
-
*
|
|
2236
|
-
* @returns Promise resolving when all deletion operations complete
|
|
2412
|
+
* @throws {ConnectionError} If an active instance with the same ID is detected
|
|
2237
2413
|
*/
|
|
2238
|
-
async
|
|
2239
|
-
if (!this.collection
|
|
2240
|
-
const
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
updatedAt: { $lt: cutoff }
|
|
2248
|
-
}));
|
|
2249
|
-
}
|
|
2250
|
-
if (failed) {
|
|
2251
|
-
const cutoff = new Date(now - failed);
|
|
2252
|
-
deletions.push(this.collection.deleteMany({
|
|
2253
|
-
status: JobStatus.FAILED,
|
|
2254
|
-
updatedAt: { $lt: cutoff }
|
|
2255
|
-
}));
|
|
2256
|
-
}
|
|
2257
|
-
if (deletions.length > 0) await Promise.all(deletions);
|
|
2414
|
+
async checkInstanceCollision() {
|
|
2415
|
+
if (!this.collection) return;
|
|
2416
|
+
const aliveThreshold = /* @__PURE__ */ new Date(Date.now() - this.options.heartbeatInterval * 2);
|
|
2417
|
+
const activeJob = await this.collection.findOne({
|
|
2418
|
+
claimedBy: this.options.schedulerInstanceId,
|
|
2419
|
+
status: JobStatus.PROCESSING,
|
|
2420
|
+
lastHeartbeat: { $gte: aliveThreshold }
|
|
2421
|
+
});
|
|
2422
|
+
if (activeJob) throw new ConnectionError(`Another active Monque instance is using schedulerInstanceId "${this.options.schedulerInstanceId}". Found processing job "${activeJob["name"]}" with recent heartbeat. Use a unique schedulerInstanceId or wait for the other instance to stop.`);
|
|
2258
2423
|
}
|
|
2259
2424
|
/**
|
|
2260
2425
|
* Enqueue a job for processing.
|
|
@@ -2274,6 +2439,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2274
2439
|
* @param options - Scheduling and deduplication options
|
|
2275
2440
|
* @returns Promise resolving to the created or existing job document
|
|
2276
2441
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2442
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
2277
2443
|
*
|
|
2278
2444
|
* @example Basic job enqueueing
|
|
2279
2445
|
* ```typescript
|
|
@@ -2299,6 +2465,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2299
2465
|
* });
|
|
2300
2466
|
* // Subsequent enqueues with same uniqueKey return existing pending/processing job
|
|
2301
2467
|
* ```
|
|
2468
|
+
*
|
|
2469
|
+
* @see {@link JobScheduler.enqueue}
|
|
2302
2470
|
*/
|
|
2303
2471
|
async enqueue(name, data, options = {}) {
|
|
2304
2472
|
this.ensureInitialized();
|
|
@@ -2331,6 +2499,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2331
2499
|
* await monque.now('process-order', { orderId: order.id });
|
|
2332
2500
|
* return order; // Return immediately, processing happens async
|
|
2333
2501
|
* ```
|
|
2502
|
+
*
|
|
2503
|
+
* @see {@link JobScheduler.now}
|
|
2334
2504
|
*/
|
|
2335
2505
|
async now(name, data) {
|
|
2336
2506
|
this.ensureInitialized();
|
|
@@ -2356,6 +2526,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2356
2526
|
* @returns Promise resolving to the created job document with `repeatInterval` set
|
|
2357
2527
|
* @throws {InvalidCronError} If cron expression is invalid
|
|
2358
2528
|
* @throws {ConnectionError} If database operation fails or scheduler not initialized
|
|
2529
|
+
* @throws {PayloadTooLargeError} If payload exceeds configured `maxPayloadSize`
|
|
2359
2530
|
*
|
|
2360
2531
|
* @example Hourly cleanup job
|
|
2361
2532
|
* ```typescript
|
|
@@ -2379,6 +2550,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2379
2550
|
* recipients: ['analytics@example.com']
|
|
2380
2551
|
* });
|
|
2381
2552
|
* ```
|
|
2553
|
+
*
|
|
2554
|
+
* @see {@link JobScheduler.schedule}
|
|
2382
2555
|
*/
|
|
2383
2556
|
async schedule(cron, name, data, options = {}) {
|
|
2384
2557
|
this.ensureInitialized();
|
|
@@ -2400,6 +2573,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2400
2573
|
* const job = await monque.enqueue('report', { type: 'daily' });
|
|
2401
2574
|
* await monque.cancelJob(job._id.toString());
|
|
2402
2575
|
* ```
|
|
2576
|
+
*
|
|
2577
|
+
* @see {@link JobManager.cancelJob}
|
|
2403
2578
|
*/
|
|
2404
2579
|
async cancelJob(jobId) {
|
|
2405
2580
|
this.ensureInitialized();
|
|
@@ -2422,6 +2597,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2422
2597
|
* await monque.retryJob(job._id.toString());
|
|
2423
2598
|
* });
|
|
2424
2599
|
* ```
|
|
2600
|
+
*
|
|
2601
|
+
* @see {@link JobManager.retryJob}
|
|
2425
2602
|
*/
|
|
2426
2603
|
async retryJob(jobId) {
|
|
2427
2604
|
this.ensureInitialized();
|
|
@@ -2442,6 +2619,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2442
2619
|
* const nextHour = new Date(Date.now() + 60 * 60 * 1000);
|
|
2443
2620
|
* await monque.rescheduleJob(jobId, nextHour);
|
|
2444
2621
|
* ```
|
|
2622
|
+
*
|
|
2623
|
+
* @see {@link JobManager.rescheduleJob}
|
|
2445
2624
|
*/
|
|
2446
2625
|
async rescheduleJob(jobId, runAt) {
|
|
2447
2626
|
this.ensureInitialized();
|
|
@@ -2463,20 +2642,23 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2463
2642
|
* console.log('Job permanently removed');
|
|
2464
2643
|
* }
|
|
2465
2644
|
* ```
|
|
2645
|
+
*
|
|
2646
|
+
* @see {@link JobManager.deleteJob}
|
|
2466
2647
|
*/
|
|
2467
2648
|
async deleteJob(jobId) {
|
|
2468
2649
|
this.ensureInitialized();
|
|
2469
2650
|
return this.manager.deleteJob(jobId);
|
|
2470
2651
|
}
|
|
2471
2652
|
/**
|
|
2472
|
-
* Cancel multiple jobs matching the given filter.
|
|
2653
|
+
* Cancel multiple jobs matching the given filter via a single updateMany call.
|
|
2473
2654
|
*
|
|
2474
|
-
* Only cancels jobs in 'pending' status
|
|
2475
|
-
*
|
|
2655
|
+
* Only cancels jobs in 'pending' status — the status guard is applied regardless
|
|
2656
|
+
* of what the filter specifies. Jobs in other states are silently skipped (not
|
|
2657
|
+
* matched by the query). Emits a 'jobs:cancelled' event with the count of
|
|
2476
2658
|
* successfully cancelled jobs.
|
|
2477
2659
|
*
|
|
2478
2660
|
* @param filter - Selector for which jobs to cancel (name, status, date range)
|
|
2479
|
-
* @returns Result with count of cancelled jobs
|
|
2661
|
+
* @returns Result with count of cancelled jobs (errors array always empty for bulk ops)
|
|
2480
2662
|
*
|
|
2481
2663
|
* @example Cancel all pending jobs for a queue
|
|
2482
2664
|
* ```typescript
|
|
@@ -2486,20 +2668,23 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2486
2668
|
* });
|
|
2487
2669
|
* console.log(`Cancelled ${result.count} jobs`);
|
|
2488
2670
|
* ```
|
|
2671
|
+
*
|
|
2672
|
+
* @see {@link JobManager.cancelJobs}
|
|
2489
2673
|
*/
|
|
2490
2674
|
async cancelJobs(filter) {
|
|
2491
2675
|
this.ensureInitialized();
|
|
2492
2676
|
return this.manager.cancelJobs(filter);
|
|
2493
2677
|
}
|
|
2494
2678
|
/**
|
|
2495
|
-
* Retry multiple jobs matching the given filter.
|
|
2679
|
+
* Retry multiple jobs matching the given filter via a single pipeline-style updateMany call.
|
|
2496
2680
|
*
|
|
2497
|
-
* Only retries jobs in 'failed' or 'cancelled' status
|
|
2498
|
-
*
|
|
2499
|
-
*
|
|
2681
|
+
* Only retries jobs in 'failed' or 'cancelled' status — the status guard is applied
|
|
2682
|
+
* regardless of what the filter specifies. Jobs in other states are silently skipped.
|
|
2683
|
+
* Uses `$rand` for per-document staggered `nextRunAt` to avoid thundering herd on retry.
|
|
2684
|
+
* Emits a 'jobs:retried' event with the count of successfully retried jobs.
|
|
2500
2685
|
*
|
|
2501
2686
|
* @param filter - Selector for which jobs to retry (name, status, date range)
|
|
2502
|
-
* @returns Result with count of retried jobs
|
|
2687
|
+
* @returns Result with count of retried jobs (errors array always empty for bulk ops)
|
|
2503
2688
|
*
|
|
2504
2689
|
* @example Retry all failed jobs
|
|
2505
2690
|
* ```typescript
|
|
@@ -2508,6 +2693,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2508
2693
|
* });
|
|
2509
2694
|
* console.log(`Retried ${result.count} jobs`);
|
|
2510
2695
|
* ```
|
|
2696
|
+
*
|
|
2697
|
+
* @see {@link JobManager.retryJobs}
|
|
2511
2698
|
*/
|
|
2512
2699
|
async retryJobs(filter) {
|
|
2513
2700
|
this.ensureInitialized();
|
|
@@ -2517,6 +2704,7 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2517
2704
|
* Delete multiple jobs matching the given filter.
|
|
2518
2705
|
*
|
|
2519
2706
|
* Deletes jobs in any status. Uses a batch delete for efficiency.
|
|
2707
|
+
* Emits a 'jobs:deleted' event with the count of deleted jobs.
|
|
2520
2708
|
* Does not emit individual 'job:deleted' events to avoid noise.
|
|
2521
2709
|
*
|
|
2522
2710
|
* @param filter - Selector for which jobs to delete (name, status, date range)
|
|
@@ -2531,6 +2719,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2531
2719
|
* });
|
|
2532
2720
|
* console.log(`Deleted ${result.count} jobs`);
|
|
2533
2721
|
* ```
|
|
2722
|
+
*
|
|
2723
|
+
* @see {@link JobManager.deleteJobs}
|
|
2534
2724
|
*/
|
|
2535
2725
|
async deleteJobs(filter) {
|
|
2536
2726
|
this.ensureInitialized();
|
|
@@ -2566,6 +2756,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2566
2756
|
* res.json(job);
|
|
2567
2757
|
* });
|
|
2568
2758
|
* ```
|
|
2759
|
+
*
|
|
2760
|
+
* @see {@link JobQueryService.getJob}
|
|
2569
2761
|
*/
|
|
2570
2762
|
async getJob(id) {
|
|
2571
2763
|
this.ensureInitialized();
|
|
@@ -2612,6 +2804,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2612
2804
|
* const jobs = await monque.getJobs();
|
|
2613
2805
|
* const pendingRecurring = jobs.filter(job => isPendingJob(job) && isRecurringJob(job));
|
|
2614
2806
|
* ```
|
|
2807
|
+
*
|
|
2808
|
+
* @see {@link JobQueryService.getJobs}
|
|
2615
2809
|
*/
|
|
2616
2810
|
async getJobs(filter = {}) {
|
|
2617
2811
|
this.ensureInitialized();
|
|
@@ -2645,6 +2839,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2645
2839
|
* });
|
|
2646
2840
|
* }
|
|
2647
2841
|
* ```
|
|
2842
|
+
*
|
|
2843
|
+
* @see {@link JobQueryService.getJobsWithCursor}
|
|
2648
2844
|
*/
|
|
2649
2845
|
async getJobsWithCursor(options = {}) {
|
|
2650
2846
|
this.ensureInitialized();
|
|
@@ -2656,6 +2852,9 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2656
2852
|
* Uses MongoDB aggregation pipeline for efficient server-side calculation.
|
|
2657
2853
|
* Returns counts per status and optional average processing duration for completed jobs.
|
|
2658
2854
|
*
|
|
2855
|
+
* Results are cached per unique filter with a configurable TTL (default 5s).
|
|
2856
|
+
* Set `statsCacheTtlMs: 0` to disable caching.
|
|
2857
|
+
*
|
|
2659
2858
|
* @param filter - Optional filter to scope statistics by job name
|
|
2660
2859
|
* @returns Promise resolving to queue statistics
|
|
2661
2860
|
* @throws {AggregationTimeoutError} If aggregation exceeds 30 second timeout
|
|
@@ -2672,6 +2871,8 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2672
2871
|
* const emailStats = await monque.getQueueStats({ name: 'send-email' });
|
|
2673
2872
|
* console.log(`${emailStats.total} email jobs in queue`);
|
|
2674
2873
|
* ```
|
|
2874
|
+
*
|
|
2875
|
+
* @see {@link JobQueryService.getQueueStats}
|
|
2675
2876
|
*/
|
|
2676
2877
|
async getQueueStats(filter) {
|
|
2677
2878
|
this.ensureInitialized();
|
|
@@ -2798,29 +2999,9 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2798
2999
|
if (!this.isInitialized) throw new ConnectionError("Monque not initialized. Call initialize() before start().");
|
|
2799
3000
|
this.isRunning = true;
|
|
2800
3001
|
this.changeStreamHandler.setup();
|
|
2801
|
-
this.
|
|
2802
|
-
this.processor.poll()
|
|
2803
|
-
|
|
2804
|
-
});
|
|
2805
|
-
}, this.options.pollInterval);
|
|
2806
|
-
this.heartbeatIntervalId = setInterval(() => {
|
|
2807
|
-
this.processor.updateHeartbeats().catch((error) => {
|
|
2808
|
-
this.emit("job:error", { error: toError(error) });
|
|
2809
|
-
});
|
|
2810
|
-
}, this.options.heartbeatInterval);
|
|
2811
|
-
if (this.options.jobRetention) {
|
|
2812
|
-
const interval = this.options.jobRetention.interval ?? DEFAULTS.retentionInterval;
|
|
2813
|
-
this.cleanupJobs().catch((error) => {
|
|
2814
|
-
this.emit("job:error", { error: toError(error) });
|
|
2815
|
-
});
|
|
2816
|
-
this.cleanupIntervalId = setInterval(() => {
|
|
2817
|
-
this.cleanupJobs().catch((error) => {
|
|
2818
|
-
this.emit("job:error", { error: toError(error) });
|
|
2819
|
-
});
|
|
2820
|
-
}, interval);
|
|
2821
|
-
}
|
|
2822
|
-
this.processor.poll().catch((error) => {
|
|
2823
|
-
this.emit("job:error", { error: toError(error) });
|
|
3002
|
+
this.lifecycleManager.startTimers({
|
|
3003
|
+
poll: () => this.processor.poll(),
|
|
3004
|
+
updateHeartbeats: () => this.processor.updateHeartbeats()
|
|
2824
3005
|
});
|
|
2825
3006
|
}
|
|
2826
3007
|
/**
|
|
@@ -2859,19 +3040,11 @@ var Monque = class extends node_events.EventEmitter {
|
|
|
2859
3040
|
async stop() {
|
|
2860
3041
|
if (!this.isRunning) return;
|
|
2861
3042
|
this.isRunning = false;
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
if (this.pollIntervalId) {
|
|
2868
|
-
clearInterval(this.pollIntervalId);
|
|
2869
|
-
this.pollIntervalId = null;
|
|
2870
|
-
}
|
|
2871
|
-
if (this.heartbeatIntervalId) {
|
|
2872
|
-
clearInterval(this.heartbeatIntervalId);
|
|
2873
|
-
this.heartbeatIntervalId = null;
|
|
2874
|
-
}
|
|
3043
|
+
this._query?.clearStatsCache();
|
|
3044
|
+
try {
|
|
3045
|
+
await this.changeStreamHandler.close();
|
|
3046
|
+
} catch {}
|
|
3047
|
+
this.lifecycleManager.stopTimers();
|
|
2875
3048
|
if (this.getActiveJobs().length === 0) return;
|
|
2876
3049
|
let checkInterval;
|
|
2877
3050
|
const waitForJobs = new Promise((resolve) => {
|
|
@@ -3006,6 +3179,7 @@ exports.JobStateError = JobStateError;
|
|
|
3006
3179
|
exports.JobStatus = JobStatus;
|
|
3007
3180
|
exports.Monque = Monque;
|
|
3008
3181
|
exports.MonqueError = MonqueError;
|
|
3182
|
+
exports.PayloadTooLargeError = PayloadTooLargeError;
|
|
3009
3183
|
exports.ShutdownTimeoutError = ShutdownTimeoutError;
|
|
3010
3184
|
exports.WorkerRegistrationError = WorkerRegistrationError;
|
|
3011
3185
|
exports.calculateBackoff = calculateBackoff;
|