Skip to content

Commit 3da9e1a

Browse files
committed
refactor: update job completion and failure handling in queue system
- Renamed `release` method to `completeJob` for clarity across queue implementations. - Introduced `failJob` method to handle job failures appropriately. - Updated error handling logic to return false when a job fails. - Enhanced tests to verify correct behavior for job completion and failure scenarios across different queue drivers.
1 parent 8eaaf0a commit 3da9e1a

8 files changed

Lines changed: 285 additions & 24 deletions

File tree

src/core/queue.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,11 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
264264
// Don't let plugin errors affect job completion
265265
}
266266

267-
// Complete the job if successful
267+
// Complete the job if successful, otherwise mark as failed
268268
if (success) {
269-
await this.release(message);
269+
await this.completeJob(message);
270+
} else {
271+
await this.failJob(message, jobError || new Error('Unknown job failure'));
270272
}
271273
}
272274
} finally {
@@ -320,7 +322,7 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
320322
*
321323
* @param message - The queue message that failed to process
322324
* @param error - The error that occurred during processing
323-
* @returns Promise resolving to true (job is considered handled despite the error)
325+
* @returns Promise resolving to false (job failed)
324326
* @protected
325327
*/
326328
protected async handleError(message: QueueMessage, error: unknown): Promise<boolean> {
@@ -336,7 +338,7 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
336338
this.emit('afterError', errorEvent);
337339
}
338340

339-
return true; // Job is considered handled (failed)
341+
return false; // Job failed
340342
}
341343

342344
protected async sleep(ms: number): Promise<void> {
@@ -367,14 +369,25 @@ export abstract class Queue<TJobMap = Record<string, any>, TJobRequest extends B
367369
protected abstract reserve(timeout: number): Promise<QueueMessage | null>;
368370

369371
/**
370-
* Releases a processed job from the queue (marks as complete).
372+
* Marks a job as successfully completed and removes it from the queue.
371373
*
372-
* @param message - The queue message to release
373-
* @returns Promise that resolves when job is released
374+
* @param message - The queue message to complete
375+
* @returns Promise that resolves when job is marked as complete
374376
* @protected
375377
* @abstract
376378
*/
377-
protected abstract release(message: QueueMessage): Promise<void>;
379+
protected abstract completeJob(message: QueueMessage): Promise<void>;
380+
381+
/**
382+
* Marks a job as failed and handles failure appropriately (remove or retry).
383+
*
384+
* @param message - The queue message that failed
385+
* @param error - The error that caused the failure
386+
* @returns Promise that resolves when job failure is handled
387+
* @protected
388+
* @abstract
389+
*/
390+
protected abstract failJob(message: QueueMessage, error: unknown): Promise<void>;
378391

379392
/**
380393
* Retrieves the current status of a job by its ID.

src/drivers/db.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,15 @@ export class DbQueue<TJobMap = Record<string, any>> extends Queue<TJobMap, DbJob
3333
};
3434
}
3535

36-
protected async release(message: QueueMessage): Promise<void> {
36+
protected async completeJob(message: QueueMessage): Promise<void> {
3737
await this.db.completeJob(message.id);
3838
}
3939

40+
protected async failJob(message: QueueMessage, error: unknown): Promise<void> {
41+
const errorMessage = error instanceof Error ? error.message : String(error);
42+
await this.db.failJob(message.id, errorMessage);
43+
}
44+
4045
async status(id: string): Promise<JobStatus> {
4146
const status = await this.db.getJobStatus(id);
4247
return status || 'done';

src/drivers/file.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,13 @@ export class FileQueue<TJobMap = Record<string, any>> extends Queue<TJobMap, Fil
148148
}
149149
}
150150

151-
protected async release(message: QueueMessage): Promise<void> {
151+
protected async completeJob(message: QueueMessage): Promise<void> {
152+
await this.complete(message.id);
153+
}
154+
155+
protected async failJob(message: QueueMessage, error: unknown): Promise<void> {
156+
// For file queue, we'll remove failed jobs too (similar to complete)
157+
// Future enhancement could track failed jobs in the index
152158
await this.complete(message.id);
153159
}
154160

src/drivers/redis.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export class RedisQueue<TJobMap = Record<string, any>> extends Queue<TJobMap, Re
216216
};
217217
}
218218

219-
protected async release(message: QueueMessage): Promise<void> {
219+
protected async completeJob(message: QueueMessage): Promise<void> {
220220
// Remove from reserved queue
221221
await this.redis.zrem(this.reservedKey, message.id);
222222

@@ -225,6 +225,15 @@ export class RedisQueue<TJobMap = Record<string, any>> extends Queue<TJobMap, Re
225225
await this.redis.hdel(this.attemptsKey, message.id);
226226
}
227227

228+
protected async failJob(message: QueueMessage, error: unknown): Promise<void> {
229+
// Remove from reserved queue
230+
await this.redis.zrem(this.reservedKey, message.id);
231+
232+
// Remove job data (Redis doesn't track failed job history by default)
233+
await this.redis.hdel(this.messagesKey, message.id);
234+
await this.redis.hdel(this.attemptsKey, message.id);
235+
}
236+
228237
async status(id: string): Promise<JobStatus> {
229238
// Check if job data exists
230239
const exists = await this.redis.hget(this.messagesKey, id);

src/drivers/sqs.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@ export class SqsQueue<TJobMap = Record<string, any>> extends Queue<
2222
TJobMap,
2323
SqsJobRequest<any>
2424
> {
25+
#onFailure: "delete" | "leaveInQueue"
26+
2527
constructor(
2628
private client: SQSClient,
2729
private queueUrl: string,
28-
options: QueueOptions = {}
30+
options: QueueOptions & { onFailure: "delete" | "leaveInQueue" } = {
31+
onFailure: "delete",
32+
}
2933
) {
3034
super(options);
3135
// SQS supports long polling via WaitTimeSeconds
3236
this.supportsLongPolling = true;
37+
this.#onFailure = options.onFailure;
3338
}
3439

3540
protected async pushMessage(payload: string, meta: JobMeta): Promise<string> {
@@ -57,7 +62,7 @@ export class SqsQueue<TJobMap = Record<string, any>> extends Queue<
5762
DelaySeconds: meta.delay || 0,
5863
MessageAttributes: messageAttributes,
5964
});
60-
65+
6166
const result = await this.client.send(command);
6267

6368
if (!result.MessageId) {
@@ -73,7 +78,7 @@ export class SqsQueue<TJobMap = Record<string, any>> extends Queue<
7378
WaitTimeSeconds: timeout,
7479
MessageAttributeNames: ["All"],
7580
});
76-
81+
7782
const result = await this.client.send(command);
7883

7984
if (!result.Messages || result.Messages.length === 0) {
@@ -105,7 +110,7 @@ export class SqsQueue<TJobMap = Record<string, any>> extends Queue<
105110
ReceiptHandle: message.ReceiptHandle,
106111
VisibilityTimeout: meta.ttr,
107112
});
108-
113+
109114
await this.client.send(visibilityCommand);
110115
}
111116

@@ -119,21 +124,43 @@ export class SqsQueue<TJobMap = Record<string, any>> extends Queue<
119124
};
120125
}
121126

122-
protected async release(message: QueueMessage): Promise<void> {
127+
protected async completeJob(message: QueueMessage): Promise<void> {
123128
if (!message.meta.receiptHandle) {
124129
throw new Error(
125-
"Cannot release SQS message: receiptHandle is missing from metadata"
130+
"Cannot complete SQS message: receiptHandle is missing from metadata"
126131
);
127132
}
128133

129134
const deleteCommand = new DeleteMessageCommand({
130135
QueueUrl: this.queueUrl,
131136
ReceiptHandle: message.meta.receiptHandle,
132137
});
133-
138+
134139
await this.client.send(deleteCommand);
135140
}
136141

142+
protected async failJob(
143+
message: QueueMessage,
144+
error: unknown
145+
): Promise<void> {
146+
if (this.#onFailure === "leaveInQueue") {
147+
if (!message.meta.receiptHandle) {
148+
throw new Error(
149+
"Cannot fail SQS message: receiptHandle is missing from metadata"
150+
);
151+
}
152+
153+
// For SQS, we delete failed messages too since SQS doesn't track failure states
154+
// Future enhancement could send to a dead letter queue
155+
const deleteCommand = new DeleteMessageCommand({
156+
QueueUrl: this.queueUrl,
157+
ReceiptHandle: message.meta.receiptHandle,
158+
});
159+
160+
await this.client.send(deleteCommand);
161+
}
162+
}
163+
137164
async status(id: string): Promise<JobStatus> {
138165
throw new Error("SQS does not support status");
139166
}

0 commit comments

Comments
 (0)