@platformatic/kafka 1.17.1 → 1.18.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.
@@ -1,3 +1,6 @@
1
+ import { type AlterClientQuotasRequestEntry } from '../../apis/admin/alter-client-quotas-v1.ts';
2
+ import { type DescribeClientQuotasRequestComponent } from '../../apis/admin/describe-client-quotas-v0.ts';
3
+ import { type DescribeLogDirsResponse, type DescribeLogDirsResponseResult, type DescribeLogDirsRequestTopic } from '../../apis/admin/describe-log-dirs-v4.ts';
1
4
  import { type CreateTopicsRequestTopicConfig } from '../../apis/admin/create-topics-v7.ts';
2
5
  import { type ConsumerGroupState } from '../../apis/enumerations.ts';
3
6
  import { type NullableString } from '../../protocol/definitions.ts';
@@ -59,3 +62,19 @@ export interface DescribeGroupsOptions {
59
62
  export interface DeleteGroupsOptions {
60
63
  groups: string[];
61
64
  }
65
+ export interface DescribeClientQuotasOptions {
66
+ components: DescribeClientQuotasRequestComponent[];
67
+ strict?: boolean;
68
+ }
69
+ export interface AlterClientQuotasOptions {
70
+ entries: AlterClientQuotasRequestEntry[];
71
+ validateOnly?: boolean;
72
+ }
73
+ export interface DescribeLogDirsOptions {
74
+ topics: DescribeLogDirsRequestTopic[];
75
+ }
76
+ export interface BrokerLogDirDescription {
77
+ broker: number;
78
+ throttleTimeMs: DescribeLogDirsResponse['throttleTimeMs'];
79
+ results: Omit<DescribeLogDirsResponseResult, 'errorCode'>[];
80
+ }
@@ -232,7 +232,9 @@ export class Base extends EventEmitter {
232
232
  return;
233
233
  }
234
234
  const autocreateTopics = options.autocreateTopics ?? this[kOptions].autocreateTopics;
235
- this[kPerformDeduplicated]('metadata', deduplicateCallback => {
235
+ this[kPerformDeduplicated](
236
+ // Unique key to avoid mixing callbacks
237
+ `metadata-${options.topics.sort().join(',')}-${autocreateTopics}-${options.forceUpdate}`, deduplicateCallback => {
236
238
  this[kPerformWithRetry]('metadata', retryCallback => {
237
239
  this[kGetBootstrapConnection]((error, connection) => {
238
240
  if (error) {
@@ -4,7 +4,7 @@ import { type ConnectionPool } from '../../network/connection-pool.ts';
4
4
  import { Base, kFetchConnections } from '../base/base.ts';
5
5
  import { MessagesStream } from './messages-stream.ts';
6
6
  import { TopicsMap } from './topics-map.ts';
7
- import { type CommitOptions, type ConsumeOptions, type ConsumerOptions, type FetchOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from './types.ts';
7
+ import { type CommitOptions, type ConsumeOptions, type ConsumerOptions, type FetchOptions, type GetLagOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from './types.ts';
8
8
  export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends Base<ConsumerOptions<Key, Value, HeaderKey, HeaderValue>> {
9
9
  #private;
10
10
  groupId: string;
@@ -31,6 +31,10 @@ export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
31
31
  listOffsetsWithTimestamps(options: ListOffsetsOptions): Promise<OffsetsWithTimestamps>;
32
32
  listCommittedOffsets(options: ListCommitsOptions, callback: CallbackWithPromise<Offsets>): void;
33
33
  listCommittedOffsets(options: ListCommitsOptions): Promise<Offsets>;
34
+ getLag(options: GetLagOptions, callback: CallbackWithPromise<Offsets>): void;
35
+ getLag(options: GetLagOptions): Promise<Offsets>;
36
+ startLagMonitoring(options: GetLagOptions, interval: number): void;
37
+ stopLagMonitoring(): void;
34
38
  findGroupCoordinator(callback: CallbackWithPromise<number>): void;
35
39
  findGroupCoordinator(): Promise<number>;
36
40
  joinGroup(options: GroupOptions, callback: CallbackWithPromise<string>): void;
@@ -1,15 +1,16 @@
1
- import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
1
+ import { createPromisifiedCallback, createTimeoutCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
2
2
  import { FetchIsolationLevels, FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
3
- import { consumerCommitsChannel, consumerConsumesChannel, consumerFetchesChannel, consumerGroupChannel, consumerHeartbeatChannel, consumerOffsetsChannel, createDiagnosticContext } from "../../diagnostic.js";
4
- import { UserError } from "../../errors.js";
3
+ import { consumerCommitsChannel, consumerConsumesChannel, consumerFetchesChannel, consumerGroupChannel, consumerHeartbeatChannel, consumerLagChannel, consumerOffsetsChannel, createDiagnosticContext } from "../../diagnostic.js";
4
+ import { protocolErrors, UserError } from "../../errors.js";
5
5
  import { INT32_SIZE } from "../../protocol/definitions.js";
6
6
  import { Reader } from "../../protocol/reader.js";
7
7
  import { Writer } from "../../protocol/writer.js";
8
+ import { kAutocommit, kRefreshOffsetsAndFetch } from "../../symbols.js";
8
9
  import { Base, kAfterCreate, kCheckNotClosed, kClearMetadata, kClosed, kCreateConnectionPool, kFetchConnections, kFormatValidationErrors, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
9
10
  import { defaultBaseOptions } from "../base/options.js";
10
11
  import { ensureMetric } from "../metrics.js";
11
12
  import { MessagesStream } from "./messages-stream.js";
12
- import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, groupIdAndOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
13
+ import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, getLagOptionsValidator, groupIdAndOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
13
14
  import { roundRobinAssigner } from "./partitions-assigners.js";
14
15
  import { TopicsMap } from "./topics-map.js";
15
16
  export class Consumer extends Base {
@@ -18,14 +19,20 @@ export class Consumer extends Base {
18
19
  memberId;
19
20
  topics;
20
21
  assignments;
22
+ #assignments;
21
23
  #members;
22
24
  #membershipActive;
23
25
  #isLeader;
24
26
  #protocol;
25
27
  #coordinatorId;
26
28
  #heartbeatInterval;
29
+ #lastHeartbeatIntervalMs;
27
30
  #lastHeartbeat;
31
+ #useConsumerGroupProtocol;
32
+ #memberEpoch;
33
+ #groupRemoteAssignor;
28
34
  #streams;
35
+ #lagMonitoring;
29
36
  /*
30
37
  The following requests are blocking in Kafka:
31
38
 
@@ -43,6 +50,7 @@ export class Consumer extends Base {
43
50
  [kFetchConnections];
44
51
  // Metrics
45
52
  #metricActiveStreams;
53
+ #metricLags;
46
54
  constructor(options) {
47
55
  super(options);
48
56
  this[kOptions] = Object.assign({}, defaultBaseOptions, defaultConsumerOptions, options);
@@ -52,14 +60,20 @@ export class Consumer extends Base {
52
60
  this.memberId = null;
53
61
  this.topics = new TopicsMap();
54
62
  this.assignments = null;
63
+ this.#assignments = [];
55
64
  this.#members = new Map();
56
65
  this.#membershipActive = false;
57
66
  this.#isLeader = false;
58
67
  this.#protocol = null;
59
68
  this.#coordinatorId = null;
60
69
  this.#heartbeatInterval = null;
70
+ this.#lastHeartbeatIntervalMs = 0;
61
71
  this.#lastHeartbeat = null;
62
72
  this.#streams = new Set();
73
+ this.#lagMonitoring = null;
74
+ this.#memberEpoch = 0;
75
+ this.#useConsumerGroupProtocol = this[kOptions].groupProtocol === 'consumer';
76
+ this.#groupRemoteAssignor = this[kOptions].groupRemoteAssignor ?? null;
63
77
  this.#validateGroupOptions(this[kOptions], groupIdAndOptionsValidator);
64
78
  // Initialize connection pool
65
79
  this[kFetchConnections] = this[kCreateConnectionPool]();
@@ -67,6 +81,7 @@ export class Consumer extends Base {
67
81
  ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers', 'Number of active Kafka consumers').inc();
68
82
  this.#metricActiveStreams = ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers_streams', 'Number of active Kafka consumers streams');
69
83
  this.topics.setMetric(ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers_topics', 'Number of topics being consumed'));
84
+ this.#metricLags = ensureMetric(this[kPrometheus], 'Histogram', 'kafka_consumers_lags', 'Lag of active Kafka consumers');
70
85
  }
71
86
  this[kAfterCreate]('consumer');
72
87
  }
@@ -89,11 +104,19 @@ export class Consumer extends Base {
89
104
  return callback[kCallbackPromise];
90
105
  }
91
106
  this[kClosed] = true;
92
- const closer = this.#membershipActive
93
- ? this.#leaveGroup.bind(this)
94
- : function noopCloser(_, callback) {
107
+ clearTimeout(this.#lagMonitoring);
108
+ let closer;
109
+ if (this.#useConsumerGroupProtocol) {
110
+ closer = this.#leaveGroupConsumerProtocol.bind(this);
111
+ }
112
+ else if (this.#membershipActive) {
113
+ closer = this.#leaveGroupClassicProtocol.bind(this);
114
+ }
115
+ else {
116
+ closer = function noopCloser(_, callback) {
95
117
  callback(null);
96
118
  };
119
+ }
97
120
  closer(force, error => {
98
121
  if (error) {
99
122
  this[kClosed] = false;
@@ -127,6 +150,9 @@ export class Consumer extends Base {
127
150
  if (!baseReady) {
128
151
  return false;
129
152
  }
153
+ if (this.#useConsumerGroupProtocol) {
154
+ return !!this.memberId && this.#memberEpoch >= 0;
155
+ }
130
156
  // We consider the group ready if we have a groupId, a memberId and heartbeat interval
131
157
  return this.#membershipActive && Boolean(this.groupId) && Boolean(this.memberId) && this.#heartbeatInterval !== null;
132
158
  }
@@ -224,6 +250,79 @@ export class Consumer extends Base {
224
250
  consumerOffsetsChannel.traceCallback(this.#listCommittedOffsets, 1, createDiagnosticContext({ client: this, operation: 'listCommittedOffsets', options }), this, options, callback);
225
251
  return callback[kCallbackPromise];
226
252
  }
253
+ getLag(options, callback) {
254
+ if (!callback) {
255
+ callback = createPromisifiedCallback();
256
+ }
257
+ if (this[kCheckNotClosed](callback)) {
258
+ return callback[kCallbackPromise];
259
+ }
260
+ const validationError = this[kValidateOptions](options, getLagOptionsValidator, '/options', false);
261
+ if (validationError) {
262
+ callback(validationError, undefined);
263
+ return callback[kCallbackPromise];
264
+ }
265
+ this.listOffsets(options, (error, offsets) => {
266
+ if (error) {
267
+ this.emit('consumer:lag:error', error);
268
+ callback(error, undefined);
269
+ return;
270
+ }
271
+ // Now gather the last committed offsets from each stream
272
+ const committeds = new Map();
273
+ for (const stream of this.#streams) {
274
+ for (const [topic, offset] of stream.committedOffsets) {
275
+ committeds.set(topic, offset);
276
+ }
277
+ }
278
+ // Build the lag map back. A -1n denotes that the consumer is not assigned to a certain partition
279
+ const lag = new Map();
280
+ for (const [topic, partitions] of offsets) {
281
+ const toInclude = new Set(options.partitions?.[topic] ?? []);
282
+ const hasPartitionsFilter = toInclude.size > 0;
283
+ const partitionLags = [];
284
+ for (let i = 0; i < partitions.length; i++) {
285
+ if (hasPartitionsFilter && !toInclude.has(i)) {
286
+ partitionLags.push(-2n);
287
+ continue;
288
+ }
289
+ const latest = partitions[i];
290
+ const committed = committeds.get(`${topic}:${i}`);
291
+ // If the consumer is not assigned to this partition, we return -1n.
292
+ // Otherwise we compute the lag as latest - committed - 1. The -1 is because latest is the offset of the next message to be produced.
293
+ partitionLags.push(typeof committed === 'undefined' ? -1n : latest - committed - 1n);
294
+ }
295
+ lag.set(topic, partitionLags);
296
+ }
297
+ // Publish to the diagnostic channel
298
+ consumerLagChannel.publish({ client: this, lag });
299
+ // Publish to the metric if available
300
+ if (this.#metricLags) {
301
+ for (const partitions of lag.values()) {
302
+ for (const l of partitions) {
303
+ if (l >= 0n) {
304
+ this.#metricLags.observe(Number(l));
305
+ }
306
+ }
307
+ }
308
+ }
309
+ this.emit('consumer:lag', lag);
310
+ callback(null, lag);
311
+ });
312
+ return callback[kCallbackPromise];
313
+ }
314
+ startLagMonitoring(options, interval) {
315
+ const validationError = this[kValidateOptions](options, getLagOptionsValidator, '/options', false);
316
+ if (validationError) {
317
+ throw validationError;
318
+ }
319
+ this.#lagMonitoring = setTimeout(() => {
320
+ this.getLag(options, () => this.#lagMonitoring.refresh());
321
+ }, interval);
322
+ }
323
+ stopLagMonitoring() {
324
+ clearTimeout(this.#lagMonitoring);
325
+ }
227
326
  findGroupCoordinator(callback) {
228
327
  if (!callback) {
229
328
  callback = createPromisifiedCallback();
@@ -245,6 +344,10 @@ export class Consumer extends Base {
245
344
  if (this[kCheckNotClosed](callback)) {
246
345
  return callback[kCallbackPromise];
247
346
  }
347
+ if (this.#useConsumerGroupProtocol) {
348
+ callback(null, '');
349
+ return callback[kCallbackPromise];
350
+ }
248
351
  const validationError = this[kValidateOptions](options, groupOptionsValidator, '/options', false);
249
352
  if (validationError) {
250
353
  callback(validationError, undefined);
@@ -270,8 +373,12 @@ export class Consumer extends Base {
270
373
  if (this[kCheckNotClosed](callback)) {
271
374
  return callback[kCallbackPromise];
272
375
  }
376
+ if (this.#useConsumerGroupProtocol) {
377
+ callback(null);
378
+ return callback[kCallbackPromise];
379
+ }
273
380
  this.#membershipActive = false;
274
- this.#leaveGroup(force, error => {
381
+ this.#leaveGroupClassicProtocol(force, error => {
275
382
  if (error) {
276
383
  this.#membershipActive = true;
277
384
  callback(error);
@@ -334,7 +441,7 @@ export class Consumer extends Base {
334
441
  groupCallback(error, undefined);
335
442
  return;
336
443
  }
337
- api(connection, this.groupId, this.generationId, this.memberId, null, Array.from(topics.values()), groupCallback);
444
+ api(connection, this.groupId, this.#useConsumerGroupProtocol ? this.#memberEpoch : this.generationId, this.memberId, null, Array.from(topics.values()), groupCallback);
338
445
  });
339
446
  }, error => {
340
447
  callback(error);
@@ -445,9 +552,14 @@ export class Consumer extends Base {
445
552
  groupCallback(error, undefined);
446
553
  return;
447
554
  }
448
- api(connection,
449
- // Note: once we start implementing KIP-848, the memberEpoch must be obtained
450
- [{ groupId: this.groupId, memberId: this.memberId, memberEpoch: -1, topics }], false, groupCallback);
555
+ api(connection, [
556
+ {
557
+ groupId: this.groupId,
558
+ memberId: this.memberId,
559
+ memberEpoch: this.#useConsumerGroupProtocol ? this.#memberEpoch : -1,
560
+ topics
561
+ }
562
+ ], false, groupCallback);
451
563
  });
452
564
  }, (error, response) => {
453
565
  if (error) {
@@ -478,7 +590,7 @@ export class Consumer extends Base {
478
590
  #joinGroup(options, callback) {
479
591
  consumerGroupChannel.traceCallback(this.#performJoinGroup, 1, createDiagnosticContext({ client: this, operation: 'joinGroup', options }), this, options, callback);
480
592
  }
481
- #leaveGroup(force, callback) {
593
+ #leaveGroupClassicProtocol(force, callback) {
482
594
  consumerGroupChannel.traceCallback(this.#performLeaveGroup, 1, createDiagnosticContext({ client: this, operation: 'leaveGroup', force }), this, force, callback);
483
595
  }
484
596
  #syncGroup(partitionsAssigner, callback) {
@@ -533,6 +645,186 @@ export class Consumer extends Base {
533
645
  clearTimeout(this.#heartbeatInterval);
534
646
  this.#heartbeatInterval = null;
535
647
  }
648
+ #consumerGroupHeartbeat(options, callback) {
649
+ options.rebalanceTimeout ??= this[kOptions].rebalanceTimeout;
650
+ consumerHeartbeatChannel.traceCallback(this.#performConsumerGroupHeartbeat, 1, createDiagnosticContext({ client: this, operation: 'consumerGroupHeartbeat' }), this, options, callback);
651
+ }
652
+ #performConsumerGroupHeartbeat(options, callback) {
653
+ this.#performGroupOperation('consumerGroupHeartbeat', (connection, groupCallback) => {
654
+ this.emitWithDebug('consumer:heartbeat', 'start');
655
+ this[kGetApi]('ConsumerGroupHeartbeat', (error, api) => {
656
+ if (error) {
657
+ groupCallback(error, undefined);
658
+ return;
659
+ }
660
+ const timeoutCallback = createTimeoutCallback(groupCallback, this[kOptions].timeout, 'Heartbeat timeout.');
661
+ api(connection, this.groupId, this.memberId || '', this.#memberEpoch, null, // instanceId
662
+ null, // rackId
663
+ options.rebalanceTimeout, this.topics.current, this.#groupRemoteAssignor, this.#assignments, timeoutCallback);
664
+ });
665
+ }, (error, response) => {
666
+ if (this[kClosed]) {
667
+ this.emitWithDebug('consumer:heartbeat', 'end');
668
+ callback(null);
669
+ return;
670
+ }
671
+ if (error) {
672
+ this.#cancelHeartbeat();
673
+ this.emitWithDebug('consumer:heartbeat', 'error', { error });
674
+ const fenced = error.response?.errorCode === protocolErrors.FENCED_MEMBER_EPOCH.code;
675
+ if (fenced) {
676
+ this.#assignments = [];
677
+ this.assignments = [];
678
+ this.#memberEpoch = 0;
679
+ this.#consumerGroupHeartbeat(options, () => { });
680
+ callback(error);
681
+ return;
682
+ }
683
+ this.#heartbeatInterval = setTimeout(() => {
684
+ this.#consumerGroupHeartbeat(options, () => { });
685
+ }, this.#lastHeartbeatIntervalMs || 1000);
686
+ callback(error);
687
+ return;
688
+ }
689
+ this.#lastHeartbeat = new Date();
690
+ this.#memberEpoch = response.memberEpoch;
691
+ if (response.memberId) {
692
+ const changed = this.memberId !== response.memberId;
693
+ this.memberId = response.memberId;
694
+ if (changed) {
695
+ this.memberId = response.memberId;
696
+ this.#consumerGroupHeartbeat(options, () => { });
697
+ this.emitWithDebug('consumer:heartbeat', 'end');
698
+ callback(null);
699
+ return;
700
+ }
701
+ }
702
+ if (response.heartbeatIntervalMs > 0) {
703
+ this.#cancelHeartbeat();
704
+ this.#lastHeartbeatIntervalMs = response.heartbeatIntervalMs;
705
+ this.#heartbeatInterval = setTimeout(() => {
706
+ this.#consumerGroupHeartbeat(options, () => { });
707
+ }, response.heartbeatIntervalMs);
708
+ }
709
+ const newAssignments = response.assignment?.topicPartitions;
710
+ if (newAssignments) {
711
+ this.#revokePartitions(newAssignments);
712
+ this.#assignPartitions(newAssignments);
713
+ }
714
+ this.emitWithDebug('consumer:heartbeat', 'end');
715
+ callback(null);
716
+ });
717
+ }
718
+ #diffAssignments(A, B) {
719
+ const result = [];
720
+ for (const a of A) {
721
+ const b = B.find(tp => tp.topicId === a.topicId);
722
+ if (!b) {
723
+ result.push(a);
724
+ }
725
+ else {
726
+ const diff = a.partitions.filter(partition => !b.partitions.includes(partition));
727
+ if (diff.length > 0) {
728
+ result.push({ topicId: a.topicId, partitions: diff });
729
+ }
730
+ }
731
+ }
732
+ return result;
733
+ }
734
+ #revokePartitions(newAssignment) {
735
+ const toRevoke = this.#diffAssignments(this.#assignments, newAssignment);
736
+ if (toRevoke.length === 0) {
737
+ return;
738
+ }
739
+ for (const stream of this.#streams) {
740
+ stream.pause();
741
+ stream[kAutocommit]();
742
+ }
743
+ this.#updateAssignments(newAssignment, error => {
744
+ for (const stream of this.#streams) {
745
+ stream.resume();
746
+ }
747
+ /* c8 ignore next 3 - Hard to test */
748
+ if (error) {
749
+ return;
750
+ }
751
+ this.#cancelHeartbeat();
752
+ this.#consumerGroupHeartbeat(this[kOptions], () => { });
753
+ });
754
+ }
755
+ #assignPartitions(newAssignment) {
756
+ const toAssign = this.#diffAssignments(newAssignment, this.#assignments);
757
+ if (toAssign.length === 0) {
758
+ return;
759
+ }
760
+ this.#updateAssignments(newAssignment, error => {
761
+ if (error) {
762
+ return;
763
+ }
764
+ this.#cancelHeartbeat();
765
+ this.#consumerGroupHeartbeat(this[kOptions], () => { });
766
+ for (const stream of this.#streams) {
767
+ // 3. Refresh partition offsets
768
+ stream[kRefreshOffsetsAndFetch]();
769
+ }
770
+ });
771
+ }
772
+ #updateAssignments(newAssignments, callback) {
773
+ this[kMetadata]({ topics: this.topics.current }, (error, metadata) => {
774
+ if (error) {
775
+ callback(error);
776
+ return;
777
+ }
778
+ const topicIdToTopic = new Map();
779
+ for (const [topic, topicMetadata] of metadata.topics) {
780
+ topicIdToTopic.set(topicMetadata.id, topic);
781
+ }
782
+ const assignments = newAssignments.map(tp => ({
783
+ topic: topicIdToTopic.get(tp.topicId),
784
+ partitions: tp.partitions
785
+ }));
786
+ this.#assignments = newAssignments;
787
+ this.assignments = assignments;
788
+ callback(null);
789
+ });
790
+ }
791
+ #joinGroupConsumerProtocol(options, callback) {
792
+ this.#memberEpoch = 0;
793
+ this.#assignments = [];
794
+ this.#membershipActive = true;
795
+ this.#consumerGroupHeartbeat(options, err => {
796
+ if (this.memberId) {
797
+ this.emitWithDebug('consumer', 'group:join', { groupId: this.groupId, memberId: this.memberId });
798
+ }
799
+ callback(err);
800
+ });
801
+ }
802
+ #leaveGroupConsumerProtocol(_, callback) {
803
+ // Leave by sending a heartbeat with memberEpoch = -1
804
+ this.#cancelHeartbeat();
805
+ this.#performDeduplicateGroupOperaton('leaveGroupConsumerProtocol', (connection, groupCallback) => {
806
+ this[kGetApi]('ConsumerGroupHeartbeat', (error, api) => {
807
+ if (error) {
808
+ groupCallback(error, undefined);
809
+ return;
810
+ }
811
+ api(connection, this.groupId, this.memberId, -1, // memberEpoch = -1 signals leave
812
+ null, // instanceId
813
+ null, // rackId
814
+ 0, // rebalanceTimeout
815
+ [], // subscribedTopicNames
816
+ this.#groupRemoteAssignor, [], // topicPartitions
817
+ groupCallback);
818
+ });
819
+ }, _error => {
820
+ this.emitWithDebug('consumer', 'group:leave', { groupId: this.groupId, memberId: this.memberId });
821
+ this.memberId = null;
822
+ this.#memberEpoch = -1;
823
+ this.#assignments = [];
824
+ this.assignments = [];
825
+ callback(null);
826
+ });
827
+ }
536
828
  #performConsume(options, trackTopics, callback) {
537
829
  // Subscribe all topics
538
830
  let joinNeeded = this.memberId === null;
@@ -545,6 +837,17 @@ export class Consumer extends Base {
545
837
  }
546
838
  // If we need to (re)join the group, do that first and then try again
547
839
  if (joinNeeded) {
840
+ if (this.#useConsumerGroupProtocol) {
841
+ this.#joinGroupConsumerProtocol(options, error => {
842
+ if (error) {
843
+ callback(error, undefined);
844
+ return;
845
+ }
846
+ this.#performConsume(options, false, callback);
847
+ });
848
+ return;
849
+ }
850
+ // Classic consumer protocol join
548
851
  this.joinGroup(options, error => {
549
852
  if (error) {
550
853
  callback(error, undefined);
@@ -1,6 +1,7 @@
1
1
  import { Readable } from 'node:stream';
2
2
  import { type CallbackWithPromise } from '../../apis/callbacks.ts';
3
3
  import { type Message } from '../../protocol/records.ts';
4
+ import { kAutocommit, kInstance, kRefreshOffsetsAndFetch } from '../../symbols.ts';
4
5
  import { kInspect } from '../base/base.ts';
5
6
  import { type Consumer } from './consumer.ts';
6
7
  import { type CommitOptionsPartition, type ConsumeOptions } from './types.ts';
@@ -8,12 +9,17 @@ export declare function noopDeserializer(data?: Buffer): Buffer | undefined;
8
9
  export declare function defaultCorruptedMessageHandler(): boolean;
9
10
  export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends Readable {
10
11
  #private;
12
+ [kInstance]: number;
11
13
  constructor(consumer: Consumer<Key, Value, HeaderKey, HeaderValue>, options: ConsumeOptions<Key, Value, HeaderKey, HeaderValue>);
14
+ get committedOffsets(): Map<string, bigint>;
12
15
  close(callback: CallbackWithPromise<void>): void;
13
16
  close(): Promise<void>;
14
17
  isActive(): boolean;
15
18
  isConnected(): boolean;
19
+ resume(): this;
20
+ pause(): this;
16
21
  addListener(event: 'autocommit', listener: (err: Error, offsets: CommitOptionsPartition[]) => void): this;
22
+ addListener(event: 'fetch', listener: () => void): this;
17
23
  addListener(event: 'data', listener: (message: Message<Key, Value, HeaderKey, HeaderValue>) => void): this;
18
24
  addListener(event: 'close', listener: () => void): this;
19
25
  addListener(event: 'end', listener: () => void): this;
@@ -22,6 +28,7 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
22
28
  addListener(event: 'readable', listener: () => void): this;
23
29
  addListener(event: 'resume', listener: () => void): this;
24
30
  on(event: 'autocommit', listener: (err: Error, offsets: CommitOptionsPartition[]) => void): this;
31
+ on(event: 'fetch', listener: () => void): this;
25
32
  on(event: 'data', listener: (message: Message<Key, Value, HeaderKey, HeaderValue>) => void): this;
26
33
  on(event: 'close', listener: () => void): this;
27
34
  on(event: 'end', listener: () => void): this;
@@ -30,6 +37,7 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
30
37
  on(event: 'readable', listener: () => void): this;
31
38
  on(event: 'resume', listener: () => void): this;
32
39
  once(event: 'autocommit', listener: (err: Error, offsets: CommitOptionsPartition[]) => void): this;
40
+ once(event: 'fetch', listener: () => void): this;
33
41
  once(event: 'data', listener: (message: Message<Key, Value, HeaderKey, HeaderValue>) => void): this;
34
42
  once(event: 'close', listener: () => void): this;
35
43
  once(event: 'end', listener: () => void): this;
@@ -37,6 +45,8 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
37
45
  once(event: 'pause', listener: () => void): this;
38
46
  once(event: 'readable', listener: () => void): this;
39
47
  once(event: 'resume', listener: () => void): this;
48
+ prependListener(event: 'autocommit', listener: (err: Error, offsets: CommitOptionsPartition[]) => void): this;
49
+ prependListener(event: 'fetch', listener: () => void): this;
40
50
  prependListener(event: 'data', listener: (message: Message<Key, Value, HeaderKey, HeaderValue>) => void): this;
41
51
  prependListener(event: 'close', listener: () => void): this;
42
52
  prependListener(event: 'end', listener: () => void): this;
@@ -44,6 +54,8 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
44
54
  prependListener(event: 'pause', listener: () => void): this;
45
55
  prependListener(event: 'readable', listener: () => void): this;
46
56
  prependListener(event: 'resume', listener: () => void): this;
57
+ prependOnceListener(event: 'autocommit', listener: (err: Error, offsets: CommitOptionsPartition[]) => void): this;
58
+ prependOnceListener(event: 'fetch', listener: () => void): this;
47
59
  prependOnceListener(event: 'data', listener: (message: Message<Key, Value, HeaderKey, HeaderValue>) => void): this;
48
60
  prependOnceListener(event: 'close', listener: () => void): this;
49
61
  prependOnceListener(event: 'end', listener: () => void): this;
@@ -55,5 +67,7 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
55
67
  _construct(callback: (error?: Error) => void): void;
56
68
  _destroy(error: Error | null, callback: (error?: Error | null) => void): void;
57
69
  _read(): void;
70
+ [kAutocommit](): void;
71
+ [kRefreshOffsetsAndFetch](): void;
58
72
  [kInspect](...args: unknown[]): void;
59
73
  }