@drarzter/kafka-client 0.7.0 → 0.7.1
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 +100 -1
- package/dist/{chunk-MJ342P4R.mjs → chunk-AMEGMOZH.mjs} +119 -17
- package/dist/chunk-AMEGMOZH.mjs.map +1 -0
- package/dist/core.d.mts +17 -2
- package/dist/core.d.ts +17 -2
- package/dist/core.js +118 -16
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +118 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/otel.d.mts +1 -1
- package/dist/otel.d.ts +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +2 -0
- package/dist/testing.mjs.map +1 -1
- package/dist/{types-DqQ7IXZr.d.mts → types-BEIGjmV6.d.mts} +64 -2
- package/dist/{types-DqQ7IXZr.d.ts → types-BEIGjmV6.d.ts} +64 -2
- package/package.json +1 -1
- package/dist/chunk-MJ342P4R.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -36,13 +36,16 @@ Type-safe Kafka client for Node.js. Framework-agnostic core with a first-class N
|
|
|
36
36
|
- [stopConsumer](#stopconsumer)
|
|
37
37
|
- [Pause and resume](#pause-and-resume)
|
|
38
38
|
- [Circuit breaker](#circuit-breaker)
|
|
39
|
+
- [getCircuitState](#getcircuitstate)
|
|
39
40
|
- [Reset consumer offsets](#reset-consumer-offsets)
|
|
40
41
|
- [Seek to offset](#seek-to-offset)
|
|
42
|
+
- [Seek to timestamp](#seek-to-timestamp)
|
|
41
43
|
- [Message TTL](#message-ttl)
|
|
42
44
|
- [DLQ replay](#dlq-replay)
|
|
43
45
|
- [Graceful shutdown](#graceful-shutdown)
|
|
44
46
|
- [Consumer handles](#consumer-handles)
|
|
45
47
|
- [onMessageLost](#onmessagelost)
|
|
48
|
+
- [onTtlExpired](#onttlexpired)
|
|
46
49
|
- [onRebalance](#onrebalance)
|
|
47
50
|
- [Consumer lag](#consumer-lag)
|
|
48
51
|
- [Handler timeout warning](#handler-timeout-warning)
|
|
@@ -465,6 +468,20 @@ for await (const envelope of kafka.consume('orders', {
|
|
|
465
468
|
|
|
466
469
|
`break`, `return`, or any early exit from the loop calls the iterator's `return()` method, which closes the internal queue and calls `handle.stop()` on the background consumer.
|
|
467
470
|
|
|
471
|
+
**Backpressure** — use `queueHighWaterMark` to prevent unbounded queue growth when processing is slower than the message rate:
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
for await (const envelope of kafka.consume('orders', {
|
|
475
|
+
queueHighWaterMark: 100, // pause partition when queue reaches 100 messages
|
|
476
|
+
})) {
|
|
477
|
+
await slowProcessing(envelope.payload); // resumes when queue drains below 50
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The partition is paused when the internal queue reaches `queueHighWaterMark` and automatically resumed when it drains below 50%. Without this option the queue is unbounded.
|
|
482
|
+
|
|
483
|
+
**Error propagation** — if the consumer fails to start (e.g. broker unreachable), the error surfaces on the next `next()` / `for await` iteration rather than being silently swallowed.
|
|
484
|
+
|
|
468
485
|
## Multiple consumer groups
|
|
469
486
|
|
|
470
487
|
### Per-consumer groupId
|
|
@@ -871,6 +888,7 @@ Options for `sendMessage()` — the third argument:
|
|
|
871
888
|
| `circuitBreaker.recoveryMs` | `30_000` | Milliseconds to wait in OPEN state before entering HALF_OPEN |
|
|
872
889
|
| `circuitBreaker.windowSize` | `threshold × 2, min 10` | Sliding window size in messages |
|
|
873
890
|
| `circuitBreaker.halfOpenSuccesses` | `1` | Consecutive successes in HALF_OPEN required to close the circuit |
|
|
891
|
+
| `queueHighWaterMark` | unbounded | Max messages buffered in the `consume()` iterator queue before the partition is paused; resumes at 50% drain. Only applies to `consume()` |
|
|
874
892
|
| `batch` | `false` | (decorator only) Use `startBatchConsumer` instead of `startConsumer` |
|
|
875
893
|
| `subscribeRetry.retries` | `5` | Max attempts for `consumer.subscribe()` when topic doesn't exist yet |
|
|
876
894
|
| `subscribeRetry.backoffMs` | `5000` | Delay between subscribe retry attempts (ms) |
|
|
@@ -892,6 +910,7 @@ Passed to `KafkaModule.register()` or returned from `registerAsync()` factory:
|
|
|
892
910
|
| `instrumentation` | `[]` | Client-wide instrumentation hooks (e.g. OTel). Applied to both send and consume paths |
|
|
893
911
|
| `transactionalId` | `${clientId}-tx` | Transactional producer ID for `transaction()` calls. Must be unique per producer instance across the cluster — two instances sharing the same ID will be fenced by Kafka. The client logs a warning when the same ID is registered twice within one process |
|
|
894
912
|
| `onMessageLost` | — | Called when a message is silently dropped without DLQ — use to alert, log to external systems, or trigger fallback logic |
|
|
913
|
+
| `onTtlExpired` | — | Called when a message is dropped due to TTL expiration (`messageTtlMs`) and `dlq` is not enabled; receives `{ topic, ageMs, messageTtlMs, headers }` |
|
|
895
914
|
| `onRebalance` | — | Called on every partition assign/revoke event across all consumers created by this client |
|
|
896
915
|
|
|
897
916
|
**Module-scoped** (default) — import `KafkaModule` in each module that needs it:
|
|
@@ -1200,6 +1219,41 @@ Options:
|
|
|
1200
1219
|
| `windowSize` | `threshold × 2, min 10` | Sliding window size in messages |
|
|
1201
1220
|
| `halfOpenSuccesses` | `1` | Consecutive successes in HALF_OPEN required to close the circuit |
|
|
1202
1221
|
|
|
1222
|
+
### getCircuitState
|
|
1223
|
+
|
|
1224
|
+
Inspect the current circuit breaker state for a partition — useful for health endpoints and dashboards:
|
|
1225
|
+
|
|
1226
|
+
```typescript
|
|
1227
|
+
const state = kafka.getCircuitState('orders', 0);
|
|
1228
|
+
// undefined — circuit not configured or never tripped
|
|
1229
|
+
// { status: 'closed', failures: 2, windowSize: 10 }
|
|
1230
|
+
// { status: 'open', failures: 5, windowSize: 10 }
|
|
1231
|
+
// { status: 'half-open', failures: 0, windowSize: 0 }
|
|
1232
|
+
|
|
1233
|
+
// With explicit group ID:
|
|
1234
|
+
const state = kafka.getCircuitState('orders', 0, 'payments-group');
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
Returns `undefined` when `circuitBreaker` is not configured for the group or the circuit has never been tripped (state is lazily initialised on the first DLQ event).
|
|
1238
|
+
|
|
1239
|
+
**Instrumentation hooks** — react to state transitions via `KafkaInstrumentation`:
|
|
1240
|
+
|
|
1241
|
+
```typescript
|
|
1242
|
+
const kafka = new KafkaClient('svc', 'group', brokers, {
|
|
1243
|
+
instrumentation: [{
|
|
1244
|
+
onCircuitOpen(topic, partition) {
|
|
1245
|
+
metrics.increment('circuit_open', { topic, partition });
|
|
1246
|
+
},
|
|
1247
|
+
onCircuitHalfOpen(topic, partition) {
|
|
1248
|
+
logger.log(`Circuit probing ${topic}[${partition}]`);
|
|
1249
|
+
},
|
|
1250
|
+
onCircuitClose(topic, partition) {
|
|
1251
|
+
metrics.increment('circuit_close', { topic, partition });
|
|
1252
|
+
},
|
|
1253
|
+
}],
|
|
1254
|
+
});
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1203
1257
|
## Reset consumer offsets
|
|
1204
1258
|
|
|
1205
1259
|
Seek a consumer group's committed offsets to the beginning or end of a topic:
|
|
@@ -1239,6 +1293,29 @@ The first argument is the consumer group ID — pass `undefined` to target the d
|
|
|
1239
1293
|
|
|
1240
1294
|
**Important:** the consumer for the specified group must be stopped before calling `seekToOffset`. An error is thrown if the group is currently running.
|
|
1241
1295
|
|
|
1296
|
+
## Seek to timestamp
|
|
1297
|
+
|
|
1298
|
+
Seek partitions to the offset nearest to a specific point in time — useful for replaying events that occurred after a known incident or deployment:
|
|
1299
|
+
|
|
1300
|
+
```typescript
|
|
1301
|
+
const ts = new Date('2024-06-01T12:00:00Z').getTime(); // Unix ms
|
|
1302
|
+
|
|
1303
|
+
await kafka.seekToTimestamp(undefined, [
|
|
1304
|
+
{ topic: 'orders', partition: 0, timestamp: ts },
|
|
1305
|
+
{ topic: 'orders', partition: 1, timestamp: ts },
|
|
1306
|
+
]);
|
|
1307
|
+
|
|
1308
|
+
// Multiple topics in one call
|
|
1309
|
+
await kafka.seekToTimestamp('payments-group', [
|
|
1310
|
+
{ topic: 'payments', partition: 0, timestamp: ts },
|
|
1311
|
+
{ topic: 'refunds', partition: 0, timestamp: ts },
|
|
1312
|
+
]);
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
Uses `admin.fetchTopicOffsetsByTime` under the hood. If no offset exists at the requested timestamp (e.g. the partition is empty or the timestamp is in the future), the partition falls back to `-1` (end of topic — new messages only).
|
|
1316
|
+
|
|
1317
|
+
**Important:** the consumer group must be stopped before seeking. Assignments for the same topic are batched into a single `admin.setOffsets` call.
|
|
1318
|
+
|
|
1242
1319
|
## Message TTL
|
|
1243
1320
|
|
|
1244
1321
|
Drop or route expired messages using `messageTtlMs` in `ConsumerOptions`:
|
|
@@ -1253,7 +1330,7 @@ await kafka.startConsumer(['orders'], handler, {
|
|
|
1253
1330
|
The TTL is evaluated against the `x-timestamp` header stamped on every outgoing message by the producer. Messages whose age at consumption time exceeds `messageTtlMs` are:
|
|
1254
1331
|
|
|
1255
1332
|
- **Routed to DLQ** with `x-dlq-reason: ttl-expired` when `dlq: true`
|
|
1256
|
-
- **Dropped** (calling `
|
|
1333
|
+
- **Dropped** (calling `onTtlExpired` if configured) otherwise
|
|
1257
1334
|
|
|
1258
1335
|
Typical use: prevent stale events from poisoning downstream systems after a consumer lag spike — e.g. discard order events or push notifications that are no longer actionable.
|
|
1259
1336
|
|
|
@@ -1361,6 +1438,28 @@ const kafka = new KafkaClient('my-app', 'my-group', ['localhost:9092'], {
|
|
|
1361
1438
|
|
|
1362
1439
|
In the normal case (`dlq: true`, DLQ send succeeds), `onMessageLost` does NOT fire — the message is preserved in `{topic}.dlq`.
|
|
1363
1440
|
|
|
1441
|
+
> **Note:** TTL-expired messages do **not** trigger `onMessageLost`. Use `onTtlExpired` to observe them separately.
|
|
1442
|
+
|
|
1443
|
+
## onTtlExpired
|
|
1444
|
+
|
|
1445
|
+
Called when a message is dropped because it exceeded `messageTtlMs` and `dlq` is not enabled. Fires instead of `onMessageLost` for expired messages:
|
|
1446
|
+
|
|
1447
|
+
```typescript
|
|
1448
|
+
import { KafkaClient, TtlExpiredContext } from '@drarzter/kafka-client/core';
|
|
1449
|
+
|
|
1450
|
+
const kafka = new KafkaClient('my-app', 'my-group', ['localhost:9092'], {
|
|
1451
|
+
onTtlExpired: (ctx: TtlExpiredContext) => {
|
|
1452
|
+
// ctx.topic — topic the message came from
|
|
1453
|
+
// ctx.ageMs — actual age of the message at drop time
|
|
1454
|
+
// ctx.messageTtlMs — the configured threshold
|
|
1455
|
+
// ctx.headers — original message headers
|
|
1456
|
+
logger.warn(`Stale message on ${ctx.topic}: ${ctx.ageMs}ms old (limit ${ctx.messageTtlMs}ms)`);
|
|
1457
|
+
},
|
|
1458
|
+
});
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
When `dlq: true`, expired messages are routed to DLQ instead and `onTtlExpired` is **not** called.
|
|
1462
|
+
|
|
1364
1463
|
## onRebalance
|
|
1365
1464
|
|
|
1366
1465
|
React to partition rebalance events without patching the consumer. Useful for flushing in-flight state before partitions are revoked, or for logging/metrics:
|
|
@@ -732,10 +732,10 @@ async function handleEachMessage(payload, opts, deps) {
|
|
|
732
732
|
});
|
|
733
733
|
deps.onDlq?.(envelope, "ttl-expired");
|
|
734
734
|
} else {
|
|
735
|
-
await deps.
|
|
735
|
+
await deps.onTtlExpired?.({
|
|
736
736
|
topic: topic2,
|
|
737
|
-
|
|
738
|
-
|
|
737
|
+
ageMs,
|
|
738
|
+
messageTtlMs: opts.messageTtlMs,
|
|
739
739
|
headers: envelope.headers
|
|
740
740
|
});
|
|
741
741
|
}
|
|
@@ -859,10 +859,10 @@ async function handleEachBatch(payload, opts, deps) {
|
|
|
859
859
|
});
|
|
860
860
|
deps.onDlq?.(envelope, "ttl-expired");
|
|
861
861
|
} else {
|
|
862
|
-
await deps.
|
|
862
|
+
await deps.onTtlExpired?.({
|
|
863
863
|
topic: batch.topic,
|
|
864
|
-
|
|
865
|
-
|
|
864
|
+
ageMs,
|
|
865
|
+
messageTtlMs: opts.messageTtlMs,
|
|
866
866
|
headers: envelope.headers
|
|
867
867
|
});
|
|
868
868
|
}
|
|
@@ -1179,28 +1179,51 @@ async function startRetryTopicConsumers(originalTopics, originalGroupId, handleM
|
|
|
1179
1179
|
var { Kafka: KafkaClass, logLevel: KafkaLogLevel } = KafkaJS;
|
|
1180
1180
|
var _activeTransactionalIds = /* @__PURE__ */ new Set();
|
|
1181
1181
|
var AsyncQueue = class {
|
|
1182
|
+
constructor(highWaterMark = Infinity, onFull = () => {
|
|
1183
|
+
}, onDrained = () => {
|
|
1184
|
+
}) {
|
|
1185
|
+
this.highWaterMark = highWaterMark;
|
|
1186
|
+
this.onFull = onFull;
|
|
1187
|
+
this.onDrained = onDrained;
|
|
1188
|
+
}
|
|
1182
1189
|
items = [];
|
|
1183
1190
|
waiting = [];
|
|
1184
1191
|
closed = false;
|
|
1192
|
+
error;
|
|
1193
|
+
paused = false;
|
|
1185
1194
|
push(item) {
|
|
1186
1195
|
if (this.waiting.length > 0) {
|
|
1187
|
-
this.waiting.shift()({ value: item, done: false });
|
|
1196
|
+
this.waiting.shift().resolve({ value: item, done: false });
|
|
1188
1197
|
} else {
|
|
1189
1198
|
this.items.push(item);
|
|
1199
|
+
if (!this.paused && this.items.length >= this.highWaterMark) {
|
|
1200
|
+
this.paused = true;
|
|
1201
|
+
this.onFull();
|
|
1202
|
+
}
|
|
1190
1203
|
}
|
|
1191
1204
|
}
|
|
1205
|
+
fail(err) {
|
|
1206
|
+
this.closed = true;
|
|
1207
|
+
this.error = err;
|
|
1208
|
+
for (const { reject } of this.waiting.splice(0)) reject(err);
|
|
1209
|
+
}
|
|
1192
1210
|
close() {
|
|
1193
1211
|
this.closed = true;
|
|
1194
|
-
for (const
|
|
1195
|
-
|
|
1196
|
-
}
|
|
1212
|
+
for (const { resolve } of this.waiting.splice(0))
|
|
1213
|
+
resolve({ value: void 0, done: true });
|
|
1197
1214
|
}
|
|
1198
1215
|
next() {
|
|
1199
|
-
if (this.
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1216
|
+
if (this.error) return Promise.reject(this.error);
|
|
1217
|
+
if (this.items.length > 0) {
|
|
1218
|
+
const value = this.items.shift();
|
|
1219
|
+
if (this.paused && this.items.length <= Math.floor(this.highWaterMark / 2)) {
|
|
1220
|
+
this.paused = false;
|
|
1221
|
+
this.onDrained();
|
|
1222
|
+
}
|
|
1223
|
+
return Promise.resolve({ value, done: false });
|
|
1224
|
+
}
|
|
1225
|
+
if (this.closed) return Promise.resolve({ value: void 0, done: true });
|
|
1226
|
+
return new Promise((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
1204
1227
|
}
|
|
1205
1228
|
};
|
|
1206
1229
|
var KafkaClient = class _KafkaClient {
|
|
@@ -1227,6 +1250,7 @@ var KafkaClient = class _KafkaClient {
|
|
|
1227
1250
|
companionGroupIds = /* @__PURE__ */ new Map();
|
|
1228
1251
|
instrumentation;
|
|
1229
1252
|
onMessageLost;
|
|
1253
|
+
onTtlExpired;
|
|
1230
1254
|
onRebalance;
|
|
1231
1255
|
/** Transactional producer ID — configurable via `KafkaClientOptions.transactionalId`. */
|
|
1232
1256
|
txId;
|
|
@@ -1258,6 +1282,7 @@ var KafkaClient = class _KafkaClient {
|
|
|
1258
1282
|
this.numPartitions = options?.numPartitions ?? 1;
|
|
1259
1283
|
this.instrumentation = options?.instrumentation ?? [];
|
|
1260
1284
|
this.onMessageLost = options?.onMessageLost;
|
|
1285
|
+
this.onTtlExpired = options?.onTtlExpired;
|
|
1261
1286
|
this.onRebalance = options?.onRebalance;
|
|
1262
1287
|
this.txId = options?.transactionalId ?? `${clientId}-tx`;
|
|
1263
1288
|
this.kafka = new KafkaClass({
|
|
@@ -1525,7 +1550,12 @@ var KafkaClient = class _KafkaClient {
|
|
|
1525
1550
|
* }
|
|
1526
1551
|
*/
|
|
1527
1552
|
consume(topic2, options) {
|
|
1528
|
-
const
|
|
1553
|
+
const gid = options?.groupId ?? this.defaultGroupId;
|
|
1554
|
+
const queue = new AsyncQueue(
|
|
1555
|
+
options?.queueHighWaterMark,
|
|
1556
|
+
() => this.pauseTopicAllPartitions(gid, topic2),
|
|
1557
|
+
() => this.resumeTopicAllPartitions(gid, topic2)
|
|
1558
|
+
);
|
|
1529
1559
|
const handlePromise = this.startConsumer(
|
|
1530
1560
|
[topic2],
|
|
1531
1561
|
async (envelope) => {
|
|
@@ -1533,6 +1563,7 @@ var KafkaClient = class _KafkaClient {
|
|
|
1533
1563
|
},
|
|
1534
1564
|
options
|
|
1535
1565
|
);
|
|
1566
|
+
handlePromise.catch((err) => queue.fail(err));
|
|
1536
1567
|
return {
|
|
1537
1568
|
[Symbol.asyncIterator]() {
|
|
1538
1569
|
return this;
|
|
@@ -1666,6 +1697,24 @@ var KafkaClient = class _KafkaClient {
|
|
|
1666
1697
|
)
|
|
1667
1698
|
);
|
|
1668
1699
|
}
|
|
1700
|
+
/** Pause all assigned partitions of a topic for a consumer group (used for queue backpressure). */
|
|
1701
|
+
pauseTopicAllPartitions(gid, topic2) {
|
|
1702
|
+
const consumer = this.consumers.get(gid);
|
|
1703
|
+
if (!consumer) return;
|
|
1704
|
+
const assignment = consumer.assignment?.() ?? [];
|
|
1705
|
+
const partitions = assignment.filter((a) => a.topic === topic2).map((a) => a.partition);
|
|
1706
|
+
if (partitions.length > 0)
|
|
1707
|
+
consumer.pause(partitions.map((p) => ({ topic: topic2, partitions: [p] })));
|
|
1708
|
+
}
|
|
1709
|
+
/** Resume all assigned partitions of a topic for a consumer group (used for queue backpressure). */
|
|
1710
|
+
resumeTopicAllPartitions(gid, topic2) {
|
|
1711
|
+
const consumer = this.consumers.get(gid);
|
|
1712
|
+
if (!consumer) return;
|
|
1713
|
+
const assignment = consumer.assignment?.() ?? [];
|
|
1714
|
+
const partitions = assignment.filter((a) => a.topic === topic2).map((a) => a.partition);
|
|
1715
|
+
if (partitions.length > 0)
|
|
1716
|
+
consumer.resume(partitions.map((p) => ({ topic: topic2, partitions: [p] })));
|
|
1717
|
+
}
|
|
1669
1718
|
/** DLQ header keys added by `sendToDlq` — stripped before re-publishing. */
|
|
1670
1719
|
static DLQ_HEADER_KEYS = /* @__PURE__ */ new Set([
|
|
1671
1720
|
"x-dlq-original-topic",
|
|
@@ -1802,6 +1851,49 @@ var KafkaClient = class _KafkaClient {
|
|
|
1802
1851
|
);
|
|
1803
1852
|
}
|
|
1804
1853
|
}
|
|
1854
|
+
async seekToTimestamp(groupId, assignments) {
|
|
1855
|
+
const gid = groupId ?? this.defaultGroupId;
|
|
1856
|
+
if (this.runningConsumers.has(gid)) {
|
|
1857
|
+
throw new Error(
|
|
1858
|
+
`seekToTimestamp: consumer group "${gid}" is still running. Call stopConsumer("${gid}") before seeking offsets.`
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
await this.ensureAdminConnected();
|
|
1862
|
+
const byTopic = /* @__PURE__ */ new Map();
|
|
1863
|
+
for (const { topic: topic2, partition, timestamp } of assignments) {
|
|
1864
|
+
const list = byTopic.get(topic2) ?? [];
|
|
1865
|
+
list.push({ partition, timestamp });
|
|
1866
|
+
byTopic.set(topic2, list);
|
|
1867
|
+
}
|
|
1868
|
+
for (const [topic2, parts] of byTopic) {
|
|
1869
|
+
const offsets = await Promise.all(
|
|
1870
|
+
parts.map(async ({ partition, timestamp }) => {
|
|
1871
|
+
const results = await this.admin.fetchTopicOffsetsByTime(
|
|
1872
|
+
topic2,
|
|
1873
|
+
timestamp
|
|
1874
|
+
);
|
|
1875
|
+
const found = results.find(
|
|
1876
|
+
(r) => r.partition === partition
|
|
1877
|
+
);
|
|
1878
|
+
return { partition, offset: found?.offset ?? "-1" };
|
|
1879
|
+
})
|
|
1880
|
+
);
|
|
1881
|
+
await this.admin.setOffsets({ groupId: gid, topic: topic2, partitions: offsets });
|
|
1882
|
+
this.logger.log(
|
|
1883
|
+
`Offsets set by timestamp for group "${gid}" on "${topic2}": ${JSON.stringify(offsets)}`
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
getCircuitState(topic2, partition, groupId) {
|
|
1888
|
+
const gid = groupId ?? this.defaultGroupId;
|
|
1889
|
+
const state = this.circuitStates.get(`${gid}:${topic2}:${partition}`);
|
|
1890
|
+
if (!state) return void 0;
|
|
1891
|
+
return {
|
|
1892
|
+
status: state.status,
|
|
1893
|
+
failures: state.window.filter((v) => !v).length,
|
|
1894
|
+
windowSize: state.window.length
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1805
1897
|
/**
|
|
1806
1898
|
* Query consumer group lag per partition.
|
|
1807
1899
|
* Lag = broker high-watermark − last committed offset.
|
|
@@ -2022,6 +2114,11 @@ var KafkaClient = class _KafkaClient {
|
|
|
2022
2114
|
if (state.status === "open") return;
|
|
2023
2115
|
const openCircuit = () => {
|
|
2024
2116
|
state.status = "open";
|
|
2117
|
+
state.window = [];
|
|
2118
|
+
state.successes = 0;
|
|
2119
|
+
clearTimeout(state.timer);
|
|
2120
|
+
for (const inst of this.instrumentation)
|
|
2121
|
+
inst.onCircuitOpen?.(envelope.topic, envelope.partition);
|
|
2025
2122
|
this.pauseConsumer(gid, [
|
|
2026
2123
|
{ topic: envelope.topic, partitions: [envelope.partition] }
|
|
2027
2124
|
]);
|
|
@@ -2031,6 +2128,8 @@ var KafkaClient = class _KafkaClient {
|
|
|
2031
2128
|
this.logger.log(
|
|
2032
2129
|
`[CircuitBreaker] HALF-OPEN \u2014 group="${gid}" topic="${envelope.topic}" partition=${envelope.partition}`
|
|
2033
2130
|
);
|
|
2131
|
+
for (const inst of this.instrumentation)
|
|
2132
|
+
inst.onCircuitHalfOpen?.(envelope.topic, envelope.partition);
|
|
2034
2133
|
this.resumeConsumer(gid, [
|
|
2035
2134
|
{ topic: envelope.topic, partitions: [envelope.partition] }
|
|
2036
2135
|
]);
|
|
@@ -2086,6 +2185,8 @@ var KafkaClient = class _KafkaClient {
|
|
|
2086
2185
|
this.logger.log(
|
|
2087
2186
|
`[CircuitBreaker] CLOSED \u2014 group="${gid}" topic="${envelope.topic}" partition=${envelope.partition}`
|
|
2088
2187
|
);
|
|
2188
|
+
for (const inst of this.instrumentation)
|
|
2189
|
+
inst.onCircuitClose?.(envelope.topic, envelope.partition);
|
|
2089
2190
|
}
|
|
2090
2191
|
} else if (state.status === "closed") {
|
|
2091
2192
|
const threshold = cfg.threshold ?? 5;
|
|
@@ -2322,6 +2423,7 @@ var KafkaClient = class _KafkaClient {
|
|
|
2322
2423
|
producer: this.producer,
|
|
2323
2424
|
instrumentation: this.instrumentation,
|
|
2324
2425
|
onMessageLost: this.onMessageLost,
|
|
2426
|
+
onTtlExpired: this.onTtlExpired,
|
|
2325
2427
|
onRetry: this.notifyRetry.bind(this),
|
|
2326
2428
|
onDlq: (envelope, reason) => this.notifyDlq(envelope, reason, gid),
|
|
2327
2429
|
onDuplicate: this.notifyDuplicate.bind(this),
|
|
@@ -2379,4 +2481,4 @@ export {
|
|
|
2379
2481
|
KafkaClient,
|
|
2380
2482
|
topic
|
|
2381
2483
|
};
|
|
2382
|
-
//# sourceMappingURL=chunk-
|
|
2484
|
+
//# sourceMappingURL=chunk-AMEGMOZH.mjs.map
|