@monque/core 1.1.2 → 1.2.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/README.md +20 -1
- package/dist/CHANGELOG.md +10 -0
- package/dist/README.md +20 -1
- package/dist/index.cjs +43 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +40 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +43 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -14
- package/src/scheduler/monque.ts +5 -3
- package/src/scheduler/services/job-processor.ts +60 -5
- package/src/scheduler/services/types.ts +15 -3
- package/src/scheduler/types.ts +43 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monque/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MongoDB-backed job scheduler with atomic locking, exponential backoff, and cron scheduling",
|
|
5
5
|
"author": "Maurice de Bruyn <debruyn.maurice@gmail.com>",
|
|
6
6
|
"repository": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"test:watch": "vitest",
|
|
54
54
|
"test:watch:unit": "vitest --config vitest.unit.config.ts",
|
|
55
55
|
"test:watch:integration": "vitest integration/",
|
|
56
|
-
"lint": "biome check
|
|
56
|
+
"lint": "biome check ."
|
|
57
57
|
},
|
|
58
58
|
"keywords": [
|
|
59
59
|
"job",
|
|
@@ -72,21 +72,17 @@
|
|
|
72
72
|
"cron-parser": "^5.5.0"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"mongodb": "
|
|
75
|
+
"mongodb": "catalog:"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@arethetypeswrong/cli": "^0.18.2",
|
|
79
78
|
"@faker-js/faker": "^10.2.0",
|
|
80
|
-
"@testcontainers/mongodb": "
|
|
81
|
-
"@total-typescript/ts-reset": "
|
|
82
|
-
"@types/
|
|
83
|
-
"@vitest/coverage-v8": "
|
|
79
|
+
"@testcontainers/mongodb": "catalog:",
|
|
80
|
+
"@total-typescript/ts-reset": "catalog:",
|
|
81
|
+
"@types/node": "catalog:",
|
|
82
|
+
"@vitest/coverage-v8": "catalog:",
|
|
84
83
|
"fishery": "^2.4.0",
|
|
85
|
-
"mongodb": "
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"typescript": "^5.9.3",
|
|
89
|
-
"unplugin-unused": "^0.5.7",
|
|
90
|
-
"vitest": "^4.0.18"
|
|
84
|
+
"mongodb": "catalog:",
|
|
85
|
+
"tsdown": "catalog:",
|
|
86
|
+
"vitest": "catalog:"
|
|
91
87
|
}
|
|
92
88
|
}
|
package/src/scheduler/monque.ts
CHANGED
|
@@ -41,7 +41,7 @@ const DEFAULTS = {
|
|
|
41
41
|
maxRetries: 10,
|
|
42
42
|
baseRetryInterval: 1000,
|
|
43
43
|
shutdownTimeout: 30000,
|
|
44
|
-
|
|
44
|
+
workerConcurrency: 5,
|
|
45
45
|
lockTimeout: 1_800_000, // 30 minutes
|
|
46
46
|
recoverStaleJobs: true,
|
|
47
47
|
heartbeatInterval: 30000, // 30 seconds
|
|
@@ -139,10 +139,12 @@ export class Monque extends EventEmitter {
|
|
|
139
139
|
maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
|
|
140
140
|
baseRetryInterval: options.baseRetryInterval ?? DEFAULTS.baseRetryInterval,
|
|
141
141
|
shutdownTimeout: options.shutdownTimeout ?? DEFAULTS.shutdownTimeout,
|
|
142
|
-
|
|
142
|
+
workerConcurrency:
|
|
143
|
+
options.workerConcurrency ?? options.defaultConcurrency ?? DEFAULTS.workerConcurrency,
|
|
143
144
|
lockTimeout: options.lockTimeout ?? DEFAULTS.lockTimeout,
|
|
144
145
|
recoverStaleJobs: options.recoverStaleJobs ?? DEFAULTS.recoverStaleJobs,
|
|
145
146
|
maxBackoffDelay: options.maxBackoffDelay,
|
|
147
|
+
instanceConcurrency: options.instanceConcurrency ?? options.maxConcurrency,
|
|
146
148
|
schedulerInstanceId: options.schedulerInstanceId ?? randomUUID(),
|
|
147
149
|
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
148
150
|
jobRetention: options.jobRetention,
|
|
@@ -918,7 +920,7 @@ export class Monque extends EventEmitter {
|
|
|
918
920
|
* ```
|
|
919
921
|
*/
|
|
920
922
|
register<T>(name: string, handler: JobHandler<T>, options: WorkerOptions = {}): void {
|
|
921
|
-
const concurrency = options.concurrency ?? this.options.
|
|
923
|
+
const concurrency = options.concurrency ?? this.options.workerConcurrency;
|
|
922
924
|
|
|
923
925
|
// Check for existing worker and throw unless replace is explicitly true
|
|
924
926
|
if (this.workers.has(name) && options.replace !== true) {
|
|
@@ -15,34 +15,91 @@ import type { SchedulerContext } from './types.js';
|
|
|
15
15
|
export class JobProcessor {
|
|
16
16
|
constructor(private readonly ctx: SchedulerContext) {}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Get the total number of active jobs across all workers.
|
|
20
|
+
*
|
|
21
|
+
* Used for instance-level throttling when `instanceConcurrency` is configured.
|
|
22
|
+
*/
|
|
23
|
+
private getTotalActiveJobs(): number {
|
|
24
|
+
let total = 0;
|
|
25
|
+
for (const worker of this.ctx.workers.values()) {
|
|
26
|
+
total += worker.activeJobs.size;
|
|
27
|
+
}
|
|
28
|
+
return total;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the number of available slots considering the global instanceConcurrency limit.
|
|
33
|
+
*
|
|
34
|
+
* @param workerAvailableSlots - Available slots for the specific worker
|
|
35
|
+
* @returns Number of slots available after applying global limit
|
|
36
|
+
*/
|
|
37
|
+
private getGloballyAvailableSlots(workerAvailableSlots: number): number {
|
|
38
|
+
const { instanceConcurrency } = this.ctx.options;
|
|
39
|
+
|
|
40
|
+
if (instanceConcurrency === undefined) {
|
|
41
|
+
return workerAvailableSlots;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const totalActive = this.getTotalActiveJobs();
|
|
45
|
+
const globalAvailable = instanceConcurrency - totalActive;
|
|
46
|
+
|
|
47
|
+
return Math.min(workerAvailableSlots, globalAvailable);
|
|
48
|
+
}
|
|
49
|
+
|
|
18
50
|
/**
|
|
19
51
|
* Poll for available jobs and process them.
|
|
20
52
|
*
|
|
21
53
|
* Called at regular intervals (configured by `pollInterval`). For each registered worker,
|
|
22
54
|
* attempts to acquire jobs up to the worker's available concurrency slots.
|
|
23
|
-
* Aborts early if the scheduler is stopping (`isRunning` is false)
|
|
55
|
+
* Aborts early if the scheduler is stopping (`isRunning` is false) or if
|
|
56
|
+
* the instance-level `instanceConcurrency` limit is reached.
|
|
24
57
|
*/
|
|
25
58
|
async poll(): Promise<void> {
|
|
26
59
|
if (!this.ctx.isRunning()) {
|
|
27
60
|
return;
|
|
28
61
|
}
|
|
29
62
|
|
|
63
|
+
// Early exit if global instanceConcurrency is reached
|
|
64
|
+
const { instanceConcurrency } = this.ctx.options;
|
|
65
|
+
|
|
66
|
+
if (instanceConcurrency !== undefined && this.getTotalActiveJobs() >= instanceConcurrency) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
for (const [name, worker] of this.ctx.workers) {
|
|
31
71
|
// Check if worker has capacity
|
|
32
|
-
const
|
|
72
|
+
const workerAvailableSlots = worker.concurrency - worker.activeJobs.size;
|
|
33
73
|
|
|
34
|
-
if (
|
|
74
|
+
if (workerAvailableSlots <= 0) {
|
|
35
75
|
continue;
|
|
36
76
|
}
|
|
37
77
|
|
|
78
|
+
// Apply global concurrency limit
|
|
79
|
+
const availableSlots = this.getGloballyAvailableSlots(workerAvailableSlots);
|
|
80
|
+
|
|
81
|
+
if (availableSlots <= 0) {
|
|
82
|
+
// Global limit reached, stop processing all workers
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
// Try to acquire jobs up to available slots
|
|
39
87
|
for (let i = 0; i < availableSlots; i++) {
|
|
40
88
|
if (!this.ctx.isRunning()) {
|
|
41
89
|
return;
|
|
42
90
|
}
|
|
91
|
+
|
|
92
|
+
// Re-check global limit before each acquisition
|
|
93
|
+
if (instanceConcurrency !== undefined && this.getTotalActiveJobs() >= instanceConcurrency) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
43
97
|
const job = await this.acquireJob(name);
|
|
44
98
|
|
|
45
99
|
if (job) {
|
|
100
|
+
// Add to activeJobs immediately to correctly track concurrency
|
|
101
|
+
worker.activeJobs.set(job._id.toString(), job);
|
|
102
|
+
|
|
46
103
|
this.processJob(job, worker).catch((error: unknown) => {
|
|
47
104
|
this.ctx.emit('job:error', { error: error as Error, job });
|
|
48
105
|
});
|
|
@@ -121,8 +178,6 @@ export class JobProcessor {
|
|
|
121
178
|
*/
|
|
122
179
|
async processJob(job: PersistedJob, worker: WorkerRegistration): Promise<void> {
|
|
123
180
|
const jobId = job._id.toString();
|
|
124
|
-
worker.activeJobs.set(jobId, job);
|
|
125
|
-
|
|
126
181
|
const startTime = Date.now();
|
|
127
182
|
this.ctx.emit('job:start', job);
|
|
128
183
|
|
|
@@ -10,11 +10,23 @@ import type { MonqueOptions } from '../types.js';
|
|
|
10
10
|
* Resolved Monque options with all defaults applied.
|
|
11
11
|
*
|
|
12
12
|
* Required options have their defaults filled in, while truly optional
|
|
13
|
-
* options (`maxBackoffDelay`, `jobRetention`) remain optional.
|
|
13
|
+
* options (`maxBackoffDelay`, `jobRetention`, `instanceConcurrency`) remain optional.
|
|
14
14
|
*/
|
|
15
15
|
export interface ResolvedMonqueOptions
|
|
16
|
-
extends Required<
|
|
17
|
-
|
|
16
|
+
extends Required<
|
|
17
|
+
Omit<
|
|
18
|
+
MonqueOptions,
|
|
19
|
+
| 'maxBackoffDelay'
|
|
20
|
+
| 'jobRetention'
|
|
21
|
+
| 'instanceConcurrency'
|
|
22
|
+
| 'defaultConcurrency'
|
|
23
|
+
| 'maxConcurrency'
|
|
24
|
+
>
|
|
25
|
+
>,
|
|
26
|
+
Pick<MonqueOptions, 'maxBackoffDelay' | 'jobRetention' | 'instanceConcurrency'> {
|
|
27
|
+
// Ensure resolved options use the new naming convention
|
|
28
|
+
workerConcurrency: number;
|
|
29
|
+
}
|
|
18
30
|
/**
|
|
19
31
|
* Shared context provided to all internal Monque services.
|
|
20
32
|
*
|
package/src/scheduler/types.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* maxRetries: 10,
|
|
10
10
|
* baseRetryInterval: 1000,
|
|
11
11
|
* shutdownTimeout: 30000,
|
|
12
|
-
*
|
|
12
|
+
* workerConcurrency: 5, // Per-worker default
|
|
13
|
+
* instanceConcurrency: 20, // Global instance limit
|
|
13
14
|
* });
|
|
14
15
|
* ```
|
|
15
16
|
*/
|
|
@@ -56,7 +57,19 @@ export interface MonqueOptions {
|
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* Default number of concurrent jobs per worker.
|
|
60
|
+
*
|
|
61
|
+
* This is the per-worker concurrency limit applied when a worker is registered
|
|
62
|
+
* without specifying its own `concurrency` option.
|
|
63
|
+
*
|
|
64
|
+
* @default 5
|
|
65
|
+
*/
|
|
66
|
+
workerConcurrency?: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Default number of concurrent jobs per worker.
|
|
70
|
+
*
|
|
59
71
|
* @default 5
|
|
72
|
+
* @deprecated Use `workerConcurrency` instead. Will be removed in a future major version.
|
|
60
73
|
*/
|
|
61
74
|
defaultConcurrency?: number;
|
|
62
75
|
|
|
@@ -120,4 +133,33 @@ export interface MonqueOptions {
|
|
|
120
133
|
interval?: number;
|
|
121
134
|
}
|
|
122
135
|
| undefined;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Maximum number of concurrent jobs processed by this instance across all registered workers.
|
|
139
|
+
*
|
|
140
|
+
* If reached, the scheduler will stop claiming new jobs until active jobs complete.
|
|
141
|
+
* Use this to prevent a single instance from overwhelming system resources.
|
|
142
|
+
*
|
|
143
|
+
* Note: This is an instance-level limit. Each worker still respects its own `concurrency`
|
|
144
|
+
* setting, but the total across all workers cannot exceed this limit.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const monque = new Monque(db, {
|
|
149
|
+
* instanceConcurrency: 10, // Instance processes max 10 jobs total
|
|
150
|
+
* workerConcurrency: 5, // Each worker defaults to 5 concurrent jobs
|
|
151
|
+
* });
|
|
152
|
+
*
|
|
153
|
+
* // With 3 workers at concurrency 5, normally 15 jobs could run.
|
|
154
|
+
* // With instanceConcurrency: 10, only 10 jobs run at any time.
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
instanceConcurrency?: number | undefined;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Maximum number of concurrent jobs processed by this instance across all registered workers.
|
|
161
|
+
*
|
|
162
|
+
* @deprecated Use `instanceConcurrency` instead. Will be removed in a future major version.
|
|
163
|
+
*/
|
|
164
|
+
maxConcurrency?: number | undefined;
|
|
123
165
|
}
|