@basaltkit/queue-kafka 1.0.2 → 1.1.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 +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +31 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -94,6 +94,7 @@ The worker's concurrency is passed as `partitionsConsumedConcurrently` — actua
|
|
|
94
94
|
| `retrySuffix` | `string` | `'.retry'` | Suffix for the retry topic. |
|
|
95
95
|
| `deadSuffix` | `string` | `'.dead'` | Suffix for the dead-letter topic. |
|
|
96
96
|
| `client` | `KafkaClient` | kafkajs | Injectable client — used in tests without a broker. |
|
|
97
|
+
| `onError` | `(error, { source, queue? }) => void` | contextual `console.error` | Infrastructure-fault hook (same pattern as rabbitmq/sqs): a worker's connect/subscribe/run failing at boot (`source: 'consumer'` — previously an unhandled, process-fatal rejection and an invisible zero-worker app), or a retry/dead-letter re-publish failing (`source: 'producer'` — reported, then rethrown so the offset is not committed and Kafka redelivers; a producer outage cannot silently lose a failing job). |
|
|
97
98
|
|
|
98
99
|
Implements the `QueueDriver` contract from `@basaltkit/queue`.
|
|
99
100
|
|
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,17 @@ export interface KafkaDriverOptions {
|
|
|
47
47
|
deadSuffix?: string;
|
|
48
48
|
/** Injectable client — defaults to kafkajs. Tests pass a fake. */
|
|
49
49
|
client?: KafkaClient;
|
|
50
|
+
/**
|
|
51
|
+
* Called on infrastructure faults the driver cannot recover in line — a
|
|
52
|
+
* worker's connect/subscribe/run failing at boot (otherwise the app reports
|
|
53
|
+
* healthy with ZERO workers and the rejection is process-fatal), or a
|
|
54
|
+
* retry/dead-letter re-publish failing inside the consume callback. Same
|
|
55
|
+
* pattern as the rabbitmq/sqs drivers. Default: console.error with context.
|
|
56
|
+
*/
|
|
57
|
+
onError?: (error: unknown, info: {
|
|
58
|
+
source: 'consumer' | 'producer';
|
|
59
|
+
queue?: string;
|
|
60
|
+
}) => void;
|
|
50
61
|
}
|
|
51
62
|
/**
|
|
52
63
|
* Kafka driver for `@basaltkit/queue`. Kafka is a log, not a task queue, so this
|
|
@@ -73,6 +84,7 @@ export declare class KafkaQueueDriver implements QueueDriver {
|
|
|
73
84
|
private readonly groupId;
|
|
74
85
|
private readonly retrySuffix;
|
|
75
86
|
private readonly deadSuffix;
|
|
87
|
+
private readonly onError;
|
|
76
88
|
constructor(options: KafkaDriverOptions);
|
|
77
89
|
setExecutor(executor: JobExecutor): void;
|
|
78
90
|
add(queue: string, jobName: string, data: unknown, options: AddJobOptions): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -41,11 +41,15 @@ export class KafkaQueueDriver {
|
|
|
41
41
|
groupId;
|
|
42
42
|
retrySuffix;
|
|
43
43
|
deadSuffix;
|
|
44
|
+
onError;
|
|
44
45
|
constructor(options) {
|
|
45
46
|
this.options = options;
|
|
46
47
|
this.groupId = options.groupId ?? 'basalt-queue';
|
|
47
48
|
this.retrySuffix = options.retrySuffix ?? '.retry';
|
|
48
49
|
this.deadSuffix = options.deadSuffix ?? '.dead';
|
|
50
|
+
this.onError =
|
|
51
|
+
options.onError ??
|
|
52
|
+
((error, info) => console.error(`[basalt:queue] kafka ${info.source} error${info.queue ? ` (queue "${info.queue}")` : ''}:`, error));
|
|
49
53
|
}
|
|
50
54
|
setExecutor(executor) {
|
|
51
55
|
this.executor = executor;
|
|
@@ -70,7 +74,8 @@ export class KafkaQueueDriver {
|
|
|
70
74
|
});
|
|
71
75
|
}
|
|
72
76
|
startWorker(queue, options = {}) {
|
|
73
|
-
|
|
77
|
+
;
|
|
78
|
+
(async () => {
|
|
74
79
|
const consumer = (await this.client()).consumer({ groupId: this.groupId });
|
|
75
80
|
this.consumers.push(consumer);
|
|
76
81
|
await consumer.connect();
|
|
@@ -80,7 +85,12 @@ export class KafkaQueueDriver {
|
|
|
80
85
|
eachMessage: ({ message }) => this.handle(queue, message),
|
|
81
86
|
...(options.concurrency !== undefined ? { partitionsConsumedConcurrently: options.concurrency } : {}),
|
|
82
87
|
});
|
|
83
|
-
})()
|
|
88
|
+
})().catch((error) => {
|
|
89
|
+
// A broker-connect/subscribe failure at boot must be VISIBLE — otherwise
|
|
90
|
+
// the app reports healthy with zero workers, and the floating rejection
|
|
91
|
+
// would be process-fatal by Node's default.
|
|
92
|
+
this.onError(error, { source: 'consumer', queue });
|
|
93
|
+
});
|
|
84
94
|
}
|
|
85
95
|
async close() {
|
|
86
96
|
const producer = await this.producerPromise?.catch(() => undefined);
|
|
@@ -102,15 +112,26 @@ export class KafkaQueueDriver {
|
|
|
102
112
|
// Clamp the max-attempts read from the (untrusted) message to a hard
|
|
103
113
|
// ceiling so a crafted `attempts` can't drive a retry-amplification loop.
|
|
104
114
|
const attempts = Math.min(Number(headers[HEADER.attempts] ?? 1) || 1, MAX_ATTEMPTS);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
try {
|
|
116
|
+
const producer = await this.producer();
|
|
117
|
+
if (attempt < attempts) {
|
|
118
|
+
await producer.send({
|
|
119
|
+
topic: this.retryTopic(queue),
|
|
120
|
+
messages: [{ value, headers: { ...headers, [HEADER.attempt]: String(attempt + 1) } }],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
await producer.send({ topic: this.deadTopic(queue), messages: [{ value, headers }] });
|
|
125
|
+
}
|
|
111
126
|
}
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
catch (publishError) {
|
|
128
|
+
// The failed job could not be re-routed. Surface the fault, then
|
|
129
|
+
// RETHROW so eachMessage rejects and the offset is NOT committed —
|
|
130
|
+
// kafkajs redelivers the message (at-least-once) instead of the job
|
|
131
|
+
// silently vanishing on a producer outage. (Rabbitmq keeps the message
|
|
132
|
+
// unacked for the same reason; Kafka's equivalent is not committing.)
|
|
133
|
+
this.onError(publishError, { source: 'producer', queue });
|
|
134
|
+
throw publishError;
|
|
114
135
|
}
|
|
115
136
|
}
|
|
116
137
|
// Offsets auto-commit after eachMessage resolves — a re-routed failure is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/queue-kafka",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Kafka driver for @basaltkit/queue: produce/consume jobs with retry and dead-letter topics (no delayed delivery or priority — Kafka has neither).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@basaltkit/queue": "^1.
|
|
17
|
+
"@basaltkit/queue": "^1.3.1"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"kafkajs": "^2.0.0"
|