@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monque/core",
3
- "version": "1.1.2",
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 src/"
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": "^7.0.0"
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": "^11.11.0",
81
- "@total-typescript/ts-reset": "^0.6.1",
82
- "@types/bun": "^1.3.6",
83
- "@vitest/coverage-v8": "^4.0.18",
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": "^7.0.0",
86
- "publint": "^0.3.17",
87
- "tsdown": "^0.20.1",
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
  }
@@ -41,7 +41,7 @@ const DEFAULTS = {
41
41
  maxRetries: 10,
42
42
  baseRetryInterval: 1000,
43
43
  shutdownTimeout: 30000,
44
- defaultConcurrency: 5,
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
- defaultConcurrency: options.defaultConcurrency ?? DEFAULTS.defaultConcurrency,
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.defaultConcurrency;
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 availableSlots = worker.concurrency - worker.activeJobs.size;
72
+ const workerAvailableSlots = worker.concurrency - worker.activeJobs.size;
33
73
 
34
- if (availableSlots <= 0) {
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<Omit<MonqueOptions, 'maxBackoffDelay' | 'jobRetention'>>,
17
- Pick<MonqueOptions, 'maxBackoffDelay' | 'jobRetention'> {}
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
  *
@@ -9,7 +9,8 @@
9
9
  * maxRetries: 10,
10
10
  * baseRetryInterval: 1000,
11
11
  * shutdownTimeout: 30000,
12
- * defaultConcurrency: 5,
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
  }