bunqueue 1.9.9 → 2.0.1
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 +10 -0
- package/dist/application/backgroundTasks.d.ts.map +1 -1
- package/dist/application/backgroundTasks.js +67 -6
- package/dist/application/backgroundTasks.js.map +1 -1
- package/dist/application/cleanupTasks.js +1 -1
- package/dist/application/cleanupTasks.js.map +1 -1
- package/dist/application/clientTracking.d.ts.map +1 -1
- package/dist/application/clientTracking.js +23 -10
- package/dist/application/clientTracking.js.map +1 -1
- package/dist/application/dependencyProcessor.d.ts.map +1 -1
- package/dist/application/dependencyProcessor.js +14 -13
- package/dist/application/dependencyProcessor.js.map +1 -1
- package/dist/application/eventsManager.d.ts.map +1 -1
- package/dist/application/eventsManager.js +16 -4
- package/dist/application/eventsManager.js.map +1 -1
- package/dist/application/jobLogsManager.d.ts +2 -2
- package/dist/application/jobLogsManager.d.ts.map +1 -1
- package/dist/application/jobLogsManager.js +13 -3
- package/dist/application/jobLogsManager.js.map +1 -1
- package/dist/application/lockManager.js +1 -1
- package/dist/application/lockManager.js.map +1 -1
- package/dist/application/operations/ack.d.ts +2 -1
- package/dist/application/operations/ack.d.ts.map +1 -1
- package/dist/application/operations/ack.js +12 -0
- package/dist/application/operations/ack.js.map +1 -1
- package/dist/application/operations/jobManagement.d.ts +1 -1
- package/dist/application/operations/jobManagement.d.ts.map +1 -1
- package/dist/application/operations/jobManagement.js +23 -3
- package/dist/application/operations/jobManagement.js.map +1 -1
- package/dist/application/operations/push.d.ts +1 -1
- package/dist/application/operations/push.d.ts.map +1 -1
- package/dist/application/operations/push.js +13 -5
- package/dist/application/operations/push.js.map +1 -1
- package/dist/application/operations/queryOperations.d.ts +3 -0
- package/dist/application/operations/queryOperations.d.ts.map +1 -1
- package/dist/application/operations/queryOperations.js +29 -0
- package/dist/application/operations/queryOperations.js.map +1 -1
- package/dist/application/queueManager.d.ts +15 -1
- package/dist/application/queueManager.d.ts.map +1 -1
- package/dist/application/queueManager.js +69 -2
- package/dist/application/queueManager.js.map +1 -1
- package/dist/application/stallDetection.js +25 -20
- package/dist/application/stallDetection.js.map +1 -1
- package/dist/application/webhookManager.d.ts.map +1 -1
- package/dist/application/webhookManager.js +18 -2
- package/dist/application/webhookManager.js.map +1 -1
- package/dist/application/workerManager.d.ts.map +1 -1
- package/dist/application/workerManager.js +4 -2
- package/dist/application/workerManager.js.map +1 -1
- package/dist/client/events.d.ts +29 -0
- package/dist/client/events.d.ts.map +1 -1
- package/dist/client/events.js +92 -21
- package/dist/client/events.js.map +1 -1
- package/dist/client/flow.d.ts +122 -3
- package/dist/client/flow.d.ts.map +1 -1
- package/dist/client/flow.js +374 -2
- package/dist/client/flow.js.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/queue/queue.d.ts +260 -1
- package/dist/client/queue/queue.d.ts.map +1 -1
- package/dist/client/queue/queue.js +1343 -16
- package/dist/client/queue/queue.js.map +1 -1
- package/dist/client/tcpPool.d.ts.map +1 -1
- package/dist/client/tcpPool.js +19 -8
- package/dist/client/tcpPool.js.map +1 -1
- package/dist/client/types.d.ts +430 -13
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js +346 -5
- package/dist/client/types.js.map +1 -1
- package/dist/client/worker/ackBatcher.d.ts +1 -0
- package/dist/client/worker/ackBatcher.d.ts.map +1 -1
- package/dist/client/worker/ackBatcher.js +9 -0
- package/dist/client/worker/ackBatcher.js.map +1 -1
- package/dist/client/worker/processor.js +6 -1
- package/dist/client/worker/processor.js.map +1 -1
- package/dist/client/worker/worker.d.ts +117 -0
- package/dist/client/worker/worker.d.ts.map +1 -1
- package/dist/client/worker/worker.js +375 -3
- package/dist/client/worker/worker.js.map +1 -1
- package/dist/domain/queue/priorityQueue.d.ts.map +1 -1
- package/dist/domain/queue/priorityQueue.js +24 -18
- package/dist/domain/queue/priorityQueue.js.map +1 -1
- package/dist/domain/queue/shard.d.ts +4 -0
- package/dist/domain/queue/shard.d.ts.map +1 -1
- package/dist/domain/queue/shard.js +21 -7
- package/dist/domain/queue/shard.js.map +1 -1
- package/dist/domain/types/job.d.ts +89 -2
- package/dist/domain/types/job.d.ts.map +1 -1
- package/dist/domain/types/job.js +94 -26
- package/dist/domain/types/job.js.map +1 -1
- package/dist/domain/types/queue.d.ts +11 -1
- package/dist/domain/types/queue.d.ts.map +1 -1
- package/dist/infrastructure/persistence/sqliteBatch.d.ts +9 -4
- package/dist/infrastructure/persistence/sqliteBatch.d.ts.map +1 -1
- package/dist/infrastructure/persistence/sqliteBatch.js +34 -17
- package/dist/infrastructure/persistence/sqliteBatch.js.map +1 -1
- package/dist/infrastructure/persistence/sqliteSerializer.d.ts.map +1 -1
- package/dist/infrastructure/persistence/sqliteSerializer.js +14 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js.map +1 -1
- package/dist/infrastructure/scheduler/cronScheduler.d.ts.map +1 -1
- package/dist/infrastructure/scheduler/cronScheduler.js +29 -15
- package/dist/infrastructure/scheduler/cronScheduler.js.map +1 -1
- package/dist/infrastructure/server/handlers/query.d.ts.map +1 -1
- package/dist/infrastructure/server/handlers/query.js +1 -16
- package/dist/infrastructure/server/handlers/query.js.map +1 -1
- package/dist/infrastructure/server/rateLimiter.d.ts.map +1 -1
- package/dist/infrastructure/server/rateLimiter.js +5 -3
- package/dist/infrastructure/server/rateLimiter.js.map +1 -1
- package/dist/infrastructure/server/tcp.d.ts.map +1 -1
- package/dist/infrastructure/server/tcp.js +36 -4
- package/dist/infrastructure/server/tcp.js.map +1 -1
- package/dist/main.js +5 -2
- package/dist/main.js.map +1 -1
- package/dist/shared/lock.d.ts +1 -1
- package/dist/shared/lock.d.ts.map +1 -1
- package/dist/shared/lock.js +6 -4
- package/dist/shared/lock.js.map +1 -1
- package/dist/shared/lru.d.ts +28 -0
- package/dist/shared/lru.d.ts.map +1 -1
- package/dist/shared/lru.js +28 -0
- package/dist/shared/lru.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { EventEmitter } from 'events';
|
|
6
6
|
import type { WorkerOptions, Processor } from '../types';
|
|
7
|
+
import type { Job as InternalJob } from '../../domain/types/job';
|
|
7
8
|
/**
|
|
8
9
|
* Worker class for processing jobs
|
|
9
10
|
*/
|
|
@@ -16,25 +17,141 @@ export declare class Worker<T = unknown, R = unknown> extends EventEmitter {
|
|
|
16
17
|
private readonly tcpPool;
|
|
17
18
|
private readonly ackBatcher;
|
|
18
19
|
private running;
|
|
20
|
+
private paused;
|
|
19
21
|
private closing;
|
|
22
|
+
private closed;
|
|
20
23
|
private activeJobs;
|
|
21
24
|
private pollTimer;
|
|
22
25
|
private consecutiveErrors;
|
|
23
26
|
private readonly activeJobIds;
|
|
24
27
|
private readonly pulledJobIds;
|
|
25
28
|
private readonly jobTokens;
|
|
29
|
+
private readonly cancelledJobs;
|
|
26
30
|
private heartbeatTimer;
|
|
27
31
|
private readonly workerId;
|
|
28
32
|
private pendingJobs;
|
|
29
33
|
private pendingJobsHead;
|
|
30
34
|
private processingScheduled;
|
|
35
|
+
private readonly limiter;
|
|
36
|
+
private limiterTokens;
|
|
37
|
+
private lastDrainedEmit;
|
|
38
|
+
private stalledUnsubscribe;
|
|
31
39
|
constructor(name: string, processor: Processor<T, R>, opts?: WorkerOptions);
|
|
32
40
|
/** Start processing */
|
|
33
41
|
run(): void;
|
|
42
|
+
/** Subscribe to stalled events from QueueManager (BullMQ v5 compatible) */
|
|
43
|
+
private subscribeToStalledEvents;
|
|
34
44
|
/** Pause processing */
|
|
35
45
|
pause(): void;
|
|
36
46
|
/** Resume processing */
|
|
37
47
|
resume(): void;
|
|
48
|
+
/** Check if worker is currently running */
|
|
49
|
+
isRunning(): boolean;
|
|
50
|
+
/** Check if worker is paused */
|
|
51
|
+
isPaused(): boolean;
|
|
52
|
+
/** Check if worker is closed */
|
|
53
|
+
isClosed(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Wait until the worker is ready (BullMQ v5 compatible).
|
|
56
|
+
* In embedded mode, resolves immediately.
|
|
57
|
+
* In TCP mode, waits for connection to be established.
|
|
58
|
+
*/
|
|
59
|
+
waitUntilReady(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Mark a job for cancellation (BullMQ v5 compatible).
|
|
62
|
+
* The job will be failed with the given reason when it completes processing.
|
|
63
|
+
* Returns true if the job was found and marked for cancellation.
|
|
64
|
+
*/
|
|
65
|
+
cancelJob(jobId: string, reason?: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Mark all active jobs for cancellation (BullMQ v5 compatible).
|
|
68
|
+
*/
|
|
69
|
+
cancelAllJobs(reason?: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* Check if a job has been marked for cancellation.
|
|
72
|
+
* Can be called from within a processor to check if the job should stop.
|
|
73
|
+
*/
|
|
74
|
+
isJobCancelled(jobId: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Check if rate limiter allows processing another job.
|
|
77
|
+
* Returns true if we can process, false if rate limited.
|
|
78
|
+
*/
|
|
79
|
+
private canProcessWithinLimit;
|
|
80
|
+
/**
|
|
81
|
+
* Record a job completion for rate limiting.
|
|
82
|
+
*/
|
|
83
|
+
private recordJobForLimiter;
|
|
84
|
+
/**
|
|
85
|
+
* Get time until rate limiter allows next job (ms).
|
|
86
|
+
* Returns 0 if not rate limited.
|
|
87
|
+
*/
|
|
88
|
+
private getTimeUntilNextSlot;
|
|
89
|
+
/**
|
|
90
|
+
* Get rate limiter info (for debugging/monitoring).
|
|
91
|
+
*/
|
|
92
|
+
getRateLimiterInfo(): {
|
|
93
|
+
current: number;
|
|
94
|
+
max: number;
|
|
95
|
+
duration: number;
|
|
96
|
+
} | null;
|
|
97
|
+
/**
|
|
98
|
+
* Apply rate limiting to this worker (BullMQ v5 compatible).
|
|
99
|
+
* The worker will not process jobs until the rate limit expires.
|
|
100
|
+
*
|
|
101
|
+
* @param expireTimeMs - Time in milliseconds until rate limit expires
|
|
102
|
+
*/
|
|
103
|
+
rateLimit(expireTimeMs: number): void;
|
|
104
|
+
/** Rate limit expiration timestamp (internal) */
|
|
105
|
+
private rateLimitExpiration;
|
|
106
|
+
/**
|
|
107
|
+
* Check if worker is currently rate limited.
|
|
108
|
+
*/
|
|
109
|
+
isRateLimited(): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Manually start the stalled job check timer (BullMQ v5 compatible).
|
|
112
|
+
* The check will run once immediately and then every stalledInterval ms.
|
|
113
|
+
* In bunqueue, stall detection is handled automatically by the manager/server.
|
|
114
|
+
*/
|
|
115
|
+
startStalledCheckTimer(): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Delay processing for a specified time (BullMQ v5 compatible).
|
|
118
|
+
* The worker will not pick up new jobs during this delay.
|
|
119
|
+
*
|
|
120
|
+
* @param milliseconds - Time to delay in ms (default: 0)
|
|
121
|
+
* @param abortController - Optional AbortController to cancel the delay
|
|
122
|
+
*/
|
|
123
|
+
delay(milliseconds?: number, abortController?: AbortController): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Get the next job from the queue (BullMQ v5 compatible).
|
|
126
|
+
* This is for manual job processing - typically you'd use the processor callback instead.
|
|
127
|
+
*
|
|
128
|
+
* @param token - Lock token for job ownership
|
|
129
|
+
* @param opts - Options (currently unused, for API compatibility)
|
|
130
|
+
* @returns The next job or undefined if queue is empty
|
|
131
|
+
*/
|
|
132
|
+
getNextJob(token?: string, _opts?: {
|
|
133
|
+
block?: boolean;
|
|
134
|
+
}): Promise<InternalJob | undefined>;
|
|
135
|
+
/**
|
|
136
|
+
* Manually process a job (BullMQ v5 compatible).
|
|
137
|
+
* This is for advanced use cases where you need manual control over job processing.
|
|
138
|
+
*
|
|
139
|
+
* @param job - The job to process
|
|
140
|
+
* @param token - Lock token for job ownership
|
|
141
|
+
* @param fetchNextCallback - Optional callback to fetch next job after completion
|
|
142
|
+
* @returns The processed job or void
|
|
143
|
+
*/
|
|
144
|
+
processJobManually(job: InternalJob, token?: string, fetchNextCallback?: () => Promise<InternalJob | undefined>): Promise<InternalJob | undefined>;
|
|
145
|
+
/**
|
|
146
|
+
* Extend locks on multiple jobs (BullMQ v5 compatible).
|
|
147
|
+
* Used to prevent jobs from being considered stalled during long processing.
|
|
148
|
+
*
|
|
149
|
+
* @param jobIds - Array of job IDs to extend locks for
|
|
150
|
+
* @param tokens - Array of lock tokens corresponding to each job
|
|
151
|
+
* @param duration - Duration in milliseconds to extend the lock
|
|
152
|
+
* @returns Number of locks successfully extended
|
|
153
|
+
*/
|
|
154
|
+
extendJobLocks(jobIds: string[], tokens: string[], duration: number): Promise<number>;
|
|
38
155
|
/** Close worker gracefully */
|
|
39
156
|
close(force?: boolean): Promise<void>;
|
|
40
157
|
private startHeartbeat;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/client/worker/worker.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../src/client/worker/worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,7 +21,9 @@ export class Worker extends EventEmitter {
|
|
|
21
21
|
tcpPool;
|
|
22
22
|
ackBatcher;
|
|
23
23
|
running = false;
|
|
24
|
+
paused = false;
|
|
24
25
|
closing = false;
|
|
26
|
+
closed = false;
|
|
25
27
|
activeJobs = 0;
|
|
26
28
|
pollTimer = null;
|
|
27
29
|
consecutiveErrors = 0;
|
|
@@ -30,6 +32,7 @@ export class Worker extends EventEmitter {
|
|
|
30
32
|
activeJobIds = new Set();
|
|
31
33
|
pulledJobIds = new Set(); // All pulled jobs (for heartbeat)
|
|
32
34
|
jobTokens = new Map(); // jobId -> lockToken
|
|
35
|
+
cancelledJobs = new Set(); // Jobs marked for cancellation
|
|
33
36
|
heartbeatTimer = null;
|
|
34
37
|
// Unique worker ID for lock ownership
|
|
35
38
|
workerId;
|
|
@@ -37,6 +40,13 @@ export class Worker extends EventEmitter {
|
|
|
37
40
|
pendingJobs = [];
|
|
38
41
|
pendingJobsHead = 0;
|
|
39
42
|
processingScheduled = false; // Prevent multiple setImmediate calls
|
|
43
|
+
// Rate limiter state (BullMQ v5 compatible)
|
|
44
|
+
limiter;
|
|
45
|
+
limiterTokens = []; // Timestamps of recent job completions
|
|
46
|
+
// Drained event tracking
|
|
47
|
+
lastDrainedEmit = 0;
|
|
48
|
+
// Stalled event subscription (BullMQ v5 compatible)
|
|
49
|
+
stalledUnsubscribe = null;
|
|
40
50
|
constructor(name, processor, opts = {}) {
|
|
41
51
|
super();
|
|
42
52
|
this.name = name;
|
|
@@ -55,6 +65,8 @@ export class Worker extends EventEmitter {
|
|
|
55
65
|
// Lock-based ownership: disable for high-throughput scenarios where stall detection is sufficient
|
|
56
66
|
useLocks: opts.useLocks ?? true,
|
|
57
67
|
};
|
|
68
|
+
// Initialize rate limiter if provided
|
|
69
|
+
this.limiter = opts.limiter ?? null;
|
|
58
70
|
this.ackBatcher = new AckBatcher({
|
|
59
71
|
batchSize: opts.batchSize ?? 10,
|
|
60
72
|
interval: WORKER_CONSTANTS.DEFAULT_ACK_INTERVAL,
|
|
@@ -81,19 +93,41 @@ export class Worker extends EventEmitter {
|
|
|
81
93
|
}
|
|
82
94
|
/** Start processing */
|
|
83
95
|
run() {
|
|
84
|
-
if (this.running)
|
|
96
|
+
if (this.running || this.closed)
|
|
85
97
|
return;
|
|
86
98
|
this.running = true;
|
|
99
|
+
this.paused = false;
|
|
87
100
|
this.closing = false;
|
|
88
101
|
this.emit('ready');
|
|
102
|
+
// Subscribe to stalled events in embedded mode (BullMQ v5)
|
|
103
|
+
if (this.embedded && !this.stalledUnsubscribe) {
|
|
104
|
+
this.subscribeToStalledEvents();
|
|
105
|
+
}
|
|
89
106
|
if (!this.embedded && this.opts.heartbeatInterval > 0) {
|
|
90
107
|
this.startHeartbeat();
|
|
91
108
|
}
|
|
92
109
|
this.poll();
|
|
93
110
|
}
|
|
111
|
+
/** Subscribe to stalled events from QueueManager (BullMQ v5 compatible) */
|
|
112
|
+
subscribeToStalledEvents() {
|
|
113
|
+
if (!this.embedded)
|
|
114
|
+
return;
|
|
115
|
+
const manager = getSharedManager();
|
|
116
|
+
this.stalledUnsubscribe = manager.subscribe((event) => {
|
|
117
|
+
if (event.queue !== this.name)
|
|
118
|
+
return;
|
|
119
|
+
if (event.eventType === "stalled" /* EventType.Stalled */) {
|
|
120
|
+
// Emit stalled event (BullMQ v5 format: jobId, prev)
|
|
121
|
+
this.emit('stalled', event.jobId, 'active');
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
94
125
|
/** Pause processing */
|
|
95
126
|
pause() {
|
|
127
|
+
if (!this.running)
|
|
128
|
+
return;
|
|
96
129
|
this.running = false;
|
|
130
|
+
this.paused = true;
|
|
97
131
|
if (this.pollTimer) {
|
|
98
132
|
clearTimeout(this.pollTimer);
|
|
99
133
|
this.pollTimer = null;
|
|
@@ -101,12 +135,325 @@ export class Worker extends EventEmitter {
|
|
|
101
135
|
}
|
|
102
136
|
/** Resume processing */
|
|
103
137
|
resume() {
|
|
138
|
+
if (this.closed)
|
|
139
|
+
return;
|
|
140
|
+
this.paused = false;
|
|
104
141
|
this.run();
|
|
105
142
|
}
|
|
143
|
+
/** Check if worker is currently running */
|
|
144
|
+
isRunning() {
|
|
145
|
+
return this.running;
|
|
146
|
+
}
|
|
147
|
+
/** Check if worker is paused */
|
|
148
|
+
isPaused() {
|
|
149
|
+
return this.paused && !this.closed;
|
|
150
|
+
}
|
|
151
|
+
/** Check if worker is closed */
|
|
152
|
+
isClosed() {
|
|
153
|
+
return this.closed;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Wait until the worker is ready (BullMQ v5 compatible).
|
|
157
|
+
* In embedded mode, resolves immediately.
|
|
158
|
+
* In TCP mode, waits for connection to be established.
|
|
159
|
+
*/
|
|
160
|
+
async waitUntilReady() {
|
|
161
|
+
if (this.embedded) {
|
|
162
|
+
// Embedded mode is always ready
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (this.tcpPool) {
|
|
166
|
+
// Wait for TCP connection by sending a ping
|
|
167
|
+
await this.tcpPool.send({ cmd: 'Ping' });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Mark a job for cancellation (BullMQ v5 compatible).
|
|
172
|
+
* The job will be failed with the given reason when it completes processing.
|
|
173
|
+
* Returns true if the job was found and marked for cancellation.
|
|
174
|
+
*/
|
|
175
|
+
cancelJob(jobId, reason) {
|
|
176
|
+
if (this.activeJobIds.has(jobId)) {
|
|
177
|
+
this.cancelledJobs.add(jobId);
|
|
178
|
+
this.emit('cancelled', { jobId, reason: reason ?? 'Job cancelled by worker' });
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Mark all active jobs for cancellation (BullMQ v5 compatible).
|
|
185
|
+
*/
|
|
186
|
+
cancelAllJobs(reason) {
|
|
187
|
+
for (const jobId of this.activeJobIds) {
|
|
188
|
+
this.cancelledJobs.add(jobId);
|
|
189
|
+
this.emit('cancelled', { jobId, reason: reason ?? 'All jobs cancelled' });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if a job has been marked for cancellation.
|
|
194
|
+
* Can be called from within a processor to check if the job should stop.
|
|
195
|
+
*/
|
|
196
|
+
isJobCancelled(jobId) {
|
|
197
|
+
return this.cancelledJobs.has(jobId);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Check if rate limiter allows processing another job.
|
|
201
|
+
* Returns true if we can process, false if rate limited.
|
|
202
|
+
*/
|
|
203
|
+
canProcessWithinLimit() {
|
|
204
|
+
if (!this.limiter)
|
|
205
|
+
return true;
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
const windowStart = now - this.limiter.duration;
|
|
208
|
+
// Remove expired tokens
|
|
209
|
+
this.limiterTokens = this.limiterTokens.filter((t) => t > windowStart);
|
|
210
|
+
// Check if we have capacity
|
|
211
|
+
return this.limiterTokens.length < this.limiter.max;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Record a job completion for rate limiting.
|
|
215
|
+
*/
|
|
216
|
+
recordJobForLimiter() {
|
|
217
|
+
if (!this.limiter)
|
|
218
|
+
return;
|
|
219
|
+
this.limiterTokens.push(Date.now());
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get time until rate limiter allows next job (ms).
|
|
223
|
+
* Returns 0 if not rate limited.
|
|
224
|
+
*/
|
|
225
|
+
getTimeUntilNextSlot() {
|
|
226
|
+
if (!this.limiter)
|
|
227
|
+
return 0;
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
const windowStart = now - this.limiter.duration;
|
|
230
|
+
// Remove expired tokens
|
|
231
|
+
this.limiterTokens = this.limiterTokens.filter((t) => t > windowStart);
|
|
232
|
+
if (this.limiterTokens.length < this.limiter.max) {
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
// Find oldest token and calculate when it expires
|
|
236
|
+
const oldestToken = Math.min(...this.limiterTokens);
|
|
237
|
+
return oldestToken + this.limiter.duration - now;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get rate limiter info (for debugging/monitoring).
|
|
241
|
+
*/
|
|
242
|
+
getRateLimiterInfo() {
|
|
243
|
+
if (!this.limiter)
|
|
244
|
+
return null;
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
const windowStart = now - this.limiter.duration;
|
|
247
|
+
const currentTokens = this.limiterTokens.filter((t) => t > windowStart).length;
|
|
248
|
+
return {
|
|
249
|
+
current: currentTokens,
|
|
250
|
+
max: this.limiter.max,
|
|
251
|
+
duration: this.limiter.duration,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Apply rate limiting to this worker (BullMQ v5 compatible).
|
|
256
|
+
* The worker will not process jobs until the rate limit expires.
|
|
257
|
+
*
|
|
258
|
+
* @param expireTimeMs - Time in milliseconds until rate limit expires
|
|
259
|
+
*/
|
|
260
|
+
rateLimit(expireTimeMs) {
|
|
261
|
+
if (expireTimeMs <= 0)
|
|
262
|
+
return;
|
|
263
|
+
// Fill rate limiter tokens to block processing
|
|
264
|
+
if (this.limiter) {
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
// Add enough tokens to block for the specified time
|
|
267
|
+
for (let i = 0; i < this.limiter.max; i++) {
|
|
268
|
+
this.limiterTokens.push(now + expireTimeMs - this.limiter.duration);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Store rate limit expiration
|
|
272
|
+
this.rateLimitExpiration = Date.now() + expireTimeMs;
|
|
273
|
+
}
|
|
274
|
+
/** Rate limit expiration timestamp (internal) */
|
|
275
|
+
rateLimitExpiration = 0;
|
|
276
|
+
/**
|
|
277
|
+
* Check if worker is currently rate limited.
|
|
278
|
+
*/
|
|
279
|
+
isRateLimited() {
|
|
280
|
+
return Date.now() < this.rateLimitExpiration;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Manually start the stalled job check timer (BullMQ v5 compatible).
|
|
284
|
+
* The check will run once immediately and then every stalledInterval ms.
|
|
285
|
+
* In bunqueue, stall detection is handled automatically by the manager/server.
|
|
286
|
+
*/
|
|
287
|
+
async startStalledCheckTimer() {
|
|
288
|
+
// In bunqueue, stall detection is handled automatically by:
|
|
289
|
+
// - Embedded mode: QueueManager.checkStalledJobs() runs periodically
|
|
290
|
+
// - TCP mode: Server-side stall detection
|
|
291
|
+
// This method is a no-op for API compatibility
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Delay processing for a specified time (BullMQ v5 compatible).
|
|
295
|
+
* The worker will not pick up new jobs during this delay.
|
|
296
|
+
*
|
|
297
|
+
* @param milliseconds - Time to delay in ms (default: 0)
|
|
298
|
+
* @param abortController - Optional AbortController to cancel the delay
|
|
299
|
+
*/
|
|
300
|
+
async delay(milliseconds = 0, abortController) {
|
|
301
|
+
if (milliseconds <= 0)
|
|
302
|
+
return;
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
const timeout = setTimeout(resolve, milliseconds);
|
|
305
|
+
if (abortController) {
|
|
306
|
+
abortController.signal.addEventListener('abort', () => {
|
|
307
|
+
clearTimeout(timeout);
|
|
308
|
+
reject(new Error('Delay aborted'));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// BullMQ v5 Manual Job Control Methods
|
|
315
|
+
// ============================================================================
|
|
316
|
+
/**
|
|
317
|
+
* Get the next job from the queue (BullMQ v5 compatible).
|
|
318
|
+
* This is for manual job processing - typically you'd use the processor callback instead.
|
|
319
|
+
*
|
|
320
|
+
* @param token - Lock token for job ownership
|
|
321
|
+
* @param opts - Options (currently unused, for API compatibility)
|
|
322
|
+
* @returns The next job or undefined if queue is empty
|
|
323
|
+
*/
|
|
324
|
+
async getNextJob(token, _opts) {
|
|
325
|
+
if (this.closed)
|
|
326
|
+
return undefined;
|
|
327
|
+
if (this.embedded) {
|
|
328
|
+
const manager = getSharedManager();
|
|
329
|
+
if (this.opts.useLocks) {
|
|
330
|
+
const { job, token: lockToken } = await manager.pullWithLock(this.name, this.workerId, 0);
|
|
331
|
+
if (job && lockToken) {
|
|
332
|
+
const jobIdStr = String(job.id);
|
|
333
|
+
this.pulledJobIds.add(jobIdStr);
|
|
334
|
+
this.jobTokens.set(jobIdStr, lockToken);
|
|
335
|
+
}
|
|
336
|
+
return job ?? undefined;
|
|
337
|
+
}
|
|
338
|
+
const job = await manager.pull(this.name, 0);
|
|
339
|
+
if (job) {
|
|
340
|
+
this.pulledJobIds.add(String(job.id));
|
|
341
|
+
}
|
|
342
|
+
return job ?? undefined;
|
|
343
|
+
}
|
|
344
|
+
// TCP mode
|
|
345
|
+
if (!this.tcp)
|
|
346
|
+
return undefined;
|
|
347
|
+
const cmd = {
|
|
348
|
+
cmd: 'PULL',
|
|
349
|
+
queue: this.name,
|
|
350
|
+
timeout: 0,
|
|
351
|
+
};
|
|
352
|
+
if (this.opts.useLocks) {
|
|
353
|
+
cmd.owner = this.workerId;
|
|
354
|
+
if (token)
|
|
355
|
+
cmd.token = token;
|
|
356
|
+
}
|
|
357
|
+
const response = await this.tcp.send(cmd);
|
|
358
|
+
if (!response.ok || !response.job)
|
|
359
|
+
return undefined;
|
|
360
|
+
const job = parseJobFromResponse(response.job, this.name);
|
|
361
|
+
const jobIdStr = String(job.id);
|
|
362
|
+
this.pulledJobIds.add(jobIdStr);
|
|
363
|
+
if (this.opts.useLocks && response.token) {
|
|
364
|
+
this.jobTokens.set(jobIdStr, response.token);
|
|
365
|
+
}
|
|
366
|
+
return job;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Manually process a job (BullMQ v5 compatible).
|
|
370
|
+
* This is for advanced use cases where you need manual control over job processing.
|
|
371
|
+
*
|
|
372
|
+
* @param job - The job to process
|
|
373
|
+
* @param token - Lock token for job ownership
|
|
374
|
+
* @param fetchNextCallback - Optional callback to fetch next job after completion
|
|
375
|
+
* @returns The processed job or void
|
|
376
|
+
*/
|
|
377
|
+
async processJobManually(job, token, fetchNextCallback) {
|
|
378
|
+
if (this.closed)
|
|
379
|
+
return undefined;
|
|
380
|
+
const jobIdStr = String(job.id);
|
|
381
|
+
this.activeJobs++;
|
|
382
|
+
this.activeJobIds.add(jobIdStr);
|
|
383
|
+
this.pulledJobIds.add(jobIdStr);
|
|
384
|
+
if (this.opts.useLocks && token) {
|
|
385
|
+
this.jobTokens.set(jobIdStr, token);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
await processJob(job, {
|
|
389
|
+
name: this.name,
|
|
390
|
+
processor: this.processor,
|
|
391
|
+
embedded: this.embedded,
|
|
392
|
+
tcp: this.tcp,
|
|
393
|
+
ackBatcher: this.ackBatcher,
|
|
394
|
+
emitter: this,
|
|
395
|
+
token: this.opts.useLocks ? token : undefined,
|
|
396
|
+
});
|
|
397
|
+
// Fetch next job if callback provided
|
|
398
|
+
if (fetchNextCallback) {
|
|
399
|
+
return await fetchNextCallback();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
this.activeJobs--;
|
|
404
|
+
this.activeJobIds.delete(jobIdStr);
|
|
405
|
+
this.pulledJobIds.delete(jobIdStr);
|
|
406
|
+
this.cancelledJobs.delete(jobIdStr);
|
|
407
|
+
if (this.opts.useLocks) {
|
|
408
|
+
this.jobTokens.delete(jobIdStr);
|
|
409
|
+
}
|
|
410
|
+
this.recordJobForLimiter();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Extend locks on multiple jobs (BullMQ v5 compatible).
|
|
415
|
+
* Used to prevent jobs from being considered stalled during long processing.
|
|
416
|
+
*
|
|
417
|
+
* @param jobIds - Array of job IDs to extend locks for
|
|
418
|
+
* @param tokens - Array of lock tokens corresponding to each job
|
|
419
|
+
* @param duration - Duration in milliseconds to extend the lock
|
|
420
|
+
* @returns Number of locks successfully extended
|
|
421
|
+
*/
|
|
422
|
+
async extendJobLocks(jobIds, tokens, duration) {
|
|
423
|
+
if (this.closed || jobIds.length === 0)
|
|
424
|
+
return 0;
|
|
425
|
+
if (jobIds.length !== tokens.length) {
|
|
426
|
+
throw new Error('jobIds and tokens arrays must have the same length');
|
|
427
|
+
}
|
|
428
|
+
if (this.embedded) {
|
|
429
|
+
const manager = getSharedManager();
|
|
430
|
+
let extended = 0;
|
|
431
|
+
for (let i = 0; i < jobIds.length; i++) {
|
|
432
|
+
const success = await manager.extendLock(jobIds[i], tokens[i], duration);
|
|
433
|
+
if (success)
|
|
434
|
+
extended++;
|
|
435
|
+
}
|
|
436
|
+
return extended;
|
|
437
|
+
}
|
|
438
|
+
// TCP mode
|
|
439
|
+
if (!this.tcp)
|
|
440
|
+
return 0;
|
|
441
|
+
const response = await this.tcp.send({
|
|
442
|
+
cmd: 'ExtendLocks',
|
|
443
|
+
ids: jobIds,
|
|
444
|
+
tokens,
|
|
445
|
+
duration,
|
|
446
|
+
});
|
|
447
|
+
const extended = response.extended;
|
|
448
|
+
return extended ?? 0;
|
|
449
|
+
}
|
|
106
450
|
/** Close worker gracefully */
|
|
107
451
|
async close(force = false) {
|
|
452
|
+
if (this.closed)
|
|
453
|
+
return;
|
|
108
454
|
this.closing = true;
|
|
109
455
|
this.running = false;
|
|
456
|
+
this.paused = false;
|
|
110
457
|
if (this.pollTimer) {
|
|
111
458
|
clearTimeout(this.pollTimer);
|
|
112
459
|
this.pollTimer = null;
|
|
@@ -129,14 +476,22 @@ export class Worker extends EventEmitter {
|
|
|
129
476
|
this.ackBatcher.stop();
|
|
130
477
|
// Small delay to ensure TCP responses are processed
|
|
131
478
|
await new Promise((r) => setTimeout(r, 100));
|
|
479
|
+
// Unsubscribe from stalled events
|
|
480
|
+
if (this.stalledUnsubscribe) {
|
|
481
|
+
this.stalledUnsubscribe();
|
|
482
|
+
this.stalledUnsubscribe = null;
|
|
483
|
+
}
|
|
132
484
|
// Clear tracking sets
|
|
133
485
|
this.activeJobIds.clear();
|
|
134
486
|
this.pulledJobIds.clear();
|
|
135
487
|
this.jobTokens.clear();
|
|
488
|
+
this.cancelledJobs.clear();
|
|
136
489
|
this.pendingJobs = [];
|
|
137
490
|
this.pendingJobsHead = 0;
|
|
138
491
|
if (this.tcpPool)
|
|
139
492
|
this.tcpPool.close();
|
|
493
|
+
this.closed = true;
|
|
494
|
+
this.closing = false;
|
|
140
495
|
this.emit('closed');
|
|
141
496
|
}
|
|
142
497
|
startHeartbeat() {
|
|
@@ -187,6 +542,14 @@ export class Worker extends EventEmitter {
|
|
|
187
542
|
}, 10);
|
|
188
543
|
return;
|
|
189
544
|
}
|
|
545
|
+
// Check rate limiter
|
|
546
|
+
if (!this.canProcessWithinLimit()) {
|
|
547
|
+
const waitTime = this.getTimeUntilNextSlot();
|
|
548
|
+
this.pollTimer = setTimeout(() => {
|
|
549
|
+
this.poll();
|
|
550
|
+
}, Math.max(waitTime, 10));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
190
553
|
void this.tryProcess();
|
|
191
554
|
}
|
|
192
555
|
async tryProcess() {
|
|
@@ -196,9 +559,9 @@ export class Worker extends EventEmitter {
|
|
|
196
559
|
let item = this.getBufferedJob();
|
|
197
560
|
if (!item) {
|
|
198
561
|
const items = await this.pullBatch();
|
|
199
|
-
// Re-check
|
|
562
|
+
// Re-check state after async operation (can be modified during await)
|
|
200
563
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
201
|
-
if (this.closing)
|
|
564
|
+
if (!this.running || this.closing)
|
|
202
565
|
return;
|
|
203
566
|
if (items.length > 0) {
|
|
204
567
|
// Register ALL pulled jobs for heartbeat tracking immediately
|
|
@@ -213,6 +576,12 @@ export class Worker extends EventEmitter {
|
|
|
213
576
|
this.startJob(item.job, item.token);
|
|
214
577
|
}
|
|
215
578
|
else {
|
|
579
|
+
// Emit drained event when queue is empty (throttled to avoid spam)
|
|
580
|
+
const now = Date.now();
|
|
581
|
+
if (now - this.lastDrainedEmit > 1000) {
|
|
582
|
+
this.lastDrainedEmit = now;
|
|
583
|
+
this.emit('drained');
|
|
584
|
+
}
|
|
216
585
|
const waitTime = this.opts.pollTimeout > 0 ? 10 : 50;
|
|
217
586
|
this.pollTimer = setTimeout(() => {
|
|
218
587
|
this.poll();
|
|
@@ -335,9 +704,12 @@ export class Worker extends EventEmitter {
|
|
|
335
704
|
this.activeJobs--;
|
|
336
705
|
this.activeJobIds.delete(jobIdStr);
|
|
337
706
|
this.pulledJobIds.delete(jobIdStr); // Remove from heartbeat tracking
|
|
707
|
+
this.cancelledJobs.delete(jobIdStr); // Clean up cancellation flag
|
|
338
708
|
if (this.opts.useLocks) {
|
|
339
709
|
this.jobTokens.delete(jobIdStr); // Clean up token
|
|
340
710
|
}
|
|
711
|
+
// Record job completion for rate limiter
|
|
712
|
+
this.recordJobForLimiter();
|
|
341
713
|
if (this.running && !this.closing)
|
|
342
714
|
this.poll();
|
|
343
715
|
});
|