@voltro/plugin-queue 0.84.0 → 0.86.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/CHANGELOG.md +1215 -0
- package/THIRD-PARTY-NOTICES.md +118 -600
- package/dist/cdcSink.js +23 -0
- package/dist/consumer.js +15 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +42 -307
- package/dist/kafka.js +85 -0
- package/dist/queueMetrics.js +39 -0
- package/dist/queueTracing.js +26 -0
- package/dist/runner.js +109 -0
- package/package.json +5 -5
package/dist/cdcSink.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { activeQueueProvider as e } from "./index.js";
|
|
2
|
+
//#region src/cdcSink.ts
|
|
3
|
+
var t = (t) => {
|
|
4
|
+
let n = () => t.provider ?? e();
|
|
5
|
+
return {
|
|
6
|
+
name: `kafka(${t.topic})`,
|
|
7
|
+
...t.provider !== void 0 && t.provider.hosts[0] !== void 0 ? { outboundHost: t.provider.hosts[0] } : {},
|
|
8
|
+
deliver: async (e) => {
|
|
9
|
+
await n().produce(t.topic, e.map((e) => ({
|
|
10
|
+
key: e.key,
|
|
11
|
+
value: JSON.stringify({
|
|
12
|
+
table: e.table,
|
|
13
|
+
op: e.op,
|
|
14
|
+
key: e.key,
|
|
15
|
+
data: e.data
|
|
16
|
+
}),
|
|
17
|
+
headers: { "x-voltro-delivery-key": e.deliveryKey }
|
|
18
|
+
})));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { t as kafkaSink };
|
package/dist/consumer.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/consumer.ts
|
|
2
|
+
var e = Symbol.for("@voltro/plugin-queue:consumers"), t = () => {
|
|
3
|
+
let t = globalThis;
|
|
4
|
+
return t[e] ??= /* @__PURE__ */ new Map();
|
|
5
|
+
}, n = (e) => {
|
|
6
|
+
if (e.topic.trim() === "") throw Error("defineQueueConsumer: `topic` must be a non-empty name");
|
|
7
|
+
if (e.maxAttempts !== void 0 && e.maxAttempts < 1) throw Error(`defineQueueConsumer("${e.topic}"): maxAttempts must be >= 1`);
|
|
8
|
+
let n = `${e.topic}|${e.groupId ?? ""}`, r = t().get(n);
|
|
9
|
+
if (r !== void 0 && r !== e) throw Error(`defineQueueConsumer: topic '${e.topic}'${e.groupId ? ` group '${e.groupId}'` : ""} is declared twice — two consumers in one group on one topic would split its partitions unpredictably. Use a distinct groupId for an independent consumer.`);
|
|
10
|
+
return t().set(n, e), e;
|
|
11
|
+
}, r = () => [...t().values()], i = () => {
|
|
12
|
+
t().clear();
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { n as defineQueueConsumer, r as registeredQueueConsumers, i as resetQueueConsumersForTest };
|
package/dist/index.d.ts
CHANGED
|
@@ -114,7 +114,7 @@ export declare interface QueueConsumerDefinition<A = unknown> {
|
|
|
114
114
|
* to parse or decode goes STRAIGHT to the dead-letter topic with the
|
|
115
115
|
* reason — never into the retry loop (a decode failure is deterministic;
|
|
116
116
|
* retrying it is an infinite loop with extra steps). */
|
|
117
|
-
readonly schema: Schema.
|
|
117
|
+
readonly schema: Schema.Codec<A, any, never, never>;
|
|
118
118
|
/**
|
|
119
119
|
* The handler. Runs SERIALLY PER PARTITION; its offset commits only after
|
|
120
120
|
* it resolves. It MUST be idempotent: delivery is at-least-once, and a
|
|
@@ -247,7 +247,7 @@ export declare interface QueueRunnerLogger {
|
|
|
247
247
|
export declare class QueueService extends QueueService_base {
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
declare const QueueService_base: Context.
|
|
250
|
+
declare const QueueService_base: Context.ServiceClass<QueueService, "@voltro/plugin-queue/QueueService", QueueServiceApi>;
|
|
251
251
|
|
|
252
252
|
export declare interface QueueServiceApi {
|
|
253
253
|
/** Produce a batch to a topic (one transport round-trip). NOT transactional
|
package/dist/index.js
CHANGED
|
@@ -1,335 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { defineQueueConsumer as e, registeredQueueConsumers as t, resetQueueConsumersForTest as n } from "./consumer.js";
|
|
2
|
+
import { queueMetrics as r, recordQueueProduced as i } from "./queueMetrics.js";
|
|
3
|
+
import { kafkaProvider as a } from "./kafka.js";
|
|
4
|
+
import { startQueueConsumers as o } from "./runner.js";
|
|
5
|
+
import { kafkaSink as s } from "./cdcSink.js";
|
|
6
|
+
import { Context as c, Effect as l, Layer as u } from "effect";
|
|
7
|
+
import { definePlugin as d } from "@voltro/protocol";
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
var f = class extends c.Service()("@voltro/plugin-queue/QueueService") {}, p = Symbol.for("@voltro/plugin-queue:active-provider"), m = () => {
|
|
7
10
|
let e = globalThis;
|
|
8
|
-
return e[
|
|
9
|
-
},
|
|
10
|
-
|
|
11
|
-
if (e.maxAttempts !== void 0 && e.maxAttempts < 1) throw Error(`defineQueueConsumer("${e.topic}"): maxAttempts must be >= 1`);
|
|
12
|
-
let t = `${e.topic}|${e.groupId ?? ""}`, n = p().get(t);
|
|
13
|
-
if (n !== void 0 && n !== e) throw Error(`defineQueueConsumer: topic '${e.topic}'${e.groupId ? ` group '${e.groupId}'` : ""} is declared twice — two consumers in one group on one topic would split its partitions unpredictably. Use a distinct groupId for an independent consumer.`);
|
|
14
|
-
return p().set(t, e), e;
|
|
15
|
-
}, h = () => [...p().values()], g = () => {
|
|
16
|
-
p().clear();
|
|
17
|
-
}, _ = () => Object.create(null), v = Symbol.for("@voltro/plugin-queue:metrics"), y = () => {
|
|
18
|
-
let e = globalThis;
|
|
19
|
-
return e[v] ??= {
|
|
20
|
-
consumed: _(),
|
|
21
|
-
dlq: _(),
|
|
22
|
-
retried: _(),
|
|
23
|
-
lastError: _(),
|
|
24
|
-
produced: _()
|
|
25
|
-
};
|
|
26
|
-
}, b = (e, t, n = 1) => {
|
|
27
|
-
e[t] = (e[t] ?? 0) + n;
|
|
28
|
-
}, x = a.counter("voltro_queue_consumed_total", { description: "Messages a consumer finished with, by topic and outcome (ok | dead-lettered)." }), S = a.counter("voltro_queue_retries_total", { description: "In-process handler retries, by topic. A retry BLOCKS its partition." }), C = a.counter("voltro_queue_produced_total", { description: "Messages produced through the outbox bridge and QueueService, by topic." }), w = a.gauge("voltro_queue_lag_messages", { description: "Messages between the offset just picked up and the partition high watermark, by topic and partition. Sampled at pickup; holds its last value for an idle or revoked partition." }), T = [], E = /* @__PURE__ */ new Map(), D = /* @__PURE__ */ new Map(), O = /* @__PURE__ */ new Map(), k = /* @__PURE__ */ new Map(), A = (e, t, n) => {
|
|
29
|
-
let r = e.get(t);
|
|
30
|
-
if (r !== void 0) return r;
|
|
31
|
-
let i = n();
|
|
32
|
-
return e.set(t, i), i;
|
|
33
|
-
}, j = (e) => {
|
|
34
|
-
b(y().consumed, e), A(E, `ok\u0000${e}`, () => a.tagged(a.tagged(x, "topic", e), "outcome", "ok")).unsafeUpdate(1, T);
|
|
35
|
-
}, M = (e, t) => {
|
|
36
|
-
let n = y();
|
|
37
|
-
b(n.dlq, e), n.lastError[e] = t.slice(0, 500), A(E, `dlq\u0000${e}`, () => a.tagged(a.tagged(x, "topic", e), "outcome", "dead-lettered")).unsafeUpdate(1, T);
|
|
38
|
-
}, N = (e) => {
|
|
39
|
-
b(y().retried, e), A(D, e, () => a.tagged(S, "topic", e)).unsafeUpdate(1, T);
|
|
40
|
-
}, P = (e, t) => {
|
|
41
|
-
t <= 0 || (b(y().produced, e, t), A(O, e, () => a.tagged(C, "topic", e)).unsafeUpdate(t, T));
|
|
42
|
-
}, F = (e, t, n, r) => {
|
|
43
|
-
let i;
|
|
44
|
-
try {
|
|
45
|
-
let e = BigInt(n) - BigInt(r) - 1n;
|
|
46
|
-
i = e < 0n ? 0 : Number(e);
|
|
47
|
-
} catch {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
A(k, `${e}\u0000${t}`, () => a.tagged(a.tagged(w, "topic", e), "partition", String(t))).unsafeUpdate(i, T);
|
|
51
|
-
}, I = (e) => {
|
|
52
|
-
let t = {};
|
|
53
|
-
for (let [n, r] of Object.entries(e ?? {})) r != null && (t[n] = Buffer.isBuffer(r) ? r.toString("utf8") : String(r));
|
|
54
|
-
return t;
|
|
55
|
-
}, L = (e) => {
|
|
56
|
-
let t = new c({
|
|
57
|
-
clientId: e.clientId ?? "voltro",
|
|
58
|
-
brokers: [...e.brokers],
|
|
59
|
-
logLevel: l.ERROR,
|
|
60
|
-
...e.ssl === void 0 ? {} : { ssl: e.ssl },
|
|
61
|
-
...e.sasl === void 0 ? {} : { sasl: e.sasl }
|
|
62
|
-
}), n = null, r = /* @__PURE__ */ new Set();
|
|
63
|
-
return {
|
|
64
|
-
name: "kafka",
|
|
65
|
-
hosts: e.brokers.map((e) => e.split("/").pop() ?? e),
|
|
66
|
-
connect: async () => {
|
|
67
|
-
n === null && (n = t.producer({ allowAutoTopicCreation: !0 }), await n.connect());
|
|
68
|
-
},
|
|
69
|
-
disconnect: async () => {
|
|
70
|
-
let e = n;
|
|
71
|
-
n = null, await Promise.all([...r].map((e) => e.disconnect().catch(() => {}))), r.clear(), e !== null && await e.disconnect().catch(() => {});
|
|
72
|
-
},
|
|
73
|
-
produce: async (e, t) => {
|
|
74
|
-
if (n === null) throw Error("kafkaProvider: produce before connect()");
|
|
75
|
-
await n.send({
|
|
76
|
-
topic: e,
|
|
77
|
-
messages: t.map((e) => ({
|
|
78
|
-
...e.key === void 0 ? {} : { key: e.key },
|
|
79
|
-
value: e.value,
|
|
80
|
-
...e.headers === void 0 ? {} : { headers: { ...e.headers } }
|
|
81
|
-
}))
|
|
82
|
-
});
|
|
83
|
-
},
|
|
84
|
-
ensureTopics: async (e) => {
|
|
85
|
-
let n = t.admin();
|
|
86
|
-
await n.connect();
|
|
87
|
-
try {
|
|
88
|
-
await n.createTopics({
|
|
89
|
-
topics: e.map((e) => ({
|
|
90
|
-
topic: e,
|
|
91
|
-
numPartitions: 1
|
|
92
|
-
})),
|
|
93
|
-
waitForLeaders: !0
|
|
94
|
-
});
|
|
95
|
-
} finally {
|
|
96
|
-
await n.disconnect();
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
consume: async (n) => {
|
|
100
|
-
let i = t.consumer({ groupId: n.groupId });
|
|
101
|
-
return r.add(i), await i.connect(), await i.subscribe({
|
|
102
|
-
topic: n.topic,
|
|
103
|
-
fromBeginning: n.fromBeginning ?? !1
|
|
104
|
-
}), await i.run({
|
|
105
|
-
eachBatchAutoResolve: !1,
|
|
106
|
-
partitionsConsumedConcurrently: e.partitionsConsumedConcurrently ?? 3,
|
|
107
|
-
eachBatch: async ({ batch: e, resolveOffset: t, commitOffsetsIfNecessary: r, heartbeat: i, isRunning: a, isStale: o }) => {
|
|
108
|
-
for (let s of e.messages) {
|
|
109
|
-
if (!a() || o()) return;
|
|
110
|
-
let c = {
|
|
111
|
-
topic: e.topic,
|
|
112
|
-
partition: e.partition,
|
|
113
|
-
offset: s.offset,
|
|
114
|
-
key: s.key === null ? null : s.key.toString("utf8"),
|
|
115
|
-
value: s.value === null ? null : s.value.toString("utf8"),
|
|
116
|
-
headers: I(s.headers),
|
|
117
|
-
timestamp: s.timestamp
|
|
118
|
-
};
|
|
119
|
-
F(e.topic, e.partition, e.highWatermark, s.offset), await n.onMessage(c, {
|
|
120
|
-
heartbeat: i,
|
|
121
|
-
isStale: o
|
|
122
|
-
}), t(s.offset), await r();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}), { stop: async () => {
|
|
126
|
-
r.delete(i), await i.disconnect().catch(() => {});
|
|
127
|
-
} };
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
}, R = "queue.consume", z = (e, t) => (r) => {
|
|
131
|
-
let i = u(e.traceparent);
|
|
132
|
-
return r.pipe(n.tap((e) => t === void 0 ? n.void : n.annotateCurrentSpan(t(e))), n.withSpan(R, {
|
|
133
|
-
attributes: {
|
|
134
|
-
"messaging.system": e.system,
|
|
135
|
-
"messaging.operation": "process",
|
|
136
|
-
"messaging.destination.name": e.topic,
|
|
137
|
-
"messaging.consumer.group.name": e.groupId,
|
|
138
|
-
"messaging.destination.partition.id": e.partition,
|
|
139
|
-
"messaging.message.offset": e.offset
|
|
140
|
-
},
|
|
141
|
-
...i ? { parent: i } : {}
|
|
142
|
-
}));
|
|
143
|
-
}, B = async (t, i, a) => {
|
|
144
|
-
let o, s = await n.runPromiseExit(n.tryPromise({
|
|
145
|
-
try: i,
|
|
146
|
-
catch: (e) => (o = { value: e }, e)
|
|
147
|
-
}).pipe(z(t, a), d));
|
|
148
|
-
if (r.isSuccess(s)) return s.value;
|
|
149
|
-
throw o === void 0 ? e.squash(s.cause) : o.value;
|
|
150
|
-
}, V = (e) => new Promise((t) => setTimeout(t, e)), H = async (e) => {
|
|
151
|
-
let { provider: t, log: n } = e, r = { current: !1 }, i = /* @__PURE__ */ new Set(), a = async (r) => {
|
|
152
|
-
let i = r.groupId ?? `${e.appName}.${r.topic}`, a = r.dlqTopic ?? `${r.topic}.dlq`, s = r.maxAttempts ?? 3, c = o.decodeUnknownEither(r.schema), l = async (e, i, o) => {
|
|
153
|
-
M(r.topic, `${e}: ${i}`), n.warn("queue: dead-lettered", {
|
|
154
|
-
topic: r.topic,
|
|
155
|
-
dlqTopic: a,
|
|
156
|
-
reason: e,
|
|
157
|
-
partition: o.partition,
|
|
158
|
-
offset: o.offset
|
|
159
|
-
}), await t.produce(a, [{
|
|
160
|
-
...o.key === null ? {} : { key: o.key },
|
|
161
|
-
value: o.value ?? "",
|
|
162
|
-
headers: {
|
|
163
|
-
...o.headers,
|
|
164
|
-
"x-voltro-dlq-reason": e,
|
|
165
|
-
"x-voltro-dlq-detail": i.slice(0, 500),
|
|
166
|
-
"x-voltro-dlq-source-topic": o.topic,
|
|
167
|
-
"x-voltro-dlq-source-partition": String(o.partition),
|
|
168
|
-
"x-voltro-dlq-source-offset": o.offset
|
|
169
|
-
}
|
|
170
|
-
}]);
|
|
171
|
-
}, u = await t.consume({
|
|
172
|
-
topic: r.topic,
|
|
173
|
-
groupId: i,
|
|
174
|
-
...r.fromBeginning === void 0 ? {} : { fromBeginning: r.fromBeginning },
|
|
175
|
-
onMessage: async (a, o) => {
|
|
176
|
-
await B({
|
|
177
|
-
system: t.name,
|
|
178
|
-
topic: r.topic,
|
|
179
|
-
groupId: i,
|
|
180
|
-
partition: a.partition,
|
|
181
|
-
offset: a.offset,
|
|
182
|
-
traceparent: a.headers.traceparent
|
|
183
|
-
}, async () => {
|
|
184
|
-
let t;
|
|
185
|
-
try {
|
|
186
|
-
t = a.value === null ? null : JSON.parse(a.value);
|
|
187
|
-
} catch (e) {
|
|
188
|
-
return await l("decode-failed", `invalid JSON: ${e instanceof Error ? e.message : String(e)}`, a), "dead-lettered";
|
|
189
|
-
}
|
|
190
|
-
let i = c(t);
|
|
191
|
-
if (i._tag === "Left") return await l("decode-failed", String(i.left), a), "dead-lettered";
|
|
192
|
-
for (let t = 1; t <= s; t++) {
|
|
193
|
-
if (o.isStale()) return "stale";
|
|
194
|
-
try {
|
|
195
|
-
return await r.handler(i.right, {
|
|
196
|
-
topic: a.topic,
|
|
197
|
-
partition: a.partition,
|
|
198
|
-
offset: a.offset,
|
|
199
|
-
key: a.key,
|
|
200
|
-
headers: a.headers,
|
|
201
|
-
attempt: t,
|
|
202
|
-
...a.headers.traceparent === void 0 ? {} : { traceparent: a.headers.traceparent },
|
|
203
|
-
get store() {
|
|
204
|
-
let t = e.store();
|
|
205
|
-
if (t === null) throw Error("queue: store not bound yet — the retry loop absorbs a message arriving before boot finished");
|
|
206
|
-
return t;
|
|
207
|
-
}
|
|
208
|
-
}), j(r.topic), "ok";
|
|
209
|
-
} catch (i) {
|
|
210
|
-
let c = i instanceof Error ? i.message : String(i);
|
|
211
|
-
if (t >= s) return await l("handler-failed", c, a), "dead-lettered";
|
|
212
|
-
N(r.topic), n.warn("queue: handler failed, retrying", {
|
|
213
|
-
topic: r.topic,
|
|
214
|
-
attempt: t,
|
|
215
|
-
maxAttempts: s,
|
|
216
|
-
detail: c
|
|
217
|
-
}), await o.heartbeat().catch(() => {}), await V((e.retryBaseMs ?? 1e3) * 2 ** (t - 1));
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return "stale";
|
|
221
|
-
}, (e) => ({ "voltro.queue.outcome": e }));
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
return n.info("queue: consumer started", {
|
|
225
|
-
topic: r.topic,
|
|
226
|
-
groupId: i,
|
|
227
|
-
dlqTopic: a
|
|
228
|
-
}), u;
|
|
229
|
-
}, s = e.consumers.map((e) => {
|
|
230
|
-
let t = { handle: null }, o = () => {
|
|
231
|
-
a(e).then((e) => {
|
|
232
|
-
if (r.current) {
|
|
233
|
-
e.stop();
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
t.handle = e;
|
|
237
|
-
}).catch((t) => {
|
|
238
|
-
if (r.current) return;
|
|
239
|
-
n.warn("queue: consumer start failed — retrying in 5s", {
|
|
240
|
-
topic: e.topic,
|
|
241
|
-
detail: t instanceof Error ? t.message : String(t)
|
|
242
|
-
});
|
|
243
|
-
let a = setTimeout(() => {
|
|
244
|
-
i.delete(a), o();
|
|
245
|
-
}, 5e3);
|
|
246
|
-
i.add(a);
|
|
247
|
-
});
|
|
248
|
-
};
|
|
249
|
-
return o(), t;
|
|
250
|
-
});
|
|
251
|
-
return { stop: async () => {
|
|
252
|
-
r.current = !0;
|
|
253
|
-
for (let e of i) clearTimeout(e);
|
|
254
|
-
i.clear(), await Promise.all(s.map((e) => e.handle?.stop() ?? Promise.resolve()));
|
|
255
|
-
} };
|
|
256
|
-
}, U = (e) => {
|
|
257
|
-
let t = () => e.provider ?? q();
|
|
258
|
-
return {
|
|
259
|
-
name: `kafka(${e.topic})`,
|
|
260
|
-
...e.provider !== void 0 && e.provider.hosts[0] !== void 0 ? { outboundHost: e.provider.hosts[0] } : {},
|
|
261
|
-
deliver: async (n) => {
|
|
262
|
-
await t().produce(e.topic, n.map((e) => ({
|
|
263
|
-
key: e.key,
|
|
264
|
-
value: JSON.stringify({
|
|
265
|
-
table: e.table,
|
|
266
|
-
op: e.op,
|
|
267
|
-
key: e.key,
|
|
268
|
-
data: e.data
|
|
269
|
-
}),
|
|
270
|
-
headers: { "x-voltro-delivery-key": e.deliveryKey }
|
|
271
|
-
})));
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
}, W = class extends t.Tag("@voltro/plugin-queue/QueueService")() {}, G = Symbol.for("@voltro/plugin-queue:active-provider"), K = () => {
|
|
275
|
-
let e = globalThis;
|
|
276
|
-
return e[G] ??= { current: null };
|
|
277
|
-
}, q = () => {
|
|
278
|
-
let e = K().current;
|
|
11
|
+
return e[p] ??= { current: null };
|
|
12
|
+
}, h = () => {
|
|
13
|
+
let e = m().current;
|
|
279
14
|
if (e === null) throw Error("[voltro] no active queue provider — is queuePlugin({...}) in this app's plugins? The outbox handler and kafkaSink need the plugin's connected provider.");
|
|
280
15
|
return e;
|
|
281
|
-
},
|
|
16
|
+
}, g = () => ({
|
|
282
17
|
effect: "queue.produce",
|
|
283
18
|
maxAttempts: 8,
|
|
284
19
|
handler: async ({ payload: e }) => {
|
|
285
20
|
let t = e.topic, n = e.messages;
|
|
286
21
|
if (typeof t != "string" || !Array.isArray(n)) throw Error("queue.produce payload must be { topic: string, messages: QueueOutgoingMessage[] }");
|
|
287
|
-
await
|
|
22
|
+
await h().produce(t, n), i(t, n.length);
|
|
288
23
|
}
|
|
289
|
-
}),
|
|
290
|
-
let
|
|
291
|
-
return
|
|
292
|
-
name:
|
|
24
|
+
}), _ = (e) => {
|
|
25
|
+
let n = e.name ?? "queue", s = a(e), c = ["inspect:read", ...s.hosts.map((e) => `network:outbound:${e}`)], p = null, h = { current: null };
|
|
26
|
+
return d({
|
|
27
|
+
name: n,
|
|
293
28
|
baseName: "queue",
|
|
294
29
|
description: "Queue interop — consume and produce against an existing Kafka: Schema-decoded consumers (at-least-once, serial per partition, retry + dead-letter), batched producing via a handler service or transactionally through the outbox, and a kafkaSink for plugin-cdc-out.",
|
|
295
|
-
permissions:
|
|
296
|
-
services:
|
|
297
|
-
await
|
|
30
|
+
permissions: c,
|
|
31
|
+
services: u.succeed(f, { produce: async (e, t) => {
|
|
32
|
+
await s.produce(e, t), i(e, t.length);
|
|
298
33
|
} }),
|
|
299
34
|
bindDataStore: (e) => {
|
|
300
|
-
|
|
35
|
+
h.current = e;
|
|
301
36
|
},
|
|
302
|
-
onActivate: (
|
|
303
|
-
await
|
|
304
|
-
let
|
|
305
|
-
|
|
306
|
-
provider:
|
|
307
|
-
consumers:
|
|
308
|
-
appName: e.groupPrefix ??
|
|
309
|
-
log:
|
|
310
|
-
store: () =>
|
|
37
|
+
onActivate: (n) => l.tryPromise(async () => {
|
|
38
|
+
await s.connect(), m().current = s;
|
|
39
|
+
let r = t();
|
|
40
|
+
p = await o({
|
|
41
|
+
provider: s,
|
|
42
|
+
consumers: r,
|
|
43
|
+
appName: e.groupPrefix ?? n.app.name,
|
|
44
|
+
log: n.logger,
|
|
45
|
+
store: () => h.current,
|
|
311
46
|
...e.retryBaseMs === void 0 ? {} : { retryBaseMs: e.retryBaseMs }
|
|
312
|
-
}),
|
|
313
|
-
brokers:
|
|
314
|
-
consumers:
|
|
47
|
+
}), n.logger.info("queue: connected", {
|
|
48
|
+
brokers: s.hosts,
|
|
49
|
+
consumers: r.length
|
|
315
50
|
});
|
|
316
51
|
}),
|
|
317
|
-
onDeactivate: () =>
|
|
318
|
-
await
|
|
52
|
+
onDeactivate: () => l.tryPromise(async () => {
|
|
53
|
+
await p?.stop(), p = null, m().current === s && (m().current = null), await s.disconnect();
|
|
319
54
|
}),
|
|
320
55
|
inspectEndpoints: [{
|
|
321
56
|
method: "GET",
|
|
322
57
|
path: "/consumers",
|
|
323
58
|
scope: "process",
|
|
324
59
|
description: "Registered consumers + per-topic consumed/retried/dead-lettered/produced counters (this replica).",
|
|
325
|
-
handler: () =>
|
|
326
|
-
let e =
|
|
60
|
+
handler: () => l.sync(() => {
|
|
61
|
+
let e = r();
|
|
327
62
|
return {
|
|
328
63
|
kind: "json",
|
|
329
64
|
data: {
|
|
330
|
-
producerSelected:
|
|
331
|
-
brokers:
|
|
332
|
-
consumers:
|
|
65
|
+
producerSelected: m().current === s,
|
|
66
|
+
brokers: s.hosts,
|
|
67
|
+
consumers: t().map((t) => ({
|
|
333
68
|
topic: t.topic,
|
|
334
69
|
groupId: t.groupId ?? null,
|
|
335
70
|
dlqTopic: t.dlqTopic ?? `${t.topic}.dlq`,
|
|
@@ -347,4 +82,4 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
347
82
|
});
|
|
348
83
|
};
|
|
349
84
|
//#endregion
|
|
350
|
-
export {
|
|
85
|
+
export { f as QueueService, h as activeQueueProvider, e as defineQueueConsumer, a as kafkaProvider, s as kafkaSink, r as queueMetrics, g as queueOutboxHandler, _ as queuePlugin, t as registeredQueueConsumers, n as resetQueueConsumersForTest, o as startQueueConsumers };
|
package/dist/kafka.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { recordQueueLag as e } from "./queueMetrics.js";
|
|
2
|
+
import { Kafka as t, logLevel as n } from "kafkajs";
|
|
3
|
+
//#region src/kafka.ts
|
|
4
|
+
var r = (e) => {
|
|
5
|
+
let t = {};
|
|
6
|
+
for (let [n, r] of Object.entries(e ?? {})) r != null && (t[n] = Buffer.isBuffer(r) ? r.toString("utf8") : String(r));
|
|
7
|
+
return t;
|
|
8
|
+
}, i = (i) => {
|
|
9
|
+
let a = new t({
|
|
10
|
+
clientId: i.clientId ?? "voltro",
|
|
11
|
+
brokers: [...i.brokers],
|
|
12
|
+
logLevel: n.ERROR,
|
|
13
|
+
...i.ssl === void 0 ? {} : { ssl: i.ssl },
|
|
14
|
+
...i.sasl === void 0 ? {} : { sasl: i.sasl }
|
|
15
|
+
}), o = null, s = /* @__PURE__ */ new Set();
|
|
16
|
+
return {
|
|
17
|
+
name: "kafka",
|
|
18
|
+
hosts: i.brokers.map((e) => e.split("/").pop() ?? e),
|
|
19
|
+
connect: async () => {
|
|
20
|
+
o === null && (o = a.producer({ allowAutoTopicCreation: !0 }), await o.connect());
|
|
21
|
+
},
|
|
22
|
+
disconnect: async () => {
|
|
23
|
+
let e = o;
|
|
24
|
+
o = null, await Promise.all([...s].map((e) => e.disconnect().catch(() => {}))), s.clear(), e !== null && await e.disconnect().catch(() => {});
|
|
25
|
+
},
|
|
26
|
+
produce: async (e, t) => {
|
|
27
|
+
if (o === null) throw Error("kafkaProvider: produce before connect()");
|
|
28
|
+
await o.send({
|
|
29
|
+
topic: e,
|
|
30
|
+
messages: t.map((e) => ({
|
|
31
|
+
...e.key === void 0 ? {} : { key: e.key },
|
|
32
|
+
value: e.value,
|
|
33
|
+
...e.headers === void 0 ? {} : { headers: { ...e.headers } }
|
|
34
|
+
}))
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
ensureTopics: async (e) => {
|
|
38
|
+
let t = a.admin();
|
|
39
|
+
await t.connect();
|
|
40
|
+
try {
|
|
41
|
+
await t.createTopics({
|
|
42
|
+
topics: e.map((e) => ({
|
|
43
|
+
topic: e,
|
|
44
|
+
numPartitions: 1
|
|
45
|
+
})),
|
|
46
|
+
waitForLeaders: !0
|
|
47
|
+
});
|
|
48
|
+
} finally {
|
|
49
|
+
await t.disconnect();
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
consume: async (t) => {
|
|
53
|
+
let n = a.consumer({ groupId: t.groupId });
|
|
54
|
+
return s.add(n), await n.connect(), await n.subscribe({
|
|
55
|
+
topic: t.topic,
|
|
56
|
+
fromBeginning: t.fromBeginning ?? !1
|
|
57
|
+
}), await n.run({
|
|
58
|
+
eachBatchAutoResolve: !1,
|
|
59
|
+
partitionsConsumedConcurrently: i.partitionsConsumedConcurrently ?? 3,
|
|
60
|
+
eachBatch: async ({ batch: n, resolveOffset: i, commitOffsetsIfNecessary: a, heartbeat: o, isRunning: s, isStale: c }) => {
|
|
61
|
+
for (let l of n.messages) {
|
|
62
|
+
if (!s() || c()) return;
|
|
63
|
+
let u = {
|
|
64
|
+
topic: n.topic,
|
|
65
|
+
partition: n.partition,
|
|
66
|
+
offset: l.offset,
|
|
67
|
+
key: l.key === null ? null : l.key.toString("utf8"),
|
|
68
|
+
value: l.value === null ? null : l.value.toString("utf8"),
|
|
69
|
+
headers: r(l.headers),
|
|
70
|
+
timestamp: l.timestamp
|
|
71
|
+
};
|
|
72
|
+
e(n.topic, n.partition, n.highWatermark, l.offset), await t.onMessage(u, {
|
|
73
|
+
heartbeat: o,
|
|
74
|
+
isStale: c
|
|
75
|
+
}), i(l.offset), await a();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}), { stop: async () => {
|
|
79
|
+
s.delete(n), await n.disconnect().catch(() => {});
|
|
80
|
+
} };
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
85
|
+
export { i as kafkaProvider };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Context as e, Metric as t } from "effect";
|
|
2
|
+
//#region src/queueMetrics.ts
|
|
3
|
+
var n = () => Object.create(null), r = Symbol.for("@voltro/plugin-queue:metrics"), i = () => {
|
|
4
|
+
let e = globalThis;
|
|
5
|
+
return e[r] ??= {
|
|
6
|
+
consumed: n(),
|
|
7
|
+
dlq: n(),
|
|
8
|
+
retried: n(),
|
|
9
|
+
lastError: n(),
|
|
10
|
+
produced: n()
|
|
11
|
+
};
|
|
12
|
+
}, a = (e, t, n = 1) => {
|
|
13
|
+
e[t] = (e[t] ?? 0) + n;
|
|
14
|
+
}, o = t.counter("voltro_queue_consumed_total", { description: "Messages a consumer finished with, by topic and outcome (ok | dead-lettered)." }), s = t.counter("voltro_queue_retries_total", { description: "In-process handler retries, by topic. A retry BLOCKS its partition." }), c = t.counter("voltro_queue_produced_total", { description: "Messages produced through the outbox bridge and QueueService, by topic." }), l = t.gauge("voltro_queue_lag_messages", { description: "Messages between the offset just picked up and the partition high watermark, by topic and partition. Sampled at pickup; holds its last value for an idle or revoked partition." }), u = e.empty(), d = /* @__PURE__ */ new Map(), f = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Map(), h = (e, t, n) => {
|
|
15
|
+
let r = e.get(t);
|
|
16
|
+
if (r !== void 0) return r;
|
|
17
|
+
let i = n();
|
|
18
|
+
return e.set(t, i), i;
|
|
19
|
+
}, g = (e) => {
|
|
20
|
+
a(i().consumed, e), h(d, `ok\u0000${e}`, () => t.withAttributes(t.withAttributes(o, { topic: e }), { outcome: "ok" })).updateUnsafe(1, u);
|
|
21
|
+
}, _ = (e, n) => {
|
|
22
|
+
let r = i();
|
|
23
|
+
a(r.dlq, e), r.lastError[e] = n.slice(0, 500), h(d, `dlq\u0000${e}`, () => t.withAttributes(t.withAttributes(o, { topic: e }), { outcome: "dead-lettered" })).updateUnsafe(1, u);
|
|
24
|
+
}, v = (e) => {
|
|
25
|
+
a(i().retried, e), h(f, e, () => t.withAttributes(s, { topic: e })).updateUnsafe(1, u);
|
|
26
|
+
}, y = (e, n) => {
|
|
27
|
+
n <= 0 || (a(i().produced, e, n), h(p, e, () => t.withAttributes(c, { topic: e })).updateUnsafe(n, u));
|
|
28
|
+
}, b = (e, n, r, i) => {
|
|
29
|
+
let a;
|
|
30
|
+
try {
|
|
31
|
+
let e = BigInt(r) - BigInt(i) - 1n;
|
|
32
|
+
a = e < 0n ? 0 : Number(e);
|
|
33
|
+
} catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
h(m, `${e}\u0000${n}`, () => t.withAttributes(t.withAttributes(l, { topic: e }), { partition: String(n) })).updateUnsafe(a, u);
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { i as queueMetrics, g as recordQueueConsumed, _ as recordQueueDeadLettered, b as recordQueueLag, y as recordQueueProduced, v as recordQueueRetry };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Cause as e, Effect as t, Exit as n } from "effect";
|
|
2
|
+
import { externalSpanFromTraceparent as r, withServerTracer as i } from "@voltro/runtime";
|
|
3
|
+
//#region src/queueTracing.ts
|
|
4
|
+
var a = "queue.consume", o = (e, n) => (i) => {
|
|
5
|
+
let o = r(e.traceparent);
|
|
6
|
+
return i.pipe(t.tap((e) => n === void 0 ? t.void : t.annotateCurrentSpan(n(e))), t.withSpan(a, {
|
|
7
|
+
attributes: {
|
|
8
|
+
"messaging.system": e.system,
|
|
9
|
+
"messaging.operation": "process",
|
|
10
|
+
"messaging.destination.name": e.topic,
|
|
11
|
+
"messaging.consumer.group.name": e.groupId,
|
|
12
|
+
"messaging.destination.partition.id": e.partition,
|
|
13
|
+
"messaging.message.offset": e.offset
|
|
14
|
+
},
|
|
15
|
+
...o ? { parent: o } : {}
|
|
16
|
+
}));
|
|
17
|
+
}, s = async (r, a, s) => {
|
|
18
|
+
let c, l = await t.runPromiseExit(t.tryPromise({
|
|
19
|
+
try: a,
|
|
20
|
+
catch: (e) => (c = { value: e }, e)
|
|
21
|
+
}).pipe(o(r, s), i));
|
|
22
|
+
if (n.isSuccess(l)) return l.value;
|
|
23
|
+
throw c === void 0 ? e.squash(l.cause) : c.value;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { a as QUEUE_CONSUME_SPAN, s as runInQueueMessageSpan, o as withQueueMessageSpan };
|