light-async-queue 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -30
- package/dist/src/constants.d.ts +24 -5
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +20 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/dashboard/Dashboard.d.ts +70 -0
- package/dist/src/dashboard/Dashboard.d.ts.map +1 -0
- package/dist/src/dashboard/Dashboard.js +308 -0
- package/dist/src/dashboard/Dashboard.js.map +1 -0
- package/dist/src/dashboard/index.d.ts +3 -0
- package/dist/src/dashboard/index.d.ts.map +1 -0
- package/dist/src/dashboard/index.js +2 -0
- package/dist/src/dashboard/index.js.map +1 -0
- package/dist/src/index.d.ts +13 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +11 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/queue/Job.d.ts +35 -4
- package/dist/src/queue/Job.d.ts.map +1 -1
- package/dist/src/queue/Job.js +92 -8
- package/dist/src/queue/Job.js.map +1 -1
- package/dist/src/queue/Queue.d.ts +73 -3
- package/dist/src/queue/Queue.d.ts.map +1 -1
- package/dist/src/queue/Queue.js +357 -35
- package/dist/src/queue/Queue.js.map +1 -1
- package/dist/src/queue/Scheduler.d.ts.map +1 -1
- package/dist/src/queue/Scheduler.js +8 -1
- package/dist/src/queue/Scheduler.js.map +1 -1
- package/dist/src/types.d.ts +79 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/CronParser.d.ts +12 -0
- package/dist/src/utils/CronParser.d.ts.map +1 -0
- package/dist/src/utils/CronParser.js +28 -0
- package/dist/src/utils/CronParser.js.map +1 -0
- package/dist/src/utils/RateLimiter.d.ts +37 -0
- package/dist/src/utils/RateLimiter.d.ts.map +1 -0
- package/dist/src/utils/RateLimiter.js +68 -0
- package/dist/src/utils/RateLimiter.js.map +1 -0
- package/dist/src/utils/WebhookManager.d.ts +29 -0
- package/dist/src/utils/WebhookManager.d.ts.map +1 -0
- package/dist/src/utils/WebhookManager.js +82 -0
- package/dist/src/utils/WebhookManager.js.map +1 -0
- package/dist/src/worker/Worker.d.ts +2 -2
- package/dist/src/worker/Worker.d.ts.map +1 -1
- package/dist/src/worker/Worker.js +57 -36
- package/dist/src/worker/Worker.js.map +1 -1
- package/dist/src/worker/childProcessor.js +17 -1
- package/dist/src/worker/childProcessor.js.map +1 -1
- package/package.json +27 -5
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { QueueConfig, JobProcessor, JobData } from
|
|
1
|
+
import { QueueConfig, JobProcessor, JobData, JobOptions } from "../types.js";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
2
3
|
/**
|
|
3
4
|
* Main Queue class - orchestrates job processing
|
|
4
5
|
*/
|
|
5
|
-
export declare class Queue {
|
|
6
|
+
export declare class Queue extends EventEmitter {
|
|
6
7
|
private config;
|
|
7
8
|
private storage;
|
|
8
9
|
private scheduler;
|
|
@@ -10,9 +11,15 @@ export declare class Queue {
|
|
|
10
11
|
private backoff;
|
|
11
12
|
private processor;
|
|
12
13
|
private workers;
|
|
14
|
+
private reservedWorkers;
|
|
13
15
|
private activeJobs;
|
|
16
|
+
private completedJobIds;
|
|
17
|
+
private repeatingJobs;
|
|
14
18
|
private isShuttingDown;
|
|
15
19
|
private isInitialized;
|
|
20
|
+
private rateLimiter?;
|
|
21
|
+
private webhookManager?;
|
|
22
|
+
private stalledCheckInterval?;
|
|
16
23
|
constructor(config: QueueConfig);
|
|
17
24
|
/**
|
|
18
25
|
* Initialize the queue
|
|
@@ -25,7 +32,7 @@ export declare class Queue {
|
|
|
25
32
|
/**
|
|
26
33
|
* Add a job to the queue
|
|
27
34
|
*/
|
|
28
|
-
add(payload: unknown): Promise<string>;
|
|
35
|
+
add(payload: unknown, options?: JobOptions): Promise<string>;
|
|
29
36
|
/**
|
|
30
37
|
* Handle a job that's ready to process
|
|
31
38
|
*/
|
|
@@ -38,6 +45,62 @@ export declare class Queue {
|
|
|
38
45
|
* Get an available worker or create a new one
|
|
39
46
|
*/
|
|
40
47
|
private getAvailableWorker;
|
|
48
|
+
/**
|
|
49
|
+
* Create a job with methods for use in processor
|
|
50
|
+
*/
|
|
51
|
+
private createJobWithMethods;
|
|
52
|
+
/**
|
|
53
|
+
* Load completed job IDs for dependency tracking
|
|
54
|
+
*/
|
|
55
|
+
private loadCompletedJobIds;
|
|
56
|
+
/**
|
|
57
|
+
* Check and update waiting jobs whose dependencies are now satisfied
|
|
58
|
+
*/
|
|
59
|
+
private checkDependentJobs;
|
|
60
|
+
/**
|
|
61
|
+
* Schedule a repeating job
|
|
62
|
+
*/
|
|
63
|
+
private scheduleRepeat;
|
|
64
|
+
/**
|
|
65
|
+
* Start stalled job checker
|
|
66
|
+
*/
|
|
67
|
+
private startStalledChecker;
|
|
68
|
+
/**
|
|
69
|
+
* Check for stalled jobs and mark them
|
|
70
|
+
*/
|
|
71
|
+
private checkStalledJobs;
|
|
72
|
+
/**
|
|
73
|
+
* Send webhook notification
|
|
74
|
+
*/
|
|
75
|
+
private sendWebhook;
|
|
76
|
+
/**
|
|
77
|
+
* Get a specific job by ID
|
|
78
|
+
*/
|
|
79
|
+
getJob(jobId: string): Promise<JobData | null>;
|
|
80
|
+
/**
|
|
81
|
+
* Get all jobs from the queue
|
|
82
|
+
*/
|
|
83
|
+
getAllJobs(): Promise<JobData[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Remove a specific job
|
|
86
|
+
*/
|
|
87
|
+
removeJob(jobId: string): Promise<boolean>;
|
|
88
|
+
/**
|
|
89
|
+
* Pause the queue (stop processing new jobs)
|
|
90
|
+
*/
|
|
91
|
+
pause(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Resume the queue
|
|
94
|
+
*/
|
|
95
|
+
resume(): void;
|
|
96
|
+
/**
|
|
97
|
+
* Drain the queue - process all pending jobs
|
|
98
|
+
*/
|
|
99
|
+
drain(): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Clean completed jobs older than a certain age
|
|
102
|
+
*/
|
|
103
|
+
clean(maxAge?: number): Promise<number>;
|
|
41
104
|
/**
|
|
42
105
|
* Get all failed jobs from DLQ
|
|
43
106
|
*/
|
|
@@ -46,14 +109,21 @@ export declare class Queue {
|
|
|
46
109
|
* Reprocess a failed job from DLQ
|
|
47
110
|
*/
|
|
48
111
|
reprocessFailed(jobId: string): Promise<boolean>;
|
|
112
|
+
/**
|
|
113
|
+
* Get queue statistics
|
|
114
|
+
*/
|
|
115
|
+
getConcurrency(): number;
|
|
49
116
|
/**
|
|
50
117
|
* Get queue statistics
|
|
51
118
|
*/
|
|
52
119
|
getStats(): Promise<{
|
|
53
120
|
active: number;
|
|
121
|
+
waiting: number;
|
|
122
|
+
delayed: number;
|
|
54
123
|
pending: number;
|
|
55
124
|
failed: number;
|
|
56
125
|
completed: number;
|
|
126
|
+
stalled: number;
|
|
57
127
|
}>;
|
|
58
128
|
/**
|
|
59
129
|
* Set up graceful shutdown handlers
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../src/queue/Queue.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"Queue.d.ts","sourceRoot":"","sources":["../../../src/queue/Queue.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,YAAY,EAEZ,OAAO,EAGP,UAAU,EAGX,MAAM,aAAa,CAAC;AAWrB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;GAEG;AACH,qBAAa,KAAM,SAAQ,YAAY;IACrC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,GAAG,CAAkB;IAC7B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAc;IAClC,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,oBAAoB,CAAC,CAAiB;gBAElC,MAAM,EAAE,WAAW;IAmD/B;;OAEG;YACW,UAAU;IAexB;;OAEG;IACH,OAAO,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI;IAItC;;OAEG;IACG,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IAkCtE;;OAEG;YACW,cAAc;IAuF5B;;OAEG;YACW,gBAAgB;IAwB9B;;OAEG;YACW,kBAAkB;IAqChC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAe5B;;OAEG;YACW,mBAAmB;IAUjC;;OAEG;YACW,kBAAkB;IAkBhC;;OAEG;YACW,cAAc;IAmD5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAQ3B;;OAEG;YACW,gBAAgB;IAoB9B;;OAEG;YACW,WAAW;IAazB;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAOpD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAOtC;;OAEG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAsBhD;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,MAAM,IAAI,IAAI;IAMd;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B5B;;OAEG;IACG,KAAK,CAAC,MAAM,GAAE,MAA4B,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBlE;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAOzC;;OAEG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBtD;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC;QACxB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IAmBF;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAwChC"}
|
package/dist/src/queue/Queue.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { StorageType, JobStatus } from
|
|
2
|
-
import { Job } from
|
|
3
|
-
import { Backoff } from
|
|
4
|
-
import { Scheduler } from
|
|
5
|
-
import { Worker } from
|
|
6
|
-
import { DeadLetterQueue } from
|
|
7
|
-
import { MemoryStore } from
|
|
8
|
-
import { FileStore } from
|
|
1
|
+
import { StorageType, JobStatus, QueueEventType, } from "../types.js";
|
|
2
|
+
import { Job } from "./Job.js";
|
|
3
|
+
import { Backoff } from "./Backoff.js";
|
|
4
|
+
import { Scheduler } from "./Scheduler.js";
|
|
5
|
+
import { Worker } from "../worker/Worker.js";
|
|
6
|
+
import { DeadLetterQueue } from "../dlq/DeadLetterQueue.js";
|
|
7
|
+
import { MemoryStore } from "../storage/MemoryStore.js";
|
|
8
|
+
import { FileStore } from "../storage/FileStore.js";
|
|
9
|
+
import { RateLimiter } from "../utils/RateLimiter.js";
|
|
10
|
+
import { WebhookManager } from "../utils/WebhookManager.js";
|
|
11
|
+
import { CronParser } from "../utils/CronParser.js";
|
|
12
|
+
import { EventEmitter } from "node:events";
|
|
9
13
|
/**
|
|
10
14
|
* Main Queue class - orchestrates job processing
|
|
11
15
|
*/
|
|
12
|
-
export class Queue {
|
|
16
|
+
export class Queue extends EventEmitter {
|
|
13
17
|
config;
|
|
14
18
|
storage;
|
|
15
19
|
scheduler;
|
|
@@ -17,14 +21,24 @@ export class Queue {
|
|
|
17
21
|
backoff;
|
|
18
22
|
processor;
|
|
19
23
|
workers;
|
|
24
|
+
reservedWorkers;
|
|
20
25
|
activeJobs;
|
|
26
|
+
completedJobIds;
|
|
27
|
+
repeatingJobs;
|
|
21
28
|
isShuttingDown;
|
|
22
29
|
isInitialized;
|
|
30
|
+
rateLimiter;
|
|
31
|
+
webhookManager;
|
|
32
|
+
stalledCheckInterval;
|
|
23
33
|
constructor(config) {
|
|
34
|
+
super();
|
|
24
35
|
this.config = config;
|
|
25
36
|
this.processor = null;
|
|
26
37
|
this.workers = [];
|
|
38
|
+
this.reservedWorkers = new Set();
|
|
27
39
|
this.activeJobs = new Map();
|
|
40
|
+
this.completedJobIds = new Set();
|
|
41
|
+
this.repeatingJobs = new Map();
|
|
28
42
|
this.isShuttingDown = false;
|
|
29
43
|
this.isInitialized = false;
|
|
30
44
|
// Initialize storage based on config
|
|
@@ -41,10 +55,19 @@ export class Queue {
|
|
|
41
55
|
this.scheduler = new Scheduler(this.storage);
|
|
42
56
|
this.dlq = new DeadLetterQueue(this.storage);
|
|
43
57
|
this.backoff = new Backoff(config.retry.backoff);
|
|
58
|
+
// Initialize rate limiter if configured
|
|
59
|
+
if (config.rateLimiter) {
|
|
60
|
+
this.rateLimiter = new RateLimiter(config.rateLimiter);
|
|
61
|
+
}
|
|
62
|
+
// Initialize webhook manager if configured
|
|
63
|
+
if (config.webhooks && config.webhooks.length > 0) {
|
|
64
|
+
this.webhookManager = new WebhookManager(config.webhooks);
|
|
65
|
+
}
|
|
44
66
|
// Set up scheduler event handler
|
|
45
|
-
this.scheduler.on(
|
|
46
|
-
this.handleJobReady(jobData).catch(error => {
|
|
47
|
-
console.error(
|
|
67
|
+
this.scheduler.on("job-ready", (jobData) => {
|
|
68
|
+
this.handleJobReady(jobData).catch((error) => {
|
|
69
|
+
console.error("[Queue] Error handling job:", error);
|
|
70
|
+
this.emit(QueueEventType.ERROR, error);
|
|
48
71
|
});
|
|
49
72
|
});
|
|
50
73
|
// Set up graceful shutdown
|
|
@@ -59,6 +82,10 @@ export class Queue {
|
|
|
59
82
|
}
|
|
60
83
|
await this.storage.initialize();
|
|
61
84
|
this.isInitialized = true;
|
|
85
|
+
// Start stalled job checker
|
|
86
|
+
this.startStalledChecker();
|
|
87
|
+
// Load completed job IDs for dependency tracking
|
|
88
|
+
await this.loadCompletedJobIds();
|
|
62
89
|
}
|
|
63
90
|
/**
|
|
64
91
|
* Set the job processor function
|
|
@@ -69,15 +96,28 @@ export class Queue {
|
|
|
69
96
|
/**
|
|
70
97
|
* Add a job to the queue
|
|
71
98
|
*/
|
|
72
|
-
async add(payload) {
|
|
99
|
+
async add(payload, options = {}) {
|
|
73
100
|
if (!this.isInitialized) {
|
|
74
101
|
await this.initialize();
|
|
75
102
|
}
|
|
76
103
|
if (this.isShuttingDown) {
|
|
77
|
-
throw new Error(
|
|
104
|
+
throw new Error("Queue is shutting down, cannot accept new jobs");
|
|
78
105
|
}
|
|
79
|
-
const job = new Job(payload, this.config.retry.maxAttempts);
|
|
106
|
+
const job = new Job(payload, this.config.retry.maxAttempts, options);
|
|
80
107
|
await this.storage.addJob(job.toData());
|
|
108
|
+
// Emit event based on job status
|
|
109
|
+
if (job.status === JobStatus.DELAYED) {
|
|
110
|
+
this.emit(QueueEventType.DELAYED, job.toData());
|
|
111
|
+
await this.sendWebhook(QueueEventType.DELAYED, { job: job.toData() });
|
|
112
|
+
}
|
|
113
|
+
else if (job.status === JobStatus.WAITING) {
|
|
114
|
+
this.emit(QueueEventType.WAITING, job.toData());
|
|
115
|
+
await this.sendWebhook(QueueEventType.WAITING, { job: job.toData() });
|
|
116
|
+
}
|
|
117
|
+
// Set up repeating job if configured
|
|
118
|
+
if (options.repeat) {
|
|
119
|
+
await this.scheduleRepeat(job);
|
|
120
|
+
}
|
|
81
121
|
// Start scheduler if not already running
|
|
82
122
|
if (!this.scheduler.getIsRunning()) {
|
|
83
123
|
this.scheduler.start();
|
|
@@ -100,50 +140,85 @@ export class Queue {
|
|
|
100
140
|
return;
|
|
101
141
|
}
|
|
102
142
|
if (!this.processor) {
|
|
103
|
-
console.error(
|
|
143
|
+
console.error("[Queue] No processor function set");
|
|
104
144
|
return;
|
|
105
145
|
}
|
|
106
146
|
const job = Job.fromData(jobData);
|
|
147
|
+
// Check if dependencies are satisfied
|
|
148
|
+
if (!job.areDependenciesSatisfied(this.completedJobIds)) {
|
|
149
|
+
return; // Job is still waiting for dependencies
|
|
150
|
+
}
|
|
151
|
+
// Apply rate limiting if configured
|
|
152
|
+
if (this.rateLimiter) {
|
|
153
|
+
const allowed = await this.rateLimiter.consume();
|
|
154
|
+
if (!allowed) {
|
|
155
|
+
return; // Rate limit reached, will retry on next tick
|
|
156
|
+
}
|
|
157
|
+
}
|
|
107
158
|
// Mark job as processing
|
|
108
159
|
job.markProcessing();
|
|
109
160
|
this.activeJobs.set(job.id, job);
|
|
110
161
|
await this.storage.updateJob(job.toData());
|
|
162
|
+
// Emit active event
|
|
163
|
+
this.emit(QueueEventType.ACTIVE, job.toData());
|
|
164
|
+
await this.sendWebhook(QueueEventType.ACTIVE, { job: job.toData() });
|
|
111
165
|
// Get or create a worker
|
|
112
166
|
const worker = await this.getAvailableWorker();
|
|
113
167
|
try {
|
|
168
|
+
// Create job with methods for processor
|
|
169
|
+
const jobWithMethods = this.createJobWithMethods(job);
|
|
114
170
|
// Execute job in worker
|
|
115
|
-
const result = await worker.execute(job.toData());
|
|
171
|
+
const result = await worker.execute(job.toData(), jobWithMethods);
|
|
116
172
|
if (result.success) {
|
|
117
173
|
// Job succeeded
|
|
118
|
-
job.markCompleted();
|
|
174
|
+
job.markCompleted(result.result);
|
|
119
175
|
await this.storage.updateJob(job.toData());
|
|
176
|
+
// Track completed job for dependency resolution
|
|
177
|
+
this.completedJobIds.add(job.id);
|
|
178
|
+
// Check for dependent jobs
|
|
179
|
+
await this.checkDependentJobs(job.id);
|
|
180
|
+
// Emit completed event
|
|
181
|
+
this.emit(QueueEventType.COMPLETED, job.toData(), result.result);
|
|
182
|
+
await this.sendWebhook(QueueEventType.COMPLETED, {
|
|
183
|
+
job: job.toData(),
|
|
184
|
+
result: result.result,
|
|
185
|
+
});
|
|
120
186
|
}
|
|
121
187
|
else {
|
|
122
188
|
// Job failed
|
|
123
|
-
|
|
189
|
+
const error = new Error(result.error || "Unknown error");
|
|
190
|
+
await this.handleJobFailure(job, error);
|
|
124
191
|
}
|
|
125
192
|
}
|
|
126
193
|
catch (error) {
|
|
127
194
|
// Worker execution error
|
|
128
|
-
|
|
195
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
196
|
+
await this.handleJobFailure(job, err);
|
|
129
197
|
}
|
|
130
198
|
finally {
|
|
131
199
|
// Remove from active jobs
|
|
132
200
|
this.activeJobs.delete(job.id);
|
|
201
|
+
this.reservedWorkers.delete(worker);
|
|
133
202
|
}
|
|
134
203
|
}
|
|
135
204
|
/**
|
|
136
205
|
* Handle job failure with retry logic
|
|
137
206
|
*/
|
|
138
207
|
async handleJobFailure(job, error) {
|
|
139
|
-
console.error(`[Queue] Job ${job.id} failed:`, error);
|
|
208
|
+
console.error(`[Queue] Job ${job.id} failed:`, error.message);
|
|
140
209
|
// Calculate next run time with backoff
|
|
141
210
|
const nextRunAt = this.backoff.getNextRunAt(job.attempts + 1);
|
|
142
|
-
job.markFailed(nextRunAt);
|
|
211
|
+
job.markFailed(error.message, nextRunAt);
|
|
143
212
|
if (job.hasExceededMaxAttempts()) {
|
|
144
213
|
// Move to dead letter queue
|
|
145
214
|
console.log(`[Queue] Job ${job.id} exceeded max attempts, moving to DLQ`);
|
|
146
215
|
await this.dlq.add(job);
|
|
216
|
+
// Emit failed event
|
|
217
|
+
this.emit(QueueEventType.FAILED, job.toData(), error);
|
|
218
|
+
await this.sendWebhook(QueueEventType.FAILED, {
|
|
219
|
+
job: job.toData(),
|
|
220
|
+
error,
|
|
221
|
+
});
|
|
147
222
|
}
|
|
148
223
|
else {
|
|
149
224
|
// Update job for retry
|
|
@@ -156,26 +231,29 @@ export class Queue {
|
|
|
156
231
|
async getAvailableWorker() {
|
|
157
232
|
// Find an idle worker
|
|
158
233
|
for (const worker of this.workers) {
|
|
159
|
-
if (!worker.isBusy()) {
|
|
234
|
+
if (!worker.isBusy() && !this.reservedWorkers.has(worker)) {
|
|
235
|
+
this.reservedWorkers.add(worker);
|
|
160
236
|
return worker;
|
|
161
237
|
}
|
|
162
238
|
}
|
|
163
239
|
// Create a new worker if under concurrency limit
|
|
164
240
|
if (this.workers.length < this.config.concurrency) {
|
|
165
241
|
if (!this.processor) {
|
|
166
|
-
throw new Error(
|
|
242
|
+
throw new Error("Processor function not set");
|
|
167
243
|
}
|
|
168
244
|
const worker = new Worker(this.processor);
|
|
169
245
|
await worker.initialize();
|
|
170
246
|
this.workers.push(worker);
|
|
247
|
+
this.reservedWorkers.add(worker);
|
|
171
248
|
return worker;
|
|
172
249
|
}
|
|
173
250
|
// Wait for a worker to become available
|
|
174
251
|
return new Promise((resolve) => {
|
|
175
252
|
const checkInterval = setInterval(() => {
|
|
176
253
|
for (const worker of this.workers) {
|
|
177
|
-
if (!worker.isBusy()) {
|
|
254
|
+
if (!worker.isBusy() && !this.reservedWorkers.has(worker)) {
|
|
178
255
|
clearInterval(checkInterval);
|
|
256
|
+
this.reservedWorkers.add(worker);
|
|
179
257
|
resolve(worker);
|
|
180
258
|
return;
|
|
181
259
|
}
|
|
@@ -183,6 +261,232 @@ export class Queue {
|
|
|
183
261
|
}, 100);
|
|
184
262
|
});
|
|
185
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Create a job with methods for use in processor
|
|
266
|
+
*/
|
|
267
|
+
createJobWithMethods(job) {
|
|
268
|
+
return {
|
|
269
|
+
...job.toData(),
|
|
270
|
+
updateProgress: async (progress) => {
|
|
271
|
+
job.updateProgress(progress);
|
|
272
|
+
await this.storage.updateJob(job.toData());
|
|
273
|
+
this.emit(QueueEventType.PROGRESS, job.toData(), progress);
|
|
274
|
+
await this.sendWebhook(QueueEventType.PROGRESS, { job: job.toData() });
|
|
275
|
+
},
|
|
276
|
+
log: (message) => {
|
|
277
|
+
console.log(`[Job ${job.id}] ${message}`);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Load completed job IDs for dependency tracking
|
|
283
|
+
*/
|
|
284
|
+
async loadCompletedJobIds() {
|
|
285
|
+
const allJobs = await this.storage.getAllJobs();
|
|
286
|
+
this.completedJobIds.clear();
|
|
287
|
+
for (const job of allJobs) {
|
|
288
|
+
if (job.status === JobStatus.COMPLETED) {
|
|
289
|
+
this.completedJobIds.add(job.id);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Check and update waiting jobs whose dependencies are now satisfied
|
|
295
|
+
*/
|
|
296
|
+
async checkDependentJobs(completedJobId) {
|
|
297
|
+
const allJobs = await this.storage.getAllJobs();
|
|
298
|
+
for (const jobData of allJobs) {
|
|
299
|
+
if (jobData.status === JobStatus.WAITING &&
|
|
300
|
+
jobData.dependsOn?.includes(completedJobId)) {
|
|
301
|
+
const job = Job.fromData(jobData);
|
|
302
|
+
if (job.areDependenciesSatisfied(this.completedJobIds)) {
|
|
303
|
+
// All dependencies satisfied, move to pending
|
|
304
|
+
job.status = JobStatus.PENDING;
|
|
305
|
+
await this.storage.updateJob(job.toData());
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Schedule a repeating job
|
|
312
|
+
*/
|
|
313
|
+
async scheduleRepeat(job) {
|
|
314
|
+
if (!job.repeatConfig) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const repeatConfig = job.repeatConfig;
|
|
318
|
+
// Check if we've hit the repeat limit
|
|
319
|
+
if (repeatConfig.limit && job.repeatCount >= repeatConfig.limit) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Calculate next run time
|
|
323
|
+
let nextRunAt;
|
|
324
|
+
if (repeatConfig.pattern) {
|
|
325
|
+
// Cron pattern
|
|
326
|
+
const cronParser = new CronParser(repeatConfig.pattern);
|
|
327
|
+
nextRunAt = cronParser.getNextRunTime(Date.now());
|
|
328
|
+
}
|
|
329
|
+
else if (repeatConfig.every) {
|
|
330
|
+
// Repeat every X ms
|
|
331
|
+
nextRunAt = Date.now() + repeatConfig.every;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// Check date constraints
|
|
337
|
+
if (repeatConfig.startDate &&
|
|
338
|
+
nextRunAt < repeatConfig.startDate.getTime()) {
|
|
339
|
+
nextRunAt = repeatConfig.startDate.getTime();
|
|
340
|
+
}
|
|
341
|
+
if (repeatConfig.endDate && nextRunAt > repeatConfig.endDate.getTime()) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Schedule the next instance
|
|
345
|
+
const delay = nextRunAt - Date.now();
|
|
346
|
+
const timeout = setTimeout(async () => {
|
|
347
|
+
const nextJob = job.createRepeatInstance();
|
|
348
|
+
nextJob.nextRunAt = nextRunAt;
|
|
349
|
+
await this.storage.addJob(nextJob.toData());
|
|
350
|
+
// Schedule the next repeat
|
|
351
|
+
await this.scheduleRepeat(nextJob);
|
|
352
|
+
}, delay);
|
|
353
|
+
this.repeatingJobs.set(job.id, timeout);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Start stalled job checker
|
|
357
|
+
*/
|
|
358
|
+
startStalledChecker() {
|
|
359
|
+
const interval = this.config.stalledInterval || 30000;
|
|
360
|
+
this.stalledCheckInterval = setInterval(async () => {
|
|
361
|
+
await this.checkStalledJobs();
|
|
362
|
+
}, interval);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Check for stalled jobs and mark them
|
|
366
|
+
*/
|
|
367
|
+
async checkStalledJobs() {
|
|
368
|
+
const stalledThreshold = this.config.stalledInterval || 30000;
|
|
369
|
+
const allJobs = await this.storage.getAllJobs();
|
|
370
|
+
for (const jobData of allJobs) {
|
|
371
|
+
if (jobData.status === JobStatus.PROCESSING) {
|
|
372
|
+
const job = Job.fromData(jobData);
|
|
373
|
+
if (job.isStalled(stalledThreshold)) {
|
|
374
|
+
console.warn(`[Queue] Job ${job.id} appears stalled`);
|
|
375
|
+
job.markStalled();
|
|
376
|
+
await this.storage.updateJob(job.toData());
|
|
377
|
+
// Emit stalled event
|
|
378
|
+
this.emit(QueueEventType.STALLED, job.toData());
|
|
379
|
+
await this.sendWebhook(QueueEventType.STALLED, { job: job.toData() });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Send webhook notification
|
|
386
|
+
*/
|
|
387
|
+
async sendWebhook(event, data) {
|
|
388
|
+
if (this.webhookManager) {
|
|
389
|
+
try {
|
|
390
|
+
await this.webhookManager.sendEvent(event, data);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.error("[Queue] Webhook error:", error);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get a specific job by ID
|
|
399
|
+
*/
|
|
400
|
+
async getJob(jobId) {
|
|
401
|
+
if (!this.isInitialized) {
|
|
402
|
+
await this.initialize();
|
|
403
|
+
}
|
|
404
|
+
return this.storage.getJob(jobId);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get all jobs from the queue
|
|
408
|
+
*/
|
|
409
|
+
async getAllJobs() {
|
|
410
|
+
if (!this.isInitialized) {
|
|
411
|
+
await this.initialize();
|
|
412
|
+
}
|
|
413
|
+
return this.storage.getAllJobs();
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Remove a specific job
|
|
417
|
+
*/
|
|
418
|
+
async removeJob(jobId) {
|
|
419
|
+
if (!this.isInitialized) {
|
|
420
|
+
await this.initialize();
|
|
421
|
+
}
|
|
422
|
+
// Check if job is active
|
|
423
|
+
if (this.activeJobs.has(jobId)) {
|
|
424
|
+
return false; // Cannot remove active job
|
|
425
|
+
}
|
|
426
|
+
const job = await this.storage.getJob(jobId);
|
|
427
|
+
if (!job) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
// Remove from storage by updating status
|
|
431
|
+
job.status = JobStatus.FAILED;
|
|
432
|
+
await this.storage.updateJob(job);
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Pause the queue (stop processing new jobs)
|
|
437
|
+
*/
|
|
438
|
+
pause() {
|
|
439
|
+
this.scheduler.stop();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Resume the queue
|
|
443
|
+
*/
|
|
444
|
+
resume() {
|
|
445
|
+
if (!this.isShuttingDown) {
|
|
446
|
+
this.scheduler.start();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Drain the queue - process all pending jobs
|
|
451
|
+
*/
|
|
452
|
+
async drain() {
|
|
453
|
+
if (!this.isInitialized) {
|
|
454
|
+
await this.initialize();
|
|
455
|
+
}
|
|
456
|
+
// Wait for all pending jobs to be processed
|
|
457
|
+
while (true) {
|
|
458
|
+
const allJobs = await this.storage.getAllJobs();
|
|
459
|
+
const pendingJobs = allJobs.filter((j) => j.status === JobStatus.PENDING ||
|
|
460
|
+
j.status === JobStatus.WAITING ||
|
|
461
|
+
j.status === JobStatus.DELAYED);
|
|
462
|
+
if (pendingJobs.length === 0 && this.activeJobs.size === 0) {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
466
|
+
}
|
|
467
|
+
this.emit(QueueEventType.DRAINED);
|
|
468
|
+
await this.sendWebhook(QueueEventType.DRAINED, {});
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Clean completed jobs older than a certain age
|
|
472
|
+
*/
|
|
473
|
+
async clean(maxAge = 24 * 60 * 60 * 1000) {
|
|
474
|
+
if (!this.isInitialized) {
|
|
475
|
+
await this.initialize();
|
|
476
|
+
}
|
|
477
|
+
const allJobs = await this.storage.getAllJobs();
|
|
478
|
+
const now = Date.now();
|
|
479
|
+
let cleaned = 0;
|
|
480
|
+
for (const job of allJobs) {
|
|
481
|
+
if (job.status === JobStatus.COMPLETED && now - job.updatedAt > maxAge) {
|
|
482
|
+
job.status = JobStatus.FAILED; // Mark for removal
|
|
483
|
+
await this.storage.updateJob(job);
|
|
484
|
+
this.completedJobIds.delete(job.id);
|
|
485
|
+
cleaned++;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return cleaned;
|
|
489
|
+
}
|
|
186
490
|
/**
|
|
187
491
|
* Get all failed jobs from DLQ
|
|
188
492
|
*/
|
|
@@ -207,6 +511,12 @@ export class Queue {
|
|
|
207
511
|
await this.storage.addJob(job.toData());
|
|
208
512
|
return true;
|
|
209
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Get queue statistics
|
|
516
|
+
*/
|
|
517
|
+
getConcurrency() {
|
|
518
|
+
return this.config.concurrency;
|
|
519
|
+
}
|
|
210
520
|
/**
|
|
211
521
|
* Get queue statistics
|
|
212
522
|
*/
|
|
@@ -218,9 +528,12 @@ export class Queue {
|
|
|
218
528
|
const failedJobs = await this.dlq.getAll();
|
|
219
529
|
return {
|
|
220
530
|
active: this.activeJobs.size,
|
|
221
|
-
|
|
531
|
+
waiting: allJobs.filter((j) => j.status === JobStatus.WAITING).length,
|
|
532
|
+
delayed: allJobs.filter((j) => j.status === JobStatus.DELAYED).length,
|
|
533
|
+
pending: allJobs.filter((j) => j.status === JobStatus.PENDING).length,
|
|
222
534
|
failed: failedJobs.length,
|
|
223
|
-
completed: allJobs.filter(j => j.status === JobStatus.COMPLETED).length,
|
|
535
|
+
completed: allJobs.filter((j) => j.status === JobStatus.COMPLETED).length,
|
|
536
|
+
stalled: allJobs.filter((j) => j.status === JobStatus.STALLED).length,
|
|
224
537
|
};
|
|
225
538
|
}
|
|
226
539
|
/**
|
|
@@ -228,12 +541,12 @@ export class Queue {
|
|
|
228
541
|
*/
|
|
229
542
|
setupGracefulShutdown() {
|
|
230
543
|
const shutdown = async () => {
|
|
231
|
-
console.log(
|
|
544
|
+
console.log("[Queue] Graceful shutdown initiated...");
|
|
232
545
|
await this.shutdown();
|
|
233
546
|
process.exit(0);
|
|
234
547
|
};
|
|
235
|
-
process.on(
|
|
236
|
-
process.on(
|
|
548
|
+
process.on("SIGINT", shutdown);
|
|
549
|
+
process.on("SIGTERM", shutdown);
|
|
237
550
|
}
|
|
238
551
|
/**
|
|
239
552
|
* Gracefully shutdown the queue
|
|
@@ -245,18 +558,27 @@ export class Queue {
|
|
|
245
558
|
this.isShuttingDown = true;
|
|
246
559
|
// Stop accepting new jobs
|
|
247
560
|
this.scheduler.stop();
|
|
561
|
+
// Stop stalled checker
|
|
562
|
+
if (this.stalledCheckInterval) {
|
|
563
|
+
clearInterval(this.stalledCheckInterval);
|
|
564
|
+
}
|
|
565
|
+
// Cancel all repeating jobs
|
|
566
|
+
for (const timeout of this.repeatingJobs.values()) {
|
|
567
|
+
clearTimeout(timeout);
|
|
568
|
+
}
|
|
569
|
+
this.repeatingJobs.clear();
|
|
248
570
|
// Wait for active jobs to complete
|
|
249
571
|
console.log(`[Queue] Waiting for ${this.activeJobs.size} active jobs to complete...`);
|
|
250
572
|
while (this.activeJobs.size > 0) {
|
|
251
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
573
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
252
574
|
}
|
|
253
575
|
// Terminate all workers
|
|
254
|
-
console.log(
|
|
255
|
-
await Promise.all(this.workers.map(worker => worker.terminate()));
|
|
576
|
+
console.log("[Queue] Terminating workers...");
|
|
577
|
+
await Promise.all(this.workers.map((worker) => worker.terminate()));
|
|
256
578
|
// Close storage
|
|
257
|
-
console.log(
|
|
579
|
+
console.log("[Queue] Closing storage...");
|
|
258
580
|
await this.storage.close();
|
|
259
|
-
console.log(
|
|
581
|
+
console.log("[Queue] Shutdown complete");
|
|
260
582
|
}
|
|
261
583
|
}
|
|
262
584
|
//# sourceMappingURL=Queue.js.map
|