bunqueue 1.9.6 → 1.9.8
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/application/backgroundTasks.d.ts +29 -0
- package/dist/application/backgroundTasks.d.ts.map +1 -0
- package/dist/application/backgroundTasks.js +155 -0
- package/dist/application/backgroundTasks.js.map +1 -0
- package/dist/application/cleanupTasks.d.ts +11 -0
- package/dist/application/cleanupTasks.d.ts.map +1 -0
- package/dist/application/cleanupTasks.js +216 -0
- package/dist/application/cleanupTasks.js.map +1 -0
- package/dist/application/clientTracking.d.ts +22 -0
- package/dist/application/clientTracking.d.ts.map +1 -0
- package/dist/application/clientTracking.js +122 -0
- package/dist/application/clientTracking.js.map +1 -0
- package/dist/application/contextFactory.d.ts +97 -0
- package/dist/application/contextFactory.d.ts.map +1 -0
- package/dist/application/contextFactory.js +169 -0
- package/dist/application/contextFactory.js.map +1 -0
- package/dist/application/dependencyProcessor.d.ts +11 -0
- package/dist/application/dependencyProcessor.d.ts.map +1 -0
- package/dist/application/dependencyProcessor.js +69 -0
- package/dist/application/dependencyProcessor.js.map +1 -0
- package/dist/application/dlqManager.d.ts +12 -0
- package/dist/application/dlqManager.d.ts.map +1 -1
- package/dist/application/dlqManager.js +36 -0
- package/dist/application/dlqManager.js.map +1 -1
- package/dist/application/lockManager.d.ts +15 -0
- package/dist/application/lockManager.d.ts.map +1 -0
- package/dist/application/lockManager.js +118 -0
- package/dist/application/lockManager.js.map +1 -0
- package/dist/application/lockOperations.d.ts +39 -0
- package/dist/application/lockOperations.d.ts.map +1 -0
- package/dist/application/lockOperations.js +101 -0
- package/dist/application/lockOperations.js.map +1 -0
- package/dist/application/operations/ack.d.ts +0 -5
- package/dist/application/operations/ack.d.ts.map +1 -1
- package/dist/application/operations/ack.js +30 -258
- package/dist/application/operations/ack.js.map +1 -1
- package/dist/application/operations/ackHelpers.d.ts +78 -0
- package/dist/application/operations/ackHelpers.d.ts.map +1 -0
- package/dist/application/operations/ackHelpers.js +162 -0
- package/dist/application/operations/ackHelpers.js.map +1 -0
- package/dist/application/operations/jobManagement.d.ts +2 -0
- package/dist/application/operations/jobManagement.d.ts.map +1 -1
- package/dist/application/operations/jobManagement.js +8 -0
- package/dist/application/operations/jobManagement.js.map +1 -1
- package/dist/application/operations/push.d.ts.map +1 -1
- package/dist/application/operations/push.js +8 -2
- package/dist/application/operations/push.js.map +1 -1
- package/dist/application/operations/queryOperations.d.ts +11 -0
- package/dist/application/operations/queryOperations.d.ts.map +1 -1
- package/dist/application/operations/queryOperations.js +32 -0
- package/dist/application/operations/queryOperations.js.map +1 -1
- package/dist/application/queueManager.d.ts +13 -183
- package/dist/application/queueManager.d.ts.map +1 -1
- package/dist/application/queueManager.js +134 -1110
- package/dist/application/queueManager.js.map +1 -1
- package/dist/application/stallDetection.d.ts +11 -0
- package/dist/application/stallDetection.d.ts.map +1 -0
- package/dist/application/stallDetection.js +128 -0
- package/dist/application/stallDetection.js.map +1 -0
- package/dist/application/statsManager.d.ts +56 -0
- package/dist/application/statsManager.d.ts.map +1 -0
- package/dist/application/statsManager.js +111 -0
- package/dist/application/statsManager.js.map +1 -0
- package/dist/application/types.d.ts +123 -0
- package/dist/application/types.d.ts.map +1 -0
- package/dist/application/types.js +16 -0
- package/dist/application/types.js.map +1 -0
- package/dist/domain/queue/dependencyTracker.d.ts +74 -0
- package/dist/domain/queue/dependencyTracker.d.ts.map +1 -0
- package/dist/domain/queue/dependencyTracker.js +126 -0
- package/dist/domain/queue/dependencyTracker.js.map +1 -0
- package/dist/domain/queue/dlqShard.d.ts +59 -0
- package/dist/domain/queue/dlqShard.d.ts.map +1 -0
- package/dist/domain/queue/dlqShard.js +165 -0
- package/dist/domain/queue/dlqShard.js.map +1 -0
- package/dist/domain/queue/limiterManager.d.ts +44 -0
- package/dist/domain/queue/limiterManager.d.ts.map +1 -0
- package/dist/domain/queue/limiterManager.js +99 -0
- package/dist/domain/queue/limiterManager.js.map +1 -0
- package/dist/domain/queue/shard.d.ts +29 -122
- package/dist/domain/queue/shard.d.ts.map +1 -1
- package/dist/domain/queue/shard.js +152 -426
- package/dist/domain/queue/shard.js.map +1 -1
- package/dist/domain/queue/temporalManager.d.ts +81 -0
- package/dist/domain/queue/temporalManager.d.ts.map +1 -0
- package/dist/domain/queue/temporalManager.js +149 -0
- package/dist/domain/queue/temporalManager.js.map +1 -0
- package/dist/domain/queue/uniqueKeyManager.d.ts +32 -0
- package/dist/domain/queue/uniqueKeyManager.d.ts.map +1 -0
- package/dist/domain/queue/uniqueKeyManager.js +87 -0
- package/dist/domain/queue/uniqueKeyManager.js.map +1 -0
- package/dist/infrastructure/backup/s3Backup.d.ts +3 -40
- package/dist/infrastructure/backup/s3Backup.d.ts.map +1 -1
- package/dist/infrastructure/backup/s3Backup.js +10 -182
- package/dist/infrastructure/backup/s3Backup.js.map +1 -1
- package/dist/infrastructure/backup/s3BackupConfig.d.ts +67 -0
- package/dist/infrastructure/backup/s3BackupConfig.d.ts.map +1 -0
- package/dist/infrastructure/backup/s3BackupConfig.js +48 -0
- package/dist/infrastructure/backup/s3BackupConfig.js.map +1 -0
- package/dist/infrastructure/backup/s3BackupOperations.d.ts +23 -0
- package/dist/infrastructure/backup/s3BackupOperations.d.ts.map +1 -0
- package/dist/infrastructure/backup/s3BackupOperations.js +170 -0
- package/dist/infrastructure/backup/s3BackupOperations.js.map +1 -0
- package/dist/infrastructure/persistence/sqlite.d.ts +4 -13
- package/dist/infrastructure/persistence/sqlite.d.ts.map +1 -1
- package/dist/infrastructure/persistence/sqlite.js +23 -178
- package/dist/infrastructure/persistence/sqlite.js.map +1 -1
- package/dist/infrastructure/persistence/sqliteBatch.d.ts +38 -0
- package/dist/infrastructure/persistence/sqliteBatch.d.ts.map +1 -0
- package/dist/infrastructure/persistence/sqliteBatch.js +124 -0
- package/dist/infrastructure/persistence/sqliteBatch.js.map +1 -0
- package/dist/infrastructure/persistence/sqliteSerializer.d.ts +17 -0
- package/dist/infrastructure/persistence/sqliteSerializer.d.ts.map +1 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js +81 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js.map +1 -0
- package/dist/infrastructure/server/handler.d.ts.map +1 -1
- package/dist/infrastructure/server/handler.js +1 -186
- package/dist/infrastructure/server/handler.js.map +1 -1
- package/dist/infrastructure/server/handlerRoutes.d.ts +23 -0
- package/dist/infrastructure/server/handlerRoutes.d.ts.map +1 -0
- package/dist/infrastructure/server/handlerRoutes.js +190 -0
- package/dist/infrastructure/server/handlerRoutes.js.map +1 -0
- package/dist/infrastructure/server/http.d.ts +4 -25
- package/dist/infrastructure/server/http.d.ts.map +1 -1
- package/dist/infrastructure/server/http.js +43 -285
- package/dist/infrastructure/server/http.js.map +1 -1
- package/dist/infrastructure/server/httpEndpoints.d.ts +19 -0
- package/dist/infrastructure/server/httpEndpoints.d.ts.map +1 -0
- package/dist/infrastructure/server/httpEndpoints.js +151 -0
- package/dist/infrastructure/server/httpEndpoints.js.map +1 -0
- package/dist/infrastructure/server/sseHandler.d.ts +27 -0
- package/dist/infrastructure/server/sseHandler.d.ts.map +1 -0
- package/dist/infrastructure/server/sseHandler.js +77 -0
- package/dist/infrastructure/server/sseHandler.js.map +1 -0
- package/dist/infrastructure/server/tcp.d.ts.map +1 -1
- package/dist/infrastructure/server/tcp.js +14 -8
- package/dist/infrastructure/server/tcp.js.map +1 -1
- package/dist/infrastructure/server/wsHandler.d.ts +31 -0
- package/dist/infrastructure/server/wsHandler.d.ts.map +1 -0
- package/dist/infrastructure/server/wsHandler.js +63 -0
- package/dist/infrastructure/server/wsHandler.js.map +1 -0
- package/dist/mcp/index.js +3 -465
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/mcpHandlers.d.ts +129 -0
- package/dist/mcp/mcpHandlers.d.ts.map +1 -0
- package/dist/mcp/mcpHandlers.js +204 -0
- package/dist/mcp/mcpHandlers.js.map +1 -0
- package/dist/mcp/mcpTools.d.ts +15 -0
- package/dist/mcp/mcpTools.d.ts.map +1 -0
- package/dist/mcp/mcpTools.js +277 -0
- package/dist/mcp/mcpTools.js.map +1 -0
- package/package.json +2 -2
- package/dist/cli/dashboard.d.ts +0 -32
- package/dist/cli/dashboard.d.ts.map +0 -1
- package/dist/cli/dashboard.js +0 -183
- package/dist/cli/dashboard.js.map +0 -1
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
* Queue Manager
|
|
3
3
|
* Core orchestrator for all queue operations
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { queueLog } from '../shared/logger';
|
|
7
|
-
import { getStallAction, incrementStallCount } from '../domain/types/stall';
|
|
5
|
+
import { DEFAULT_LOCK_TTL } from '../domain/types/job';
|
|
8
6
|
import { Shard } from '../domain/queue/shard';
|
|
9
7
|
import { SqliteStorage } from '../infrastructure/persistence/sqlite';
|
|
10
8
|
import { CronScheduler } from '../infrastructure/scheduler/cronScheduler';
|
|
11
9
|
import { WebhookManager } from './webhookManager';
|
|
12
10
|
import { WorkerManager } from './workerManager';
|
|
13
11
|
import { EventsManager } from './eventsManager';
|
|
14
|
-
import { RWLock
|
|
15
|
-
import { shardIndex,
|
|
12
|
+
import { RWLock } from '../shared/lock';
|
|
13
|
+
import { shardIndex, SHARD_COUNT } from '../shared/hash';
|
|
16
14
|
import { pushJob, pushJobBatch } from './operations/push';
|
|
17
15
|
import { pullJob, pullJobBatch } from './operations/pull';
|
|
18
|
-
import { ackJob, ackJobBatch, ackJobBatchWithResults, failJob
|
|
16
|
+
import { ackJob, ackJobBatch, ackJobBatchWithResults, failJob } from './operations/ack';
|
|
19
17
|
import * as queueControl from './operations/queueControl';
|
|
20
18
|
import * as jobMgmt from './operations/jobManagement';
|
|
21
19
|
import * as queryOps from './operations/queryOperations';
|
|
@@ -23,18 +21,11 @@ import * as dlqOps from './dlqManager';
|
|
|
23
21
|
import * as logsOps from './jobLogsManager';
|
|
24
22
|
import { generatePrometheusMetrics } from './metricsExporter';
|
|
25
23
|
import { LRUMap, BoundedSet, BoundedMap } from '../shared/lru';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
maxWaitingDeps: 10_000,
|
|
32
|
-
cleanupIntervalMs: 10_000,
|
|
33
|
-
jobTimeoutCheckMs: 5_000,
|
|
34
|
-
dependencyCheckMs: 1_000,
|
|
35
|
-
stallCheckMs: 5_000,
|
|
36
|
-
dlqMaintenanceMs: 60_000,
|
|
37
|
-
};
|
|
24
|
+
import { DEFAULT_CONFIG } from './types';
|
|
25
|
+
import * as lockMgr from './lockManager';
|
|
26
|
+
import * as bgTasks from './backgroundTasks';
|
|
27
|
+
import * as statsMgr from './statsManager';
|
|
28
|
+
import { ContextFactory } from './contextFactory';
|
|
38
29
|
/**
|
|
39
30
|
* QueueManager - Central coordinator
|
|
40
31
|
*/
|
|
@@ -52,17 +43,12 @@ export class QueueManager {
|
|
|
52
43
|
jobResults;
|
|
53
44
|
customIdMap;
|
|
54
45
|
jobLogs;
|
|
55
|
-
// Deferred dependency resolution queue
|
|
46
|
+
// Deferred dependency resolution queue
|
|
56
47
|
pendingDepChecks = new Set();
|
|
57
|
-
|
|
58
|
-
// Two-phase stall detection (like BullMQ)
|
|
59
|
-
// Jobs are added here on first check, confirmed stalled on second check
|
|
48
|
+
// Two-phase stall detection
|
|
60
49
|
stalledCandidates = new Set();
|
|
61
|
-
// Lock-based job ownership tracking
|
|
62
|
-
// Maps jobId to lock info (token, owner, expiration)
|
|
50
|
+
// Lock-based job ownership tracking
|
|
63
51
|
jobLocks = new Map();
|
|
64
|
-
// Client-job tracking for connection-based release
|
|
65
|
-
// When a TCP connection closes, all jobs owned by that client are released
|
|
66
52
|
clientJobs = new Map();
|
|
67
53
|
// Cron scheduler
|
|
68
54
|
cronScheduler;
|
|
@@ -80,18 +66,16 @@ export class QueueManager {
|
|
|
80
66
|
totalFailed: { value: 0n },
|
|
81
67
|
};
|
|
82
68
|
startTime = Date.now();
|
|
83
|
-
// Background
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
stallCheckInterval = null;
|
|
87
|
-
dlqMaintenanceInterval = null;
|
|
88
|
-
lockCheckInterval = null;
|
|
89
|
-
// Queue names cache for O(1) listQueues instead of O(32 * queues)
|
|
69
|
+
// Background task handles
|
|
70
|
+
backgroundTaskHandles = null;
|
|
71
|
+
// Queue names cache
|
|
90
72
|
queueNamesCache = new Set();
|
|
73
|
+
// Context factory
|
|
74
|
+
contextFactory;
|
|
91
75
|
constructor(config = {}) {
|
|
92
76
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
93
77
|
this.storage = config.dataPath ? new SqliteStorage({ path: config.dataPath }) : null;
|
|
94
|
-
// Initialize bounded collections
|
|
78
|
+
// Initialize bounded collections
|
|
95
79
|
this.completedJobs = new BoundedSet(this.config.maxCompletedJobs, (jobId) => {
|
|
96
80
|
this.jobIndex.delete(jobId);
|
|
97
81
|
});
|
|
@@ -114,56 +98,52 @@ export class QueueManager {
|
|
|
114
98
|
this.webhookManager = new WebhookManager();
|
|
115
99
|
this.workerManager = new WorkerManager();
|
|
116
100
|
this.eventsManager = new EventsManager(this.webhookManager);
|
|
101
|
+
// Initialize context factory
|
|
102
|
+
this.contextFactory = new ContextFactory(this.getContextDependencies(), this.getContextCallbacks());
|
|
117
103
|
// Load and start
|
|
118
|
-
|
|
119
|
-
this.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
storage: this.storage,
|
|
125
|
-
shards: this.shards,
|
|
126
|
-
shardLocks: this.shardLocks,
|
|
127
|
-
completedJobs: this.completedJobs,
|
|
128
|
-
customIdMap: this.customIdMap,
|
|
129
|
-
jobIndex: this.jobIndex,
|
|
130
|
-
totalPushed: this.metrics.totalPushed,
|
|
131
|
-
broadcast: this.eventsManager.broadcast.bind(this.eventsManager),
|
|
132
|
-
};
|
|
104
|
+
bgTasks.recover(this.contextFactory.getBackgroundContext());
|
|
105
|
+
if (this.storage) {
|
|
106
|
+
this.cronScheduler.load(this.storage.loadCronJobs());
|
|
107
|
+
}
|
|
108
|
+
this.backgroundTaskHandles = bgTasks.startBackgroundTasks(this.contextFactory.getBackgroundContext(), this.cronScheduler);
|
|
133
109
|
}
|
|
134
|
-
|
|
110
|
+
// ============ Context Dependencies ============
|
|
111
|
+
getContextDependencies() {
|
|
135
112
|
return {
|
|
113
|
+
config: this.config,
|
|
136
114
|
storage: this.storage,
|
|
137
115
|
shards: this.shards,
|
|
138
116
|
shardLocks: this.shardLocks,
|
|
139
117
|
processingShards: this.processingShards,
|
|
140
118
|
processingLocks: this.processingLocks,
|
|
141
119
|
jobIndex: this.jobIndex,
|
|
142
|
-
|
|
143
|
-
|
|
120
|
+
completedJobs: this.completedJobs,
|
|
121
|
+
jobResults: this.jobResults,
|
|
122
|
+
customIdMap: this.customIdMap,
|
|
123
|
+
jobLogs: this.jobLogs,
|
|
124
|
+
jobLocks: this.jobLocks,
|
|
125
|
+
clientJobs: this.clientJobs,
|
|
126
|
+
stalledCandidates: this.stalledCandidates,
|
|
127
|
+
pendingDepChecks: this.pendingDepChecks,
|
|
128
|
+
queueNamesCache: this.queueNamesCache,
|
|
129
|
+
eventsManager: this.eventsManager,
|
|
130
|
+
webhookManager: this.webhookManager,
|
|
131
|
+
metrics: this.metrics,
|
|
132
|
+
startTime: this.startTime,
|
|
133
|
+
maxLogsPerJob: this.maxLogsPerJob,
|
|
144
134
|
};
|
|
145
135
|
}
|
|
146
|
-
|
|
136
|
+
getContextCallbacks() {
|
|
147
137
|
return {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
processingShards: this.processingShards,
|
|
152
|
-
processingLocks: this.processingLocks,
|
|
153
|
-
completedJobs: this.completedJobs,
|
|
154
|
-
jobResults: this.jobResults,
|
|
155
|
-
jobIndex: this.jobIndex,
|
|
156
|
-
totalCompleted: this.metrics.totalCompleted,
|
|
157
|
-
totalFailed: this.metrics.totalFailed,
|
|
158
|
-
broadcast: this.eventsManager.broadcast.bind(this.eventsManager),
|
|
138
|
+
fail: this.fail.bind(this),
|
|
139
|
+
registerQueueName: this.registerQueueName.bind(this),
|
|
140
|
+
unregisterQueueName: this.unregisterQueueName.bind(this),
|
|
159
141
|
onJobCompleted: this.onJobCompleted.bind(this),
|
|
160
142
|
onJobsCompleted: this.onJobsCompleted.bind(this),
|
|
161
|
-
needsBroadcast: this.eventsManager.needsBroadcast.bind(this.eventsManager),
|
|
162
143
|
hasPendingDeps: this.hasPendingDeps.bind(this),
|
|
163
144
|
onRepeat: this.handleRepeat.bind(this),
|
|
164
145
|
};
|
|
165
146
|
}
|
|
166
|
-
/** Handle repeatable job - re-queue with incremented count */
|
|
167
147
|
handleRepeat(job) {
|
|
168
148
|
if (!job.repeat)
|
|
169
149
|
return;
|
|
@@ -189,143 +169,88 @@ export class QueueManager {
|
|
|
189
169
|
},
|
|
190
170
|
});
|
|
191
171
|
}
|
|
192
|
-
getJobMgmtContext() {
|
|
193
|
-
return {
|
|
194
|
-
storage: this.storage,
|
|
195
|
-
shards: this.shards,
|
|
196
|
-
shardLocks: this.shardLocks,
|
|
197
|
-
processingShards: this.processingShards,
|
|
198
|
-
processingLocks: this.processingLocks,
|
|
199
|
-
jobIndex: this.jobIndex,
|
|
200
|
-
webhookManager: this.webhookManager,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
getQueryContext() {
|
|
204
|
-
return {
|
|
205
|
-
storage: this.storage,
|
|
206
|
-
shards: this.shards,
|
|
207
|
-
shardLocks: this.shardLocks,
|
|
208
|
-
processingShards: this.processingShards,
|
|
209
|
-
processingLocks: this.processingLocks,
|
|
210
|
-
jobIndex: this.jobIndex,
|
|
211
|
-
completedJobs: this.completedJobs,
|
|
212
|
-
jobResults: this.jobResults,
|
|
213
|
-
customIdMap: this.customIdMap,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
getDlqContext() {
|
|
217
|
-
return {
|
|
218
|
-
shards: this.shards,
|
|
219
|
-
jobIndex: this.jobIndex,
|
|
220
|
-
storage: this.storage,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
172
|
// ============ Core Operations ============
|
|
224
173
|
async push(queue, input) {
|
|
225
|
-
// Register queue name in cache for O(1) listQueues
|
|
226
174
|
this.registerQueueName(queue);
|
|
227
|
-
return pushJob(queue, input, this.getPushContext());
|
|
175
|
+
return pushJob(queue, input, this.contextFactory.getPushContext());
|
|
228
176
|
}
|
|
229
177
|
async pushBatch(queue, inputs) {
|
|
230
|
-
// Register queue name in cache for O(1) listQueues
|
|
231
178
|
this.registerQueueName(queue);
|
|
232
|
-
return pushJobBatch(queue, inputs, this.getPushContext());
|
|
179
|
+
return pushJobBatch(queue, inputs, this.contextFactory.getPushContext());
|
|
233
180
|
}
|
|
234
181
|
async pull(queue, timeoutMs = 0) {
|
|
235
|
-
return pullJob(queue, timeoutMs, this.getPullContext());
|
|
182
|
+
return pullJob(queue, timeoutMs, this.contextFactory.getPullContext());
|
|
236
183
|
}
|
|
237
|
-
/**
|
|
238
|
-
* Pull a job and create a lock for it (BullMQ-style).
|
|
239
|
-
* Returns both the job and its lock token for ownership verification.
|
|
240
|
-
*/
|
|
241
184
|
async pullWithLock(queue, owner, timeoutMs = 0, lockTtl = DEFAULT_LOCK_TTL) {
|
|
242
|
-
const job = await pullJob(queue, timeoutMs, this.getPullContext());
|
|
185
|
+
const job = await pullJob(queue, timeoutMs, this.contextFactory.getPullContext());
|
|
243
186
|
if (!job)
|
|
244
187
|
return { job: null, token: null };
|
|
245
|
-
const token =
|
|
188
|
+
const token = lockMgr.createLock(job.id, owner, this.contextFactory.getLockContext(), lockTtl);
|
|
246
189
|
return { job, token };
|
|
247
190
|
}
|
|
248
|
-
/** Pull multiple jobs in single lock acquisition - O(1) instead of O(n) locks */
|
|
249
191
|
async pullBatch(queue, count, timeoutMs = 0) {
|
|
250
|
-
return pullJobBatch(queue, count, timeoutMs, this.getPullContext());
|
|
192
|
+
return pullJobBatch(queue, count, timeoutMs, this.contextFactory.getPullContext());
|
|
251
193
|
}
|
|
252
|
-
/**
|
|
253
|
-
* Pull multiple jobs and create locks for them (BullMQ-style).
|
|
254
|
-
* Returns both jobs and their lock tokens for ownership verification.
|
|
255
|
-
*/
|
|
256
194
|
async pullBatchWithLock(queue, count, owner, timeoutMs = 0, lockTtl = DEFAULT_LOCK_TTL) {
|
|
257
|
-
const jobs = await pullJobBatch(queue, count, timeoutMs, this.getPullContext());
|
|
195
|
+
const jobs = await pullJobBatch(queue, count, timeoutMs, this.contextFactory.getPullContext());
|
|
258
196
|
const tokens = [];
|
|
259
197
|
for (const job of jobs) {
|
|
260
|
-
const token =
|
|
198
|
+
const token = lockMgr.createLock(job.id, owner, this.contextFactory.getLockContext(), lockTtl);
|
|
261
199
|
tokens.push(token ?? '');
|
|
262
200
|
}
|
|
263
201
|
return { jobs, tokens };
|
|
264
202
|
}
|
|
265
203
|
async ack(jobId, result, token) {
|
|
266
|
-
|
|
267
|
-
if (token && !this.verifyLock(jobId, token)) {
|
|
204
|
+
if (token && !lockMgr.verifyLock(jobId, token, this.contextFactory.getLockContext())) {
|
|
268
205
|
throw new Error(`Invalid or expired lock token for job ${jobId}`);
|
|
269
206
|
}
|
|
270
|
-
await ackJob(jobId, result, this.getAckContext());
|
|
271
|
-
|
|
272
|
-
this.releaseLock(jobId, token);
|
|
207
|
+
await ackJob(jobId, result, this.contextFactory.getAckContext());
|
|
208
|
+
lockMgr.releaseLock(jobId, this.contextFactory.getLockContext(), token);
|
|
273
209
|
}
|
|
274
|
-
/** Acknowledge multiple jobs in parallel with Promise.all */
|
|
275
210
|
async ackBatch(jobIds, tokens) {
|
|
276
|
-
|
|
211
|
+
const lockCtx = this.contextFactory.getLockContext();
|
|
277
212
|
if (tokens?.length === jobIds.length) {
|
|
278
213
|
for (let i = 0; i < jobIds.length; i++) {
|
|
279
214
|
const t = tokens[i];
|
|
280
|
-
if (t && !
|
|
215
|
+
if (t && !lockMgr.verifyLock(jobIds[i], t, lockCtx)) {
|
|
281
216
|
throw new Error(`Invalid or expired lock token for job ${jobIds[i]}`);
|
|
282
217
|
}
|
|
283
218
|
}
|
|
284
219
|
}
|
|
285
|
-
await ackJobBatch(jobIds, this.getAckContext());
|
|
286
|
-
// Release locks after successful ack
|
|
220
|
+
await ackJobBatch(jobIds, this.contextFactory.getAckContext());
|
|
287
221
|
if (tokens) {
|
|
288
222
|
for (let i = 0; i < jobIds.length; i++) {
|
|
289
|
-
|
|
223
|
+
lockMgr.releaseLock(jobIds[i], lockCtx, tokens[i]);
|
|
290
224
|
}
|
|
291
225
|
}
|
|
292
226
|
}
|
|
293
|
-
/** Acknowledge multiple jobs with individual results - batch optimized */
|
|
294
227
|
async ackBatchWithResults(items) {
|
|
295
|
-
|
|
228
|
+
const lockCtx = this.contextFactory.getLockContext();
|
|
296
229
|
for (const item of items) {
|
|
297
|
-
if (item.token && !
|
|
230
|
+
if (item.token && !lockMgr.verifyLock(item.id, item.token, lockCtx)) {
|
|
298
231
|
throw new Error(`Invalid or expired lock token for job ${item.id}`);
|
|
299
232
|
}
|
|
300
233
|
}
|
|
301
|
-
await ackJobBatchWithResults(items, this.getAckContext());
|
|
302
|
-
// Release locks after successful ack
|
|
234
|
+
await ackJobBatchWithResults(items, this.contextFactory.getAckContext());
|
|
303
235
|
for (const item of items) {
|
|
304
|
-
|
|
236
|
+
lockMgr.releaseLock(item.id, lockCtx, item.token);
|
|
305
237
|
}
|
|
306
238
|
}
|
|
307
239
|
async fail(jobId, error, token) {
|
|
308
|
-
|
|
309
|
-
if (token && !
|
|
240
|
+
const lockCtx = this.contextFactory.getLockContext();
|
|
241
|
+
if (token && !lockMgr.verifyLock(jobId, token, lockCtx)) {
|
|
310
242
|
throw new Error(`Invalid or expired lock token for job ${jobId}`);
|
|
311
243
|
}
|
|
312
|
-
await failJob(jobId, error, this.getAckContext());
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Update job heartbeat for stall detection (single job).
|
|
318
|
-
* If token is provided, also renews the lock.
|
|
319
|
-
*/
|
|
244
|
+
await failJob(jobId, error, this.contextFactory.getAckContext());
|
|
245
|
+
lockMgr.releaseLock(jobId, lockCtx, token);
|
|
246
|
+
}
|
|
320
247
|
jobHeartbeat(jobId, token) {
|
|
321
248
|
const loc = this.jobIndex.get(jobId);
|
|
322
249
|
if (loc?.type !== 'processing')
|
|
323
250
|
return false;
|
|
324
|
-
// If token provided, renew lock (which also updates heartbeat)
|
|
325
251
|
if (token) {
|
|
326
|
-
return
|
|
252
|
+
return lockMgr.renewJobLock(jobId, token, this.contextFactory.getLockContext());
|
|
327
253
|
}
|
|
328
|
-
// Legacy mode: just update heartbeat without token verification
|
|
329
254
|
const processing = this.processingShards[loc.shardIdx];
|
|
330
255
|
const job = processing.get(jobId);
|
|
331
256
|
if (job) {
|
|
@@ -334,435 +259,112 @@ export class QueueManager {
|
|
|
334
259
|
}
|
|
335
260
|
return false;
|
|
336
261
|
}
|
|
337
|
-
/**
|
|
338
|
-
* Update job heartbeat for multiple jobs (batch).
|
|
339
|
-
* If tokens are provided, also renews the locks.
|
|
340
|
-
*/
|
|
341
262
|
jobHeartbeatBatch(jobIds, tokens) {
|
|
342
263
|
let count = 0;
|
|
343
264
|
for (let i = 0; i < jobIds.length; i++) {
|
|
344
|
-
|
|
345
|
-
if (this.jobHeartbeat(jobIds[i], token))
|
|
265
|
+
if (this.jobHeartbeat(jobIds[i], tokens?.[i]))
|
|
346
266
|
count++;
|
|
347
267
|
}
|
|
348
268
|
return count;
|
|
349
269
|
}
|
|
350
|
-
// ============ Lock Management
|
|
351
|
-
/**
|
|
352
|
-
* Create a lock for a job when it's pulled for processing.
|
|
353
|
-
* @returns The lock token, or null if job not in processing
|
|
354
|
-
*/
|
|
270
|
+
// ============ Lock Management ============
|
|
355
271
|
createLock(jobId, owner, ttl = DEFAULT_LOCK_TTL) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return null;
|
|
359
|
-
// Check if lock already exists (shouldn't happen, but defensive)
|
|
360
|
-
if (this.jobLocks.has(jobId)) {
|
|
361
|
-
queueLog.warn('Lock already exists for job', { jobId: String(jobId), owner });
|
|
362
|
-
return null;
|
|
363
|
-
}
|
|
364
|
-
const lock = createJobLock(jobId, owner, ttl);
|
|
365
|
-
this.jobLocks.set(jobId, lock);
|
|
366
|
-
return lock.token;
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Verify that a token is valid for a job.
|
|
370
|
-
* @returns true if token matches the active lock
|
|
371
|
-
*/
|
|
272
|
+
return lockMgr.createLock(jobId, owner, this.contextFactory.getLockContext(), ttl);
|
|
273
|
+
}
|
|
372
274
|
verifyLock(jobId, token) {
|
|
373
|
-
|
|
374
|
-
if (!lock)
|
|
375
|
-
return false;
|
|
376
|
-
if (lock.token !== token)
|
|
377
|
-
return false;
|
|
378
|
-
if (isLockExpired(lock))
|
|
379
|
-
return false;
|
|
380
|
-
return true;
|
|
275
|
+
return lockMgr.verifyLock(jobId, token, this.contextFactory.getLockContext());
|
|
381
276
|
}
|
|
382
|
-
/**
|
|
383
|
-
* Renew a lock with the given token.
|
|
384
|
-
* @returns true if renewal succeeded, false if token invalid or lock expired
|
|
385
|
-
*/
|
|
386
277
|
renewJobLock(jobId, token, newTtl) {
|
|
387
|
-
|
|
388
|
-
if (!lock)
|
|
389
|
-
return false;
|
|
390
|
-
if (lock.token !== token)
|
|
391
|
-
return false;
|
|
392
|
-
if (isLockExpired(lock)) {
|
|
393
|
-
// Lock already expired, remove it
|
|
394
|
-
this.jobLocks.delete(jobId);
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
renewLock(lock, newTtl);
|
|
398
|
-
// Also update lastHeartbeat on the job (for legacy stall detection compatibility)
|
|
399
|
-
const loc = this.jobIndex.get(jobId);
|
|
400
|
-
if (loc?.type === 'processing') {
|
|
401
|
-
const job = this.processingShards[loc.shardIdx].get(jobId);
|
|
402
|
-
if (job)
|
|
403
|
-
job.lastHeartbeat = Date.now();
|
|
404
|
-
}
|
|
405
|
-
return true;
|
|
278
|
+
return lockMgr.renewJobLock(jobId, token, this.contextFactory.getLockContext(), newTtl);
|
|
406
279
|
}
|
|
407
|
-
/**
|
|
408
|
-
* Renew locks for multiple jobs (batch operation).
|
|
409
|
-
* @returns Array of jobIds that were successfully renewed
|
|
410
|
-
*/
|
|
411
280
|
renewJobLockBatch(items) {
|
|
412
|
-
|
|
413
|
-
for (const item of items) {
|
|
414
|
-
if (this.renewJobLock(item.id, item.token, item.ttl)) {
|
|
415
|
-
renewed.push(String(item.id));
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return renewed;
|
|
281
|
+
return lockMgr.renewJobLockBatch(items, this.contextFactory.getLockContext());
|
|
419
282
|
}
|
|
420
|
-
/**
|
|
421
|
-
* Release a lock when job is completed or failed.
|
|
422
|
-
* Should be called by ACK/FAIL operations.
|
|
423
|
-
*/
|
|
424
283
|
releaseLock(jobId, token) {
|
|
425
|
-
|
|
426
|
-
if (!lock)
|
|
427
|
-
return true; // No lock to release
|
|
428
|
-
// If token provided, verify it matches
|
|
429
|
-
if (token && lock.token !== token) {
|
|
430
|
-
queueLog.warn('Token mismatch on lock release', {
|
|
431
|
-
jobId: String(jobId),
|
|
432
|
-
expected: lock.token.substring(0, 8),
|
|
433
|
-
got: token.substring(0, 8),
|
|
434
|
-
});
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
this.jobLocks.delete(jobId);
|
|
438
|
-
return true;
|
|
284
|
+
return lockMgr.releaseLock(jobId, this.contextFactory.getLockContext(), token);
|
|
439
285
|
}
|
|
440
|
-
/**
|
|
441
|
-
* Get lock info for a job (for debugging/monitoring).
|
|
442
|
-
*/
|
|
443
286
|
getLockInfo(jobId) {
|
|
444
|
-
return this.
|
|
287
|
+
return lockMgr.getLockInfo(jobId, this.contextFactory.getLockContext());
|
|
445
288
|
}
|
|
446
289
|
// ============ Client-Job Tracking ============
|
|
447
|
-
/**
|
|
448
|
-
* Register a job as owned by a client (called on PULL).
|
|
449
|
-
*/
|
|
450
290
|
registerClientJob(clientId, jobId) {
|
|
451
|
-
|
|
452
|
-
if (!jobs) {
|
|
453
|
-
jobs = new Set();
|
|
454
|
-
this.clientJobs.set(clientId, jobs);
|
|
455
|
-
}
|
|
456
|
-
jobs.add(jobId);
|
|
291
|
+
lockMgr.registerClientJob(clientId, jobId, this.contextFactory.getLockContext());
|
|
457
292
|
}
|
|
458
|
-
/**
|
|
459
|
-
* Unregister a job from a client (called on ACK/FAIL).
|
|
460
|
-
*/
|
|
461
293
|
unregisterClientJob(clientId, jobId) {
|
|
462
|
-
|
|
463
|
-
return;
|
|
464
|
-
const jobs = this.clientJobs.get(clientId);
|
|
465
|
-
if (jobs) {
|
|
466
|
-
jobs.delete(jobId);
|
|
467
|
-
if (jobs.size === 0) {
|
|
468
|
-
this.clientJobs.delete(clientId);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
294
|
+
lockMgr.unregisterClientJob(clientId, jobId, this.contextFactory.getLockContext());
|
|
471
295
|
}
|
|
472
|
-
/**
|
|
473
|
-
* Release all jobs owned by a client back to queue (called on TCP disconnect).
|
|
474
|
-
* Returns the number of jobs released.
|
|
475
|
-
*/
|
|
476
296
|
releaseClientJobs(clientId) {
|
|
477
|
-
|
|
478
|
-
if (!jobs || jobs.size === 0) {
|
|
479
|
-
this.clientJobs.delete(clientId);
|
|
480
|
-
return 0;
|
|
481
|
-
}
|
|
482
|
-
let released = 0;
|
|
483
|
-
const now = Date.now();
|
|
484
|
-
for (const jobId of jobs) {
|
|
485
|
-
const loc = this.jobIndex.get(jobId);
|
|
486
|
-
if (loc?.type !== 'processing')
|
|
487
|
-
continue;
|
|
488
|
-
const procIdx = loc.shardIdx;
|
|
489
|
-
const job = this.processingShards[procIdx].get(jobId);
|
|
490
|
-
if (!job)
|
|
491
|
-
continue;
|
|
492
|
-
// Remove from processing
|
|
493
|
-
this.processingShards[procIdx].delete(jobId);
|
|
494
|
-
// Release lock if exists
|
|
495
|
-
this.jobLocks.delete(jobId);
|
|
496
|
-
// Release concurrency
|
|
497
|
-
const idx = shardIndex(job.queue);
|
|
498
|
-
const shard = this.shards[idx];
|
|
499
|
-
shard.releaseConcurrency(job.queue);
|
|
500
|
-
// Release group if active
|
|
501
|
-
if (job.groupId) {
|
|
502
|
-
shard.releaseGroup(job.queue, job.groupId);
|
|
503
|
-
}
|
|
504
|
-
// Reset job state for retry
|
|
505
|
-
job.startedAt = null;
|
|
506
|
-
job.lastHeartbeat = now;
|
|
507
|
-
// Re-queue the job
|
|
508
|
-
shard.getQueue(job.queue).push(job);
|
|
509
|
-
const isDelayed = job.runAt > now;
|
|
510
|
-
shard.incrementQueued(jobId, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
511
|
-
this.jobIndex.set(jobId, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
512
|
-
released++;
|
|
513
|
-
}
|
|
514
|
-
// Clear client tracking
|
|
515
|
-
this.clientJobs.delete(clientId);
|
|
516
|
-
if (released > 0) {
|
|
517
|
-
queueLog.info('Released client jobs', { clientId: clientId.substring(0, 8), released });
|
|
518
|
-
}
|
|
519
|
-
return released;
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* Check and handle expired locks.
|
|
523
|
-
* Jobs with expired locks are requeued for retry.
|
|
524
|
-
*/
|
|
525
|
-
checkExpiredLocks() {
|
|
526
|
-
const now = Date.now();
|
|
527
|
-
const expired = [];
|
|
528
|
-
for (const [jobId, lock] of this.jobLocks) {
|
|
529
|
-
if (isLockExpired(lock, now)) {
|
|
530
|
-
expired.push({ jobId, lock });
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
for (const { jobId, lock } of expired) {
|
|
534
|
-
const procIdx = processingShardIndex(String(jobId));
|
|
535
|
-
const job = this.processingShards[procIdx].get(jobId);
|
|
536
|
-
if (job) {
|
|
537
|
-
const idx = shardIndex(job.queue);
|
|
538
|
-
const shard = this.shards[idx];
|
|
539
|
-
const queue = shard.getQueue(job.queue);
|
|
540
|
-
// Remove from processing
|
|
541
|
-
this.processingShards[procIdx].delete(jobId);
|
|
542
|
-
// Increment attempts and reset state
|
|
543
|
-
job.attempts++;
|
|
544
|
-
job.startedAt = null;
|
|
545
|
-
job.lastHeartbeat = now;
|
|
546
|
-
job.stallCount++;
|
|
547
|
-
// Check if max stalls exceeded
|
|
548
|
-
const stallConfig = shard.getStallConfig(job.queue);
|
|
549
|
-
if (stallConfig.maxStalls > 0 && job.stallCount >= stallConfig.maxStalls) {
|
|
550
|
-
// Move to DLQ using shard's addToDlq method
|
|
551
|
-
shard.addToDlq(job, "stalled" /* FailureReason.Stalled */, `Lock expired after ${lock.renewalCount} renewals`);
|
|
552
|
-
this.jobIndex.set(jobId, { type: 'dlq', queueName: job.queue });
|
|
553
|
-
queueLog.warn('Job moved to DLQ due to lock expiration', {
|
|
554
|
-
jobId: String(jobId),
|
|
555
|
-
queue: job.queue,
|
|
556
|
-
owner: lock.owner,
|
|
557
|
-
renewals: lock.renewalCount,
|
|
558
|
-
stallCount: job.stallCount,
|
|
559
|
-
});
|
|
560
|
-
this.eventsManager.broadcast({
|
|
561
|
-
eventType: "failed" /* EventType.Failed */,
|
|
562
|
-
jobId,
|
|
563
|
-
queue: job.queue,
|
|
564
|
-
timestamp: now,
|
|
565
|
-
error: 'Lock expired (max stalls reached)',
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
// Requeue for retry (always push - priority queue handles ordering)
|
|
570
|
-
queue.push(job);
|
|
571
|
-
this.jobIndex.set(jobId, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
572
|
-
queueLog.info('Job requeued due to lock expiration', {
|
|
573
|
-
jobId: String(jobId),
|
|
574
|
-
queue: job.queue,
|
|
575
|
-
owner: lock.owner,
|
|
576
|
-
renewals: lock.renewalCount,
|
|
577
|
-
attempt: job.attempts,
|
|
578
|
-
});
|
|
579
|
-
this.eventsManager.broadcast({
|
|
580
|
-
eventType: "stalled" /* EventType.Stalled */,
|
|
581
|
-
jobId,
|
|
582
|
-
queue: job.queue,
|
|
583
|
-
timestamp: now,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
// Remove the expired lock
|
|
588
|
-
this.jobLocks.delete(jobId);
|
|
589
|
-
}
|
|
590
|
-
if (expired.length > 0) {
|
|
591
|
-
queueLog.info('Processed expired locks', { count: expired.length });
|
|
592
|
-
}
|
|
297
|
+
return lockMgr.releaseClientJobs(clientId, this.contextFactory.getLockContext());
|
|
593
298
|
}
|
|
594
|
-
// ============ Query Operations
|
|
299
|
+
// ============ Query Operations ============
|
|
595
300
|
async getJob(jobId) {
|
|
596
|
-
return queryOps.getJob(jobId, this.getQueryContext());
|
|
301
|
+
return queryOps.getJob(jobId, this.contextFactory.getQueryContext());
|
|
597
302
|
}
|
|
598
303
|
getResult(jobId) {
|
|
599
|
-
return queryOps.getJobResult(jobId, this.getQueryContext());
|
|
304
|
+
return queryOps.getJobResult(jobId, this.contextFactory.getQueryContext());
|
|
600
305
|
}
|
|
601
306
|
getJobByCustomId(customId) {
|
|
602
|
-
return queryOps.getJobByCustomId(customId, this.getQueryContext());
|
|
307
|
+
return queryOps.getJobByCustomId(customId, this.contextFactory.getQueryContext());
|
|
603
308
|
}
|
|
604
309
|
getProgress(jobId) {
|
|
605
|
-
return queryOps.getJobProgress(jobId, this.getQueryContext());
|
|
310
|
+
return queryOps.getJobProgress(jobId, this.contextFactory.getQueryContext());
|
|
606
311
|
}
|
|
607
312
|
count(queue) {
|
|
608
|
-
return queueControl.getQueueCount(queue,
|
|
313
|
+
return queueControl.getQueueCount(queue, this.contextFactory.getQueueControlContext());
|
|
609
314
|
}
|
|
610
|
-
// ============ Queue Control
|
|
315
|
+
// ============ Queue Control ============
|
|
611
316
|
pause(queue) {
|
|
612
|
-
queueControl.pauseQueue(queue,
|
|
317
|
+
queueControl.pauseQueue(queue, this.contextFactory.getQueueControlContext());
|
|
613
318
|
}
|
|
614
319
|
resume(queue) {
|
|
615
|
-
queueControl.resumeQueue(queue,
|
|
320
|
+
queueControl.resumeQueue(queue, this.contextFactory.getQueueControlContext());
|
|
616
321
|
}
|
|
617
322
|
isPaused(queue) {
|
|
618
|
-
return queueControl.isQueuePaused(queue,
|
|
323
|
+
return queueControl.isQueuePaused(queue, this.contextFactory.getQueueControlContext());
|
|
619
324
|
}
|
|
620
325
|
drain(queue) {
|
|
621
|
-
return queueControl.drainQueue(queue,
|
|
326
|
+
return queueControl.drainQueue(queue, this.contextFactory.getQueueControlContext());
|
|
622
327
|
}
|
|
623
328
|
obliterate(queue) {
|
|
624
|
-
queueControl.obliterateQueue(queue,
|
|
625
|
-
// Remove from cache
|
|
329
|
+
queueControl.obliterateQueue(queue, this.contextFactory.getQueueControlContext());
|
|
626
330
|
this.unregisterQueueName(queue);
|
|
627
331
|
}
|
|
628
332
|
listQueues() {
|
|
629
|
-
// O(1) using cache instead of O(32 * queues) iterating all shards
|
|
630
333
|
return Array.from(this.queueNamesCache);
|
|
631
334
|
}
|
|
632
|
-
/** Register queue name in cache - called when first job is pushed */
|
|
633
335
|
registerQueueName(queue) {
|
|
634
336
|
this.queueNamesCache.add(queue);
|
|
635
337
|
}
|
|
636
|
-
/** Unregister queue name from cache - called on obliterate */
|
|
637
338
|
unregisterQueueName(queue) {
|
|
638
339
|
this.queueNamesCache.delete(queue);
|
|
639
340
|
}
|
|
640
341
|
clean(queue, graceMs, state, limit) {
|
|
641
|
-
return queueControl.cleanQueue(queue, graceMs,
|
|
342
|
+
return queueControl.cleanQueue(queue, graceMs, this.contextFactory.getQueueControlContext(), state, limit);
|
|
642
343
|
}
|
|
643
|
-
/** Get job counts grouped by priority for a queue */
|
|
644
344
|
getCountsPerPriority(queue) {
|
|
645
345
|
const idx = shardIndex(queue);
|
|
646
346
|
const counts = this.shards[idx].getCountsPerPriority(queue);
|
|
647
347
|
return Object.fromEntries(counts);
|
|
648
348
|
}
|
|
649
|
-
/**
|
|
650
|
-
* Get jobs with filtering and pagination
|
|
651
|
-
* @param queue - Queue name
|
|
652
|
-
* @param options - Filter options
|
|
653
|
-
* @returns Array of jobs matching the criteria
|
|
654
|
-
*/
|
|
655
349
|
getJobs(queue, options = {}) {
|
|
656
|
-
const { state, start = 0, end = 100, asc = true } = options;
|
|
657
350
|
const idx = shardIndex(queue);
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (!state || state === 'waiting') {
|
|
663
|
-
const queueJobs = shard.getQueue(queue).values();
|
|
664
|
-
jobs.push(...queueJobs.filter((j) => j.runAt <= now));
|
|
665
|
-
}
|
|
666
|
-
if (!state || state === 'delayed') {
|
|
667
|
-
const queueJobs = shard.getQueue(queue).values();
|
|
668
|
-
jobs.push(...queueJobs.filter((j) => j.runAt > now));
|
|
669
|
-
}
|
|
670
|
-
if (!state || state === 'active') {
|
|
671
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
672
|
-
for (const job of this.processingShards[i].values()) {
|
|
673
|
-
if (job.queue === queue) {
|
|
674
|
-
jobs.push(job);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
if (!state || state === 'failed') {
|
|
680
|
-
const dlqJobs = shard.getDlq(queue);
|
|
681
|
-
jobs.push(...dlqJobs);
|
|
682
|
-
}
|
|
683
|
-
// For completed jobs, check completed jobs set
|
|
684
|
-
if (state === 'completed') {
|
|
685
|
-
// Iterate completedJobs and filter by queue
|
|
686
|
-
// Note: This is not efficient for large sets, but provides the data
|
|
687
|
-
for (const jobId of this.completedJobs) {
|
|
688
|
-
const result = this.jobResults.get(jobId);
|
|
689
|
-
if (result) {
|
|
690
|
-
// We don't have the full job object for completed jobs in memory
|
|
691
|
-
// Just count them or return IDs - for now skip completed state
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
// Completed jobs are stored in SQLite, would need storage access
|
|
695
|
-
// For now, return empty for completed state if not in DLQ
|
|
696
|
-
}
|
|
697
|
-
// Sort by createdAt
|
|
698
|
-
jobs.sort((a, b) => (asc ? a.createdAt - b.createdAt : b.createdAt - a.createdAt));
|
|
699
|
-
// Apply pagination
|
|
700
|
-
return jobs.slice(start, end);
|
|
351
|
+
return queryOps.getJobs(queue, idx, options, {
|
|
352
|
+
...this.contextFactory.getQueryContext(),
|
|
353
|
+
shardCount: SHARD_COUNT,
|
|
354
|
+
});
|
|
701
355
|
}
|
|
702
|
-
// ============ DLQ Operations
|
|
356
|
+
// ============ DLQ Operations ============
|
|
703
357
|
getDlq(queue, count) {
|
|
704
|
-
return dlqOps.getDlqJobs(queue, this.getDlqContext(), count);
|
|
358
|
+
return dlqOps.getDlqJobs(queue, this.contextFactory.getDlqContext(), count);
|
|
705
359
|
}
|
|
706
360
|
retryDlq(queue, jobId) {
|
|
707
|
-
return dlqOps.retryDlqJobs(queue, this.getDlqContext(), jobId);
|
|
361
|
+
return dlqOps.retryDlqJobs(queue, this.contextFactory.getDlqContext(), jobId);
|
|
708
362
|
}
|
|
709
363
|
purgeDlq(queue) {
|
|
710
|
-
return dlqOps.purgeDlqJobs(queue, this.getDlqContext());
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Retry a completed job by re-queueing it
|
|
714
|
-
* @param queue - Queue name
|
|
715
|
-
* @param jobId - Specific job ID to retry (optional - retries all if not specified)
|
|
716
|
-
* @returns Number of jobs retried
|
|
717
|
-
*/
|
|
718
|
-
retryCompleted(queue, jobId) {
|
|
719
|
-
if (jobId) {
|
|
720
|
-
// Check if job is in completedJobs set
|
|
721
|
-
if (!this.completedJobs.has(jobId)) {
|
|
722
|
-
return 0;
|
|
723
|
-
}
|
|
724
|
-
// Get job from storage
|
|
725
|
-
const job = this.storage?.getJob(jobId);
|
|
726
|
-
if (job?.queue !== queue) {
|
|
727
|
-
return 0;
|
|
728
|
-
}
|
|
729
|
-
return this.requeueCompletedJob(job);
|
|
730
|
-
}
|
|
731
|
-
// Retry all completed jobs for queue
|
|
732
|
-
let count = 0;
|
|
733
|
-
for (const id of this.completedJobs) {
|
|
734
|
-
const job = this.storage?.getJob(id);
|
|
735
|
-
if (job?.queue === queue) {
|
|
736
|
-
count += this.requeueCompletedJob(job);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
return count;
|
|
364
|
+
return dlqOps.purgeDlqJobs(queue, this.contextFactory.getDlqContext());
|
|
740
365
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
*/
|
|
744
|
-
requeueCompletedJob(job) {
|
|
745
|
-
// Reset job state
|
|
746
|
-
job.attempts = 0;
|
|
747
|
-
job.startedAt = null;
|
|
748
|
-
job.completedAt = null;
|
|
749
|
-
job.runAt = Date.now();
|
|
750
|
-
job.progress = 0;
|
|
751
|
-
// Re-queue
|
|
752
|
-
const idx = shardIndex(job.queue);
|
|
753
|
-
const shard = this.shards[idx];
|
|
754
|
-
shard.getQueue(job.queue).push(job);
|
|
755
|
-
shard.incrementQueued(job.id, false, job.createdAt, job.queue, job.runAt);
|
|
756
|
-
// Update index
|
|
757
|
-
this.jobIndex.set(job.id, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
758
|
-
// Cleanup completed tracking
|
|
759
|
-
this.completedJobs.delete(job.id);
|
|
760
|
-
this.jobResults.delete(job.id);
|
|
761
|
-
// Update storage
|
|
762
|
-
this.storage?.updateForRetry(job);
|
|
763
|
-
// Notify
|
|
764
|
-
shard.notify();
|
|
765
|
-
return 1;
|
|
366
|
+
retryCompleted(queue, jobId) {
|
|
367
|
+
return dlqOps.retryCompletedJobs(queue, this.contextFactory.getRetryCompletedContext(), jobId);
|
|
766
368
|
}
|
|
767
369
|
// ============ Rate Limiting ============
|
|
768
370
|
setRateLimit(queue, limit) {
|
|
@@ -777,49 +379,37 @@ export class QueueManager {
|
|
|
777
379
|
clearConcurrency(queue) {
|
|
778
380
|
this.shards[shardIndex(queue)].clearConcurrency(queue);
|
|
779
381
|
}
|
|
780
|
-
// ============ Job Management
|
|
382
|
+
// ============ Job Management ============
|
|
781
383
|
async cancel(jobId) {
|
|
782
|
-
return jobMgmt.cancelJob(jobId, this.getJobMgmtContext());
|
|
384
|
+
return jobMgmt.cancelJob(jobId, this.contextFactory.getJobMgmtContext());
|
|
783
385
|
}
|
|
784
386
|
async updateProgress(jobId, progress, message) {
|
|
785
|
-
return jobMgmt.updateJobProgress(jobId, progress, this.getJobMgmtContext(), message);
|
|
387
|
+
return jobMgmt.updateJobProgress(jobId, progress, this.contextFactory.getJobMgmtContext(), message);
|
|
786
388
|
}
|
|
787
389
|
async updateJobData(jobId, data) {
|
|
788
|
-
return jobMgmt.updateJobData(jobId, data, this.getJobMgmtContext());
|
|
390
|
+
return jobMgmt.updateJobData(jobId, data, this.contextFactory.getJobMgmtContext());
|
|
789
391
|
}
|
|
790
392
|
async changePriority(jobId, priority) {
|
|
791
|
-
return jobMgmt.changeJobPriority(jobId, priority, this.getJobMgmtContext());
|
|
393
|
+
return jobMgmt.changeJobPriority(jobId, priority, this.contextFactory.getJobMgmtContext());
|
|
792
394
|
}
|
|
793
395
|
async promote(jobId) {
|
|
794
|
-
return jobMgmt.promoteJob(jobId, this.getJobMgmtContext());
|
|
396
|
+
return jobMgmt.promoteJob(jobId, this.contextFactory.getJobMgmtContext());
|
|
795
397
|
}
|
|
796
398
|
async moveToDelayed(jobId, delay) {
|
|
797
|
-
return jobMgmt.moveJobToDelayed(jobId, delay, this.getJobMgmtContext());
|
|
399
|
+
return jobMgmt.moveJobToDelayed(jobId, delay, this.contextFactory.getJobMgmtContext());
|
|
798
400
|
}
|
|
799
401
|
async discard(jobId) {
|
|
800
|
-
return jobMgmt.discardJob(jobId, this.getJobMgmtContext());
|
|
402
|
+
return jobMgmt.discardJob(jobId, this.contextFactory.getJobMgmtContext());
|
|
801
403
|
}
|
|
802
|
-
// ============ Job Logs
|
|
404
|
+
// ============ Job Logs ============
|
|
803
405
|
addLog(jobId, message, level = 'info') {
|
|
804
|
-
return logsOps.addJobLog(jobId, message,
|
|
805
|
-
jobIndex: this.jobIndex,
|
|
806
|
-
jobLogs: this.jobLogs,
|
|
807
|
-
maxLogsPerJob: this.maxLogsPerJob,
|
|
808
|
-
}, level);
|
|
406
|
+
return logsOps.addJobLog(jobId, message, this.contextFactory.getLogsContext(), level);
|
|
809
407
|
}
|
|
810
408
|
getLogs(jobId) {
|
|
811
|
-
return logsOps.getJobLogs(jobId,
|
|
812
|
-
jobIndex: this.jobIndex,
|
|
813
|
-
jobLogs: this.jobLogs,
|
|
814
|
-
maxLogsPerJob: this.maxLogsPerJob,
|
|
815
|
-
});
|
|
409
|
+
return logsOps.getJobLogs(jobId, this.contextFactory.getLogsContext());
|
|
816
410
|
}
|
|
817
411
|
clearLogs(jobId) {
|
|
818
|
-
logsOps.clearJobLogs(jobId,
|
|
819
|
-
jobIndex: this.jobIndex,
|
|
820
|
-
jobLogs: this.jobLogs,
|
|
821
|
-
maxLogsPerJob: this.maxLogsPerJob,
|
|
822
|
-
});
|
|
412
|
+
logsOps.clearJobLogs(jobId, this.contextFactory.getLogsContext());
|
|
823
413
|
}
|
|
824
414
|
// ============ Metrics ============
|
|
825
415
|
getPrometheusMetrics() {
|
|
@@ -847,515 +437,51 @@ export class QueueManager {
|
|
|
847
437
|
subscribe(callback) {
|
|
848
438
|
return this.eventsManager.subscribe(callback);
|
|
849
439
|
}
|
|
850
|
-
/** Wait for job completion - event-driven, no polling */
|
|
851
440
|
waitForJobCompletion(jobId, timeoutMs) {
|
|
852
441
|
return this.eventsManager.waitForJobCompletion(jobId, timeoutMs);
|
|
853
442
|
}
|
|
854
|
-
// ============ Internal State Access
|
|
855
|
-
/** Get job index for dependency validation */
|
|
443
|
+
// ============ Internal State Access ============
|
|
856
444
|
getJobIndex() {
|
|
857
445
|
return this.jobIndex;
|
|
858
446
|
}
|
|
859
|
-
/** Get completed jobs set for dependency validation */
|
|
860
447
|
getCompletedJobs() {
|
|
861
448
|
return this.completedJobs;
|
|
862
449
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
*/
|
|
450
|
+
getShards() {
|
|
451
|
+
return this.shards;
|
|
452
|
+
}
|
|
867
453
|
onJobCompleted(completedId) {
|
|
868
454
|
this.pendingDepChecks.add(completedId);
|
|
869
455
|
}
|
|
870
|
-
/**
|
|
871
|
-
* Batch version of onJobCompleted - more efficient for large batches
|
|
872
|
-
*/
|
|
873
456
|
onJobsCompleted(completedIds) {
|
|
874
|
-
for (const id of completedIds)
|
|
457
|
+
for (const id of completedIds)
|
|
875
458
|
this.pendingDepChecks.add(id);
|
|
876
|
-
}
|
|
877
459
|
}
|
|
878
|
-
/**
|
|
879
|
-
* Check if there are any jobs waiting for dependencies
|
|
880
|
-
* Used to skip dependency tracking when not needed
|
|
881
|
-
*/
|
|
882
460
|
hasPendingDeps() {
|
|
883
|
-
// Check if any shard has waiting dependencies
|
|
884
461
|
for (const shard of this.shards) {
|
|
885
462
|
if (shard.waitingDeps.size > 0)
|
|
886
463
|
return true;
|
|
887
464
|
}
|
|
888
465
|
return false;
|
|
889
466
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
* Instead of O(n) full scan of all waiting deps
|
|
894
|
-
*/
|
|
895
|
-
async processPendingDependencies() {
|
|
896
|
-
if (this.pendingDepChecks.size === 0)
|
|
897
|
-
return;
|
|
898
|
-
// Copy and clear the pending set
|
|
899
|
-
const completedIds = Array.from(this.pendingDepChecks);
|
|
900
|
-
this.pendingDepChecks.clear();
|
|
901
|
-
// Collect jobs to check by shard
|
|
902
|
-
const jobsToCheckByShard = new Map();
|
|
903
|
-
// Use reverse index to find only affected jobs - O(m) instead of O(n)
|
|
904
|
-
for (const completedId of completedIds) {
|
|
905
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
906
|
-
const waitingJobIds = this.shards[i].getJobsWaitingFor(completedId);
|
|
907
|
-
if (waitingJobIds && waitingJobIds.size > 0) {
|
|
908
|
-
let shardJobs = jobsToCheckByShard.get(i);
|
|
909
|
-
if (!shardJobs) {
|
|
910
|
-
shardJobs = new Set();
|
|
911
|
-
jobsToCheckByShard.set(i, shardJobs);
|
|
912
|
-
}
|
|
913
|
-
for (const jobId of waitingJobIds) {
|
|
914
|
-
shardJobs.add(jobId);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
// Process each shard that has affected jobs - in parallel using Promise.all
|
|
920
|
-
await Promise.all(Array.from(jobsToCheckByShard.entries()).map(async ([i, jobIdsToCheck]) => {
|
|
921
|
-
const shard = this.shards[i];
|
|
922
|
-
const jobsToPromote = [];
|
|
923
|
-
// Check only the affected jobs, not all waiting deps
|
|
924
|
-
for (const jobId of jobIdsToCheck) {
|
|
925
|
-
const job = shard.waitingDeps.get(jobId);
|
|
926
|
-
if (job?.dependsOn.every((dep) => this.completedJobs.has(dep))) {
|
|
927
|
-
jobsToPromote.push(job);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
// Now acquire lock and modify
|
|
931
|
-
if (jobsToPromote.length > 0) {
|
|
932
|
-
await withWriteLock(this.shardLocks[i], () => {
|
|
933
|
-
const now = Date.now();
|
|
934
|
-
for (const job of jobsToPromote) {
|
|
935
|
-
if (shard.waitingDeps.has(job.id)) {
|
|
936
|
-
shard.waitingDeps.delete(job.id);
|
|
937
|
-
// Unregister from dependency index
|
|
938
|
-
shard.unregisterDependencies(job.id, job.dependsOn);
|
|
939
|
-
shard.getQueue(job.queue).push(job);
|
|
940
|
-
// Update running counters for O(1) stats and temporal index
|
|
941
|
-
const isDelayed = job.runAt > now;
|
|
942
|
-
shard.incrementQueued(job.id, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
943
|
-
this.jobIndex.set(job.id, { type: 'queue', shardIdx: i, queueName: job.queue });
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
if (jobsToPromote.length > 0) {
|
|
947
|
-
shard.notify();
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
}));
|
|
952
|
-
}
|
|
953
|
-
// ============ Background Tasks ============
|
|
954
|
-
startBackgroundTasks() {
|
|
955
|
-
this.cleanupInterval = setInterval(() => {
|
|
956
|
-
this.cleanup();
|
|
957
|
-
}, this.config.cleanupIntervalMs);
|
|
958
|
-
this.timeoutInterval = setInterval(() => {
|
|
959
|
-
this.checkJobTimeouts();
|
|
960
|
-
}, this.config.jobTimeoutCheckMs);
|
|
961
|
-
this.depCheckInterval = setInterval(() => {
|
|
962
|
-
this.processPendingDependencies().catch((err) => {
|
|
963
|
-
queueLog.error('Dependency check failed', { error: String(err) });
|
|
964
|
-
});
|
|
965
|
-
}, this.config.dependencyCheckMs);
|
|
966
|
-
this.stallCheckInterval = setInterval(() => {
|
|
967
|
-
this.checkStalledJobs();
|
|
968
|
-
}, this.config.stallCheckMs);
|
|
969
|
-
this.dlqMaintenanceInterval = setInterval(() => {
|
|
970
|
-
this.performDlqMaintenance();
|
|
971
|
-
}, this.config.dlqMaintenanceMs);
|
|
972
|
-
// Lock expiration check runs at same interval as stall check
|
|
973
|
-
this.lockCheckInterval = setInterval(() => {
|
|
974
|
-
this.checkExpiredLocks();
|
|
975
|
-
}, this.config.stallCheckMs);
|
|
976
|
-
this.cronScheduler.start();
|
|
977
|
-
}
|
|
978
|
-
checkJobTimeouts() {
|
|
979
|
-
const now = Date.now();
|
|
980
|
-
for (const procShard of this.processingShards) {
|
|
981
|
-
for (const [jobId, job] of procShard) {
|
|
982
|
-
if (job.timeout && job.startedAt && now - job.startedAt > job.timeout) {
|
|
983
|
-
this.fail(jobId, 'Job timeout exceeded').catch((err) => {
|
|
984
|
-
queueLog.error('Failed to mark timed out job as failed', {
|
|
985
|
-
jobId: String(jobId),
|
|
986
|
-
error: String(err),
|
|
987
|
-
});
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Check for stalled jobs and handle them
|
|
995
|
-
* Uses two-phase detection (like BullMQ) to prevent false positives:
|
|
996
|
-
* - Phase 1: Jobs marked as candidates in previous check are confirmed stalled
|
|
997
|
-
* - Phase 2: Current processing jobs are marked as candidates for next check
|
|
998
|
-
*/
|
|
999
|
-
checkStalledJobs() {
|
|
1000
|
-
const now = Date.now();
|
|
1001
|
-
const confirmedStalled = [];
|
|
1002
|
-
// Phase 1: Check jobs that were candidates from previous cycle
|
|
1003
|
-
// If still in processing and still meets stall criteria → confirmed stalled
|
|
1004
|
-
for (const jobId of this.stalledCandidates) {
|
|
1005
|
-
// Find job in processing shards
|
|
1006
|
-
const procIdx = processingShardIndex(String(jobId));
|
|
1007
|
-
const job = this.processingShards[procIdx].get(jobId);
|
|
1008
|
-
if (!job) {
|
|
1009
|
-
// Job completed between checks - not stalled (false positive avoided!)
|
|
1010
|
-
this.stalledCandidates.delete(jobId);
|
|
1011
|
-
continue;
|
|
1012
|
-
}
|
|
1013
|
-
const stallConfig = this.shards[shardIndex(job.queue)].getStallConfig(job.queue);
|
|
1014
|
-
if (!stallConfig.enabled) {
|
|
1015
|
-
this.stalledCandidates.delete(jobId);
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
// Re-check stall criteria (job might have received heartbeat)
|
|
1019
|
-
const action = getStallAction(job, stallConfig, now);
|
|
1020
|
-
if (action !== "keep" /* StallAction.Keep */) {
|
|
1021
|
-
// Confirmed stalled - was candidate AND still meets criteria
|
|
1022
|
-
confirmedStalled.push({ job, action });
|
|
1023
|
-
}
|
|
1024
|
-
// Remove from candidates (will be re-added in phase 2 if still processing)
|
|
1025
|
-
this.stalledCandidates.delete(jobId);
|
|
1026
|
-
}
|
|
1027
|
-
// Phase 2: Mark current processing jobs as candidates for NEXT check
|
|
1028
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1029
|
-
const procShard = this.processingShards[i];
|
|
1030
|
-
for (const [jobId, job] of procShard) {
|
|
1031
|
-
const stallConfig = this.shards[shardIndex(job.queue)].getStallConfig(job.queue);
|
|
1032
|
-
if (!stallConfig.enabled)
|
|
1033
|
-
continue;
|
|
1034
|
-
// Only mark as candidate if past grace period and no recent heartbeat
|
|
1035
|
-
const action = getStallAction(job, stallConfig, now);
|
|
1036
|
-
if (action !== "keep" /* StallAction.Keep */) {
|
|
1037
|
-
// Add to candidates - will be checked in NEXT cycle
|
|
1038
|
-
this.stalledCandidates.add(jobId);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
// Process confirmed stalled jobs
|
|
1043
|
-
for (const { job, action } of confirmedStalled) {
|
|
1044
|
-
this.handleStalledJob(job, action).catch((err) => {
|
|
1045
|
-
queueLog.error('Failed to handle stalled job', {
|
|
1046
|
-
jobId: String(job.id),
|
|
1047
|
-
error: String(err),
|
|
1048
|
-
});
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
/**
|
|
1053
|
-
* Handle a stalled job based on the action
|
|
1054
|
-
*/
|
|
1055
|
-
async handleStalledJob(job, action) {
|
|
1056
|
-
const idx = shardIndex(job.queue);
|
|
1057
|
-
const shard = this.shards[idx];
|
|
1058
|
-
const procIdx = processingShardIndex(String(job.id));
|
|
1059
|
-
// Emit stalled event
|
|
1060
|
-
this.eventsManager.broadcast({
|
|
1061
|
-
eventType: "stalled" /* EventType.Stalled */,
|
|
1062
|
-
queue: job.queue,
|
|
1063
|
-
jobId: job.id,
|
|
1064
|
-
timestamp: Date.now(),
|
|
1065
|
-
data: { stallCount: job.stallCount + 1, action },
|
|
1066
|
-
});
|
|
1067
|
-
void this.webhookManager.trigger('stalled', String(job.id), job.queue, {
|
|
1068
|
-
data: { stallCount: job.stallCount + 1, action },
|
|
1069
|
-
});
|
|
1070
|
-
if (action === "move_to_dlq" /* StallAction.MoveToDlq */) {
|
|
1071
|
-
// Max stalls reached - move to DLQ
|
|
1072
|
-
queueLog.warn('Job exceeded max stalls, moving to DLQ', {
|
|
1073
|
-
jobId: String(job.id),
|
|
1074
|
-
queue: job.queue,
|
|
1075
|
-
stallCount: job.stallCount,
|
|
1076
|
-
});
|
|
1077
|
-
// Remove from processing
|
|
1078
|
-
this.processingShards[procIdx].delete(job.id);
|
|
1079
|
-
shard.releaseConcurrency(job.queue);
|
|
1080
|
-
// Add to DLQ with stalled reason
|
|
1081
|
-
const entry = shard.addToDlq(job, "stalled" /* FailureReason.Stalled */, `Job stalled ${job.stallCount + 1} times`);
|
|
1082
|
-
this.jobIndex.set(job.id, { type: 'dlq', queueName: job.queue });
|
|
1083
|
-
// Persist DLQ entry
|
|
1084
|
-
this.storage?.saveDlqEntry(entry);
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
// Retry - increment stall count and re-queue
|
|
1088
|
-
incrementStallCount(job);
|
|
1089
|
-
job.attempts++;
|
|
1090
|
-
job.startedAt = null;
|
|
1091
|
-
job.runAt = Date.now() + calculateBackoff(job);
|
|
1092
|
-
job.lastHeartbeat = Date.now();
|
|
1093
|
-
queueLog.warn('Job stalled, retrying', {
|
|
1094
|
-
jobId: String(job.id),
|
|
1095
|
-
queue: job.queue,
|
|
1096
|
-
stallCount: job.stallCount,
|
|
1097
|
-
attempt: job.attempts,
|
|
1098
|
-
});
|
|
1099
|
-
// Remove from processing
|
|
1100
|
-
this.processingShards[procIdx].delete(job.id);
|
|
1101
|
-
shard.releaseConcurrency(job.queue);
|
|
1102
|
-
// Re-queue
|
|
1103
|
-
shard.getQueue(job.queue).push(job);
|
|
1104
|
-
const isDelayed = job.runAt > Date.now();
|
|
1105
|
-
shard.incrementQueued(job.id, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
1106
|
-
this.jobIndex.set(job.id, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
1107
|
-
// Persist
|
|
1108
|
-
this.storage?.updateForRetry(job);
|
|
1109
|
-
}
|
|
467
|
+
// ============ Stats ============
|
|
468
|
+
getStats() {
|
|
469
|
+
return statsMgr.getStats(this.contextFactory.getStatsContext(), this.cronScheduler);
|
|
1110
470
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
*/
|
|
1114
|
-
performDlqMaintenance() {
|
|
1115
|
-
const ctx = this.getDlqContext();
|
|
1116
|
-
// Process each queue
|
|
1117
|
-
for (const queueName of this.queueNamesCache) {
|
|
1118
|
-
try {
|
|
1119
|
-
// Auto-retry eligible entries
|
|
1120
|
-
const retried = dlqOps.processAutoRetry(queueName, ctx);
|
|
1121
|
-
if (retried > 0) {
|
|
1122
|
-
queueLog.info('DLQ auto-retry completed', { queue: queueName, retried });
|
|
1123
|
-
}
|
|
1124
|
-
// Purge expired entries
|
|
1125
|
-
const purged = dlqOps.purgeExpiredDlq(queueName, ctx);
|
|
1126
|
-
if (purged > 0) {
|
|
1127
|
-
queueLog.info('DLQ purge completed', { queue: queueName, purged });
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
catch (err) {
|
|
1131
|
-
queueLog.error('DLQ maintenance failed', { queue: queueName, error: String(err) });
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
471
|
+
getMemoryStats() {
|
|
472
|
+
return statsMgr.getMemoryStats(this.contextFactory.getStatsContext());
|
|
1134
473
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
return;
|
|
1138
|
-
// Load pending jobs
|
|
1139
|
-
const now = Date.now();
|
|
1140
|
-
for (const job of this.storage.loadPendingJobs()) {
|
|
1141
|
-
const idx = shardIndex(job.queue);
|
|
1142
|
-
const shard = this.shards[idx];
|
|
1143
|
-
shard.getQueue(job.queue).push(job);
|
|
1144
|
-
this.jobIndex.set(job.id, { type: 'queue', shardIdx: idx, queueName: job.queue });
|
|
1145
|
-
// Update running counters for O(1) stats
|
|
1146
|
-
const isDelayed = job.runAt > now;
|
|
1147
|
-
shard.incrementQueued(job.id, isDelayed, job.createdAt, job.queue, job.runAt);
|
|
1148
|
-
// Register queue name in cache
|
|
1149
|
-
this.registerQueueName(job.queue);
|
|
1150
|
-
}
|
|
1151
|
-
// Load DLQ entries
|
|
1152
|
-
const dlqEntries = this.storage.loadDlq();
|
|
1153
|
-
let dlqCount = 0;
|
|
1154
|
-
for (const [queue, entries] of dlqEntries) {
|
|
1155
|
-
const idx = shardIndex(queue);
|
|
1156
|
-
const shard = this.shards[idx];
|
|
1157
|
-
for (const entry of entries) {
|
|
1158
|
-
// Add to shard's DLQ (directly set since we're loading)
|
|
1159
|
-
let dlq = shard.dlq.get(queue);
|
|
1160
|
-
if (!dlq) {
|
|
1161
|
-
dlq = [];
|
|
1162
|
-
shard.dlq.set(queue, dlq);
|
|
1163
|
-
}
|
|
1164
|
-
dlq.push(entry);
|
|
1165
|
-
shard.incrementDlq();
|
|
1166
|
-
dlqCount++;
|
|
1167
|
-
}
|
|
1168
|
-
this.registerQueueName(queue);
|
|
1169
|
-
}
|
|
1170
|
-
if (dlqCount > 0) {
|
|
1171
|
-
queueLog.info('Loaded DLQ entries', { count: dlqCount });
|
|
1172
|
-
}
|
|
1173
|
-
// Load cron jobs
|
|
1174
|
-
this.cronScheduler.load(this.storage.loadCronJobs());
|
|
1175
|
-
}
|
|
1176
|
-
// eslint-disable-next-line complexity
|
|
1177
|
-
cleanup() {
|
|
1178
|
-
// LRU collections auto-evict, but we still need to clean up:
|
|
1179
|
-
// 1. Orphaned processing shard entries (jobs stuck in processing)
|
|
1180
|
-
// 2. Stale waiting dependencies
|
|
1181
|
-
// 3. Orphaned unique keys and active groups
|
|
1182
|
-
// 4. Refresh delayed job counters (jobs that became ready)
|
|
1183
|
-
const now = Date.now();
|
|
1184
|
-
const stallTimeout = 30 * 60 * 1000; // 30 minutes max for processing
|
|
1185
|
-
// Refresh delayed counters - update jobs that have become ready
|
|
1186
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1187
|
-
this.shards[i].refreshDelayedCount(now);
|
|
1188
|
-
}
|
|
1189
|
-
// Compact priority queues if stale ratio > 20% (reclaim memory)
|
|
1190
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1191
|
-
for (const q of this.shards[i].queues.values()) {
|
|
1192
|
-
if (q.needsCompaction(0.2)) {
|
|
1193
|
-
q.compact();
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
// Clean orphaned processing entries
|
|
1198
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1199
|
-
const orphaned = [];
|
|
1200
|
-
for (const [jobId, job] of this.processingShards[i]) {
|
|
1201
|
-
if (job.startedAt && now - job.startedAt > stallTimeout) {
|
|
1202
|
-
orphaned.push(jobId);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
for (const jobId of orphaned) {
|
|
1206
|
-
const job = this.processingShards[i].get(jobId);
|
|
1207
|
-
if (job) {
|
|
1208
|
-
this.processingShards[i].delete(jobId);
|
|
1209
|
-
this.jobIndex.delete(jobId);
|
|
1210
|
-
queueLog.warn('Cleaned orphaned processing job', { jobId: String(jobId) });
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
// Clean stale waiting dependencies (waiting > 1 hour)
|
|
1215
|
-
const depTimeout = 60 * 60 * 1000; // 1 hour
|
|
1216
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1217
|
-
const shard = this.shards[i];
|
|
1218
|
-
const stale = [];
|
|
1219
|
-
for (const [_id, job] of shard.waitingDeps) {
|
|
1220
|
-
if (now - job.createdAt > depTimeout) {
|
|
1221
|
-
stale.push(job);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
for (const job of stale) {
|
|
1225
|
-
shard.waitingDeps.delete(job.id);
|
|
1226
|
-
// Remove from dependency index
|
|
1227
|
-
shard.unregisterDependencies(job.id, job.dependsOn);
|
|
1228
|
-
this.jobIndex.delete(job.id);
|
|
1229
|
-
queueLog.warn('Cleaned stale waiting dependency', { jobId: String(job.id) });
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
// Clean orphaned and expired unique keys
|
|
1233
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1234
|
-
const shard = this.shards[i];
|
|
1235
|
-
// First, clean expired unique keys
|
|
1236
|
-
const expiredCleaned = shard.cleanExpiredUniqueKeys();
|
|
1237
|
-
if (expiredCleaned > 0) {
|
|
1238
|
-
queueLog.info('Cleaned expired unique keys', { shard: i, removed: expiredCleaned });
|
|
1239
|
-
}
|
|
1240
|
-
// Then trim if too many keys remain
|
|
1241
|
-
for (const [queueName, keys] of shard.uniqueKeys) {
|
|
1242
|
-
if (keys.size > 1000) {
|
|
1243
|
-
// If too many keys, trim oldest half
|
|
1244
|
-
const toRemove = Math.floor(keys.size / 2);
|
|
1245
|
-
const iter = keys.keys();
|
|
1246
|
-
for (let j = 0; j < toRemove; j++) {
|
|
1247
|
-
const { value, done } = iter.next();
|
|
1248
|
-
if (done)
|
|
1249
|
-
break;
|
|
1250
|
-
keys.delete(value);
|
|
1251
|
-
}
|
|
1252
|
-
queueLog.info('Trimmed unique keys', { queue: queueName, removed: toRemove });
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
// Clean orphaned active groups
|
|
1256
|
-
for (const [queueName, groups] of shard.activeGroups) {
|
|
1257
|
-
if (groups.size > 1000) {
|
|
1258
|
-
const toRemove = Math.floor(groups.size / 2);
|
|
1259
|
-
const iter = groups.values();
|
|
1260
|
-
for (let j = 0; j < toRemove; j++) {
|
|
1261
|
-
const { value, done } = iter.next();
|
|
1262
|
-
if (done)
|
|
1263
|
-
break;
|
|
1264
|
-
groups.delete(value);
|
|
1265
|
-
}
|
|
1266
|
-
queueLog.info('Trimmed active groups', { queue: queueName, removed: toRemove });
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
// Clean stale stalledCandidates (jobs no longer in processing)
|
|
1271
|
-
for (const jobId of this.stalledCandidates) {
|
|
1272
|
-
const loc = this.jobIndex.get(jobId);
|
|
1273
|
-
if (loc?.type !== 'processing') {
|
|
1274
|
-
this.stalledCandidates.delete(jobId);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
// Clean orphaned jobIndex entries (pointing to invalid locations)
|
|
1278
|
-
// This is expensive so only run if index is large
|
|
1279
|
-
if (this.jobIndex.size > 100_000) {
|
|
1280
|
-
let orphanedCount = 0;
|
|
1281
|
-
for (const [jobId, loc] of this.jobIndex) {
|
|
1282
|
-
if (loc.type === 'processing') {
|
|
1283
|
-
const procIdx = processingShardIndex(String(jobId));
|
|
1284
|
-
if (!this.processingShards[procIdx].has(jobId)) {
|
|
1285
|
-
this.jobIndex.delete(jobId);
|
|
1286
|
-
orphanedCount++;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
else if (loc.type === 'queue') {
|
|
1290
|
-
// Check if job still exists in shard
|
|
1291
|
-
const shard = this.shards[loc.shardIdx];
|
|
1292
|
-
if (!shard.getQueue(loc.queueName).has(jobId)) {
|
|
1293
|
-
this.jobIndex.delete(jobId);
|
|
1294
|
-
orphanedCount++;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
if (orphanedCount > 0) {
|
|
1299
|
-
queueLog.info('Cleaned orphaned jobIndex entries', { count: orphanedCount });
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
// Clean orphaned job locks (locks for jobs no longer in processing)
|
|
1303
|
-
for (const jobId of this.jobLocks.keys()) {
|
|
1304
|
-
const loc = this.jobIndex.get(jobId);
|
|
1305
|
-
if (loc?.type !== 'processing') {
|
|
1306
|
-
this.jobLocks.delete(jobId);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
// Remove empty queues to free memory (like obliterate but only for empty queues)
|
|
1310
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1311
|
-
const shard = this.shards[i];
|
|
1312
|
-
const emptyQueues = [];
|
|
1313
|
-
for (const [queueName, queue] of shard.queues) {
|
|
1314
|
-
// Queue is empty and has no DLQ entries
|
|
1315
|
-
const dlqEntries = shard.dlq.get(queueName);
|
|
1316
|
-
if (queue.size === 0 && (!dlqEntries || dlqEntries.length === 0)) {
|
|
1317
|
-
emptyQueues.push(queueName);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
for (const queueName of emptyQueues) {
|
|
1321
|
-
shard.queues.delete(queueName);
|
|
1322
|
-
shard.dlq.delete(queueName);
|
|
1323
|
-
shard.uniqueKeys.delete(queueName);
|
|
1324
|
-
shard.queueState.delete(queueName);
|
|
1325
|
-
shard.activeGroups.delete(queueName);
|
|
1326
|
-
shard.rateLimiters.delete(queueName);
|
|
1327
|
-
shard.concurrencyLimiters.delete(queueName);
|
|
1328
|
-
shard.stallConfig.delete(queueName);
|
|
1329
|
-
shard.dlqConfig.delete(queueName);
|
|
1330
|
-
this.unregisterQueueName(queueName);
|
|
1331
|
-
}
|
|
1332
|
-
if (emptyQueues.length > 0) {
|
|
1333
|
-
queueLog.info('Removed empty queues', { shard: i, count: emptyQueues.length });
|
|
1334
|
-
}
|
|
1335
|
-
// Clean orphaned temporal index entries (memory leak fix)
|
|
1336
|
-
const cleanedTemporal = shard.cleanOrphanedTemporalEntries();
|
|
1337
|
-
if (cleanedTemporal > 0) {
|
|
1338
|
-
queueLog.info('Cleaned orphaned temporal entries', { shard: i, count: cleanedTemporal });
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
474
|
+
compactMemory() {
|
|
475
|
+
statsMgr.compactMemory(this.contextFactory.getStatsContext());
|
|
1341
476
|
}
|
|
1342
477
|
// ============ Lifecycle ============
|
|
1343
478
|
shutdown() {
|
|
1344
479
|
this.cronScheduler.stop();
|
|
1345
480
|
this.workerManager.stop();
|
|
1346
481
|
this.eventsManager.clear();
|
|
1347
|
-
if (this.
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
clearInterval(this.timeoutInterval);
|
|
1351
|
-
if (this.depCheckInterval)
|
|
1352
|
-
clearInterval(this.depCheckInterval);
|
|
1353
|
-
if (this.stallCheckInterval)
|
|
1354
|
-
clearInterval(this.stallCheckInterval);
|
|
1355
|
-
if (this.dlqMaintenanceInterval)
|
|
1356
|
-
clearInterval(this.dlqMaintenanceInterval);
|
|
1357
|
-
if (this.lockCheckInterval)
|
|
1358
|
-
clearInterval(this.lockCheckInterval);
|
|
482
|
+
if (this.backgroundTaskHandles) {
|
|
483
|
+
bgTasks.stopBackgroundTasks(this.backgroundTaskHandles);
|
|
484
|
+
}
|
|
1359
485
|
this.storage?.close();
|
|
1360
486
|
// Clear in-memory collections
|
|
1361
487
|
this.jobIndex.clear();
|
|
@@ -1368,9 +494,8 @@ export class QueueManager {
|
|
|
1368
494
|
this.jobLocks.clear();
|
|
1369
495
|
this.stalledCandidates.clear();
|
|
1370
496
|
this.clientJobs.clear();
|
|
1371
|
-
for (const shard of this.processingShards)
|
|
497
|
+
for (const shard of this.processingShards)
|
|
1372
498
|
shard.clear();
|
|
1373
|
-
}
|
|
1374
499
|
for (const shard of this.shards) {
|
|
1375
500
|
shard.waitingDeps.clear();
|
|
1376
501
|
shard.dependencyIndex.clear();
|
|
@@ -1379,106 +504,5 @@ export class QueueManager {
|
|
|
1379
504
|
shard.activeGroups.clear();
|
|
1380
505
|
}
|
|
1381
506
|
}
|
|
1382
|
-
getStats() {
|
|
1383
|
-
let waiting = 0, delayed = 0, active = 0, dlq = 0;
|
|
1384
|
-
// O(32) instead of O(n) - use running counters from each shard
|
|
1385
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1386
|
-
const shardStats = this.shards[i].getStats();
|
|
1387
|
-
const queuedTotal = shardStats.queuedJobs;
|
|
1388
|
-
const delayedInShard = shardStats.delayedJobs;
|
|
1389
|
-
// waiting = queued jobs that are not delayed
|
|
1390
|
-
waiting += Math.max(0, queuedTotal - delayedInShard);
|
|
1391
|
-
delayed += delayedInShard;
|
|
1392
|
-
dlq += shardStats.dlqJobs;
|
|
1393
|
-
active += this.processingShards[i].size;
|
|
1394
|
-
}
|
|
1395
|
-
const cronStats = this.cronScheduler.getStats();
|
|
1396
|
-
return {
|
|
1397
|
-
waiting,
|
|
1398
|
-
delayed,
|
|
1399
|
-
active,
|
|
1400
|
-
dlq,
|
|
1401
|
-
completed: this.completedJobs.size,
|
|
1402
|
-
totalPushed: this.metrics.totalPushed.value,
|
|
1403
|
-
totalPulled: this.metrics.totalPulled.value,
|
|
1404
|
-
totalCompleted: this.metrics.totalCompleted.value,
|
|
1405
|
-
totalFailed: this.metrics.totalFailed.value,
|
|
1406
|
-
uptime: Date.now() - this.startTime,
|
|
1407
|
-
cronJobs: cronStats.total,
|
|
1408
|
-
cronPending: cronStats.pending,
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Get detailed memory statistics for debugging memory issues.
|
|
1413
|
-
* Returns counts of entries in all major collections.
|
|
1414
|
-
*/
|
|
1415
|
-
getMemoryStats() {
|
|
1416
|
-
let processingTotal = 0;
|
|
1417
|
-
let queuedTotal = 0;
|
|
1418
|
-
let waitingDepsTotal = 0;
|
|
1419
|
-
let temporalIndexTotal = 0;
|
|
1420
|
-
let delayedHeapTotal = 0;
|
|
1421
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1422
|
-
processingTotal += this.processingShards[i].size;
|
|
1423
|
-
const shardStats = this.shards[i].getStats();
|
|
1424
|
-
queuedTotal += shardStats.queuedJobs;
|
|
1425
|
-
waitingDepsTotal += this.shards[i].waitingDeps.size;
|
|
1426
|
-
// Get internal structure sizes
|
|
1427
|
-
const internalSizes = this.shards[i].getInternalSizes();
|
|
1428
|
-
temporalIndexTotal += internalSizes.temporalIndex;
|
|
1429
|
-
delayedHeapTotal += internalSizes.delayedHeap;
|
|
1430
|
-
}
|
|
1431
|
-
// Count total jobs across all clients
|
|
1432
|
-
let clientJobsTotal = 0;
|
|
1433
|
-
for (const jobs of this.clientJobs.values()) {
|
|
1434
|
-
clientJobsTotal += jobs.size;
|
|
1435
|
-
}
|
|
1436
|
-
return {
|
|
1437
|
-
jobIndex: this.jobIndex.size,
|
|
1438
|
-
completedJobs: this.completedJobs.size,
|
|
1439
|
-
jobResults: this.jobResults.size,
|
|
1440
|
-
jobLogs: this.jobLogs.size,
|
|
1441
|
-
customIdMap: this.customIdMap.size,
|
|
1442
|
-
jobLocks: this.jobLocks.size,
|
|
1443
|
-
clientJobs: this.clientJobs.size,
|
|
1444
|
-
clientJobsTotal,
|
|
1445
|
-
pendingDepChecks: this.pendingDepChecks.size,
|
|
1446
|
-
stalledCandidates: this.stalledCandidates.size,
|
|
1447
|
-
processingTotal,
|
|
1448
|
-
queuedTotal,
|
|
1449
|
-
waitingDepsTotal,
|
|
1450
|
-
temporalIndexTotal,
|
|
1451
|
-
delayedHeapTotal,
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
/**
|
|
1455
|
-
* Force compact all collections to reduce memory usage.
|
|
1456
|
-
* Use after large batch operations or when memory pressure is high.
|
|
1457
|
-
*/
|
|
1458
|
-
compactMemory() {
|
|
1459
|
-
// Compact priority queues that have high stale ratios
|
|
1460
|
-
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
1461
|
-
for (const q of this.shards[i].queues.values()) {
|
|
1462
|
-
if (q.needsCompaction(0.1)) {
|
|
1463
|
-
// More aggressive: 10% stale threshold
|
|
1464
|
-
q.compact();
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
// Clean up empty client tracking entries
|
|
1469
|
-
for (const [clientId, jobs] of this.clientJobs) {
|
|
1470
|
-
if (jobs.size === 0) {
|
|
1471
|
-
this.clientJobs.delete(clientId);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
// Clean orphaned job locks (jobs no longer in processing)
|
|
1475
|
-
for (const jobId of this.jobLocks.keys()) {
|
|
1476
|
-
const loc = this.jobIndex.get(jobId);
|
|
1477
|
-
if (loc?.type !== 'processing') {
|
|
1478
|
-
this.jobLocks.delete(jobId);
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
queueLog.info('Memory compacted');
|
|
1482
|
-
}
|
|
1483
507
|
}
|
|
1484
508
|
//# sourceMappingURL=queueManager.js.map
|