@zudojs/queue 1.4.0 → 1.5.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
CHANGED
|
@@ -57,6 +57,35 @@ console.log(await queue.getStats());
|
|
|
57
57
|
await queue.close();
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
## Dead-letter store
|
|
61
|
+
|
|
62
|
+
Each queue keeps the most recent 1000 dead-lettered jobs in a store of its own,
|
|
63
|
+
cleared by `close()`. To choose the cap, or to keep the jobs after the queue
|
|
64
|
+
closes, pass a store as `deadLetterStore`. No type annotation is needed, for
|
|
65
|
+
an untyped store or one typed for the queue's payload.
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import {
|
|
69
|
+
createInMemoryDeadLetterStore,
|
|
70
|
+
createInMemoryQueue,
|
|
71
|
+
createQueueName,
|
|
72
|
+
} from "@zudojs/queue";
|
|
73
|
+
|
|
74
|
+
// Keep only the 50 most recent failures.
|
|
75
|
+
const store = createInMemoryDeadLetterStore({ maxEntries: 50 });
|
|
76
|
+
const broken = createInMemoryQueue(createQueueName("broken"), {
|
|
77
|
+
deadLetterStore: store,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// A store typed for the payload, so its entries read as DeadLetterJob<Email>.
|
|
81
|
+
const failedEmails = createInMemoryDeadLetterStore<Email>();
|
|
82
|
+
const emails = createInMemoryQueue<Email>(createQueueName("emails"), {
|
|
83
|
+
deadLetterStore: failedEmails,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`close()` leaves a store you passed in alone; its contents are yours.
|
|
88
|
+
|
|
60
89
|
## Features
|
|
61
90
|
|
|
62
91
|
- In-memory queue for development and testing
|
|
@@ -136,6 +165,32 @@ queue.events?.on("job:completed", ({ job, result }) => {
|
|
|
136
165
|
});
|
|
137
166
|
```
|
|
138
167
|
|
|
168
|
+
`job:failed` fires on **every** failed attempt, not only the last, and a
|
|
169
|
+
processor failure reports `job.state === "failed"` on a retryable attempt
|
|
170
|
+
and on the final one alike, with `job.attempt` still counting the attempts
|
|
171
|
+
before this one. What follows tells them apart:
|
|
172
|
+
|
|
173
|
+
| Attempt | `job:failed` `job.state` | Then |
|
|
174
|
+
|---------|--------------------------|------|
|
|
175
|
+
| Retryable (attempts left) | `"failed"` | `job:retrying` (`"retrying"`); after the backoff the job is `"waiting"` again |
|
|
176
|
+
| Final (attempts exhausted) | `"failed"` | `job:dead-lettered` (`"dead_letter"`) |
|
|
177
|
+
| Stalled, reclaimed | `"active"` (as found), `JobStalledError` | the job is `"waiting"` again |
|
|
178
|
+
| Stalled `maxStalledCount` times | `"dead_letter"`, `JobStalledError` | `job:dead-lettered` |
|
|
179
|
+
|
|
180
|
+
To alert only on dead letters, subscribe to `job:dead-lettered`. It fires once
|
|
181
|
+
per job, when the job enters the dead-letter store, with the entry's `error`
|
|
182
|
+
(`JobMaxAttemptsError`, or `JobStalledError` for a stalled job) and `reason`
|
|
183
|
+
(the last attempt's error message, or `"Stalled N time(s)."`):
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
queue.events?.on("job:dead-lettered", ({ job, error, reason }) => {
|
|
187
|
+
alerts.page(`${job.name} ${job.id} dead-lettered: ${reason}`, error);
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
It fires even if a custom `deadLetterStore.add()` rejects, in step with
|
|
192
|
+
`getStats().deadLettered`. Before 1.5.0 there was no dead-letter event.
|
|
193
|
+
|
|
139
194
|
## Workers, timeouts and context
|
|
140
195
|
|
|
141
196
|
**One consumer at a time.** `queue.process()` registers a processor and, by
|
|
@@ -81,6 +81,9 @@ export class InMemoryQueue {
|
|
|
81
81
|
this.emitter.setLogger(this.options.logger);
|
|
82
82
|
}
|
|
83
83
|
this.ownsDeadLetterStore = this.options.deadLetterStore === undefined;
|
|
84
|
+
// A caller's store is typed `DeadLetterStore<unknown>` (see
|
|
85
|
+
// `QueueOptions.deadLetterStore`); this queue only ever adds its own
|
|
86
|
+
// `TData` jobs to it, so it is read back as a store of `TData`.
|
|
84
87
|
this.deadLetterStore =
|
|
85
88
|
this.options.deadLetterStore ??
|
|
86
89
|
createInMemoryDeadLetterStore({
|
|
@@ -571,16 +574,22 @@ export class InMemoryQueue {
|
|
|
571
574
|
failedAt: new Date().toISOString(),
|
|
572
575
|
});
|
|
573
576
|
this.jobs.set(job.id, deadLettered);
|
|
577
|
+
const reason = `Stalled ${count} time(s).`;
|
|
574
578
|
void this.deadLetterStore
|
|
575
579
|
.add({
|
|
576
580
|
job: deadLettered,
|
|
577
581
|
deadLetterAt: new Date(),
|
|
578
582
|
error,
|
|
579
583
|
attempts: deadLettered.attempt,
|
|
580
|
-
reason
|
|
584
|
+
reason,
|
|
581
585
|
})
|
|
582
586
|
.catch(() => { });
|
|
583
587
|
this.emitter.emit("job:failed", { job: deadLettered, error });
|
|
588
|
+
this.emitter.emit("job:dead-lettered", {
|
|
589
|
+
job: deadLettered,
|
|
590
|
+
error,
|
|
591
|
+
reason,
|
|
592
|
+
});
|
|
584
593
|
this.recordSettled(deadLettered);
|
|
585
594
|
continue;
|
|
586
595
|
}
|
|
@@ -171,6 +171,11 @@ export async function handleJobFailure(job, errorMessage, deps) {
|
|
|
171
171
|
}
|
|
172
172
|
const deadLetterJob = updateJobState(incrementedJob, JobStateEnum.DEAD_LETTER, { error: maxAttemptsError.message });
|
|
173
173
|
jobs.set(job.id, deadLetterJob);
|
|
174
|
+
emitter.emit("job:dead-lettered", {
|
|
175
|
+
job: deadLetterJob,
|
|
176
|
+
error: maxAttemptsError,
|
|
177
|
+
reason: errorMessage,
|
|
178
|
+
});
|
|
174
179
|
deps.onSettled?.(deadLetterJob);
|
|
175
180
|
}
|
|
176
181
|
//# sourceMappingURL=inMemoryQueue.processing.js.map
|
|
@@ -63,8 +63,15 @@ export interface QueueOptions {
|
|
|
63
63
|
* emitter, reachable as `queue.events`.
|
|
64
64
|
*/
|
|
65
65
|
readonly eventEmitter?: QueueEventEmitter;
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Store that receives jobs which exhausted their attempts.
|
|
68
|
+
*
|
|
69
|
+
* Any store is accepted without an annotation: the untyped
|
|
70
|
+
* `createInMemoryDeadLetterStore()` and one typed for the queue's payload,
|
|
71
|
+
* `createInMemoryDeadLetterStore<TData>()`. (1.4.0 typed this
|
|
72
|
+
* `DeadLetterStore<never>`, which rejected both.)
|
|
73
|
+
*/
|
|
74
|
+
readonly deadLetterStore?: DeadLetterStore<unknown>;
|
|
68
75
|
/**
|
|
69
76
|
* Whether `add()` rejects while the queue is paused.
|
|
70
77
|
*
|
|
@@ -260,6 +267,11 @@ export type QueueEventMap = {
|
|
|
260
267
|
job: Job;
|
|
261
268
|
result: unknown;
|
|
262
269
|
};
|
|
270
|
+
/**
|
|
271
|
+
* An attempt failed. Fires on every failed attempt, retryable or final;
|
|
272
|
+
* `job.state` is `"failed"` for a processor failure either way. Use
|
|
273
|
+
* `job:retrying` or `job:dead-lettered` to tell the two apart.
|
|
274
|
+
*/
|
|
263
275
|
"job:failed": {
|
|
264
276
|
job: Job;
|
|
265
277
|
error: Error;
|
|
@@ -268,6 +280,17 @@ export type QueueEventMap = {
|
|
|
268
280
|
job: Job;
|
|
269
281
|
attempt: number;
|
|
270
282
|
};
|
|
283
|
+
/**
|
|
284
|
+
* The job was moved to the dead-letter store (attempts exhausted, or
|
|
285
|
+
* stalled `maxStalledCount` times). Fires once per job, after the
|
|
286
|
+
* `job:failed` for its last attempt; `job.state` is `"dead_letter"` and
|
|
287
|
+
* `error` / `reason` are the dead-letter entry's.
|
|
288
|
+
*/
|
|
289
|
+
"job:dead-lettered": {
|
|
290
|
+
job: Job;
|
|
291
|
+
error: Error;
|
|
292
|
+
reason?: string;
|
|
293
|
+
};
|
|
271
294
|
/** A running job was aborted from outside — a drain, a close, a cancel. */
|
|
272
295
|
"job:cancelled": {
|
|
273
296
|
job: Job;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/queue",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Background job and asynchronous task infrastructure with in-memory and adapter-based queue implementations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"!dist/.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zudojs/errors": "1.3.
|
|
29
|
-
"@zudojs/constants": "1.1.
|
|
30
|
-
"@zudojs/serialization": "1.2.
|
|
28
|
+
"@zudojs/errors": "1.3.1",
|
|
29
|
+
"@zudojs/constants": "1.1.3",
|
|
30
|
+
"@zudojs/serialization": "1.2.2"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"typescript": "7.0.2",
|