@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.
- package/dist/apis/admin/alter-client-quotas-v1.d.ts +11 -5
- package/dist/apis/admin/alter-client-quotas-v1.js +3 -1
- package/dist/apis/admin/describe-client-quotas-v0.d.ts +12 -6
- package/dist/apis/admin/describe-client-quotas-v0.js +1 -0
- package/dist/apis/callbacks.d.ts +1 -0
- package/dist/apis/callbacks.js +15 -1
- package/dist/apis/consumer/consumer-group-heartbeat-v0.d.ts +1 -1
- package/dist/apis/consumer/consumer-group-heartbeat-v0.js +8 -10
- package/dist/apis/enumerations.d.ts +18 -1
- package/dist/apis/enumerations.js +8 -0
- package/dist/clients/admin/admin.d.ts +9 -1
- package/dist/clients/admin/admin.js +136 -2
- package/dist/clients/admin/options.d.ts +149 -0
- package/dist/clients/admin/options.js +109 -1
- package/dist/clients/admin/types.d.ts +19 -0
- package/dist/clients/base/base.js +3 -1
- package/dist/clients/consumer/consumer.d.ts +5 -1
- package/dist/clients/consumer/consumer.js +316 -13
- package/dist/clients/consumer/messages-stream.d.ts +14 -0
- package/dist/clients/consumer/messages-stream.js +63 -9
- package/dist/clients/consumer/options.d.ts +77 -2
- package/dist/clients/consumer/options.js +19 -1
- package/dist/clients/consumer/types.d.ts +10 -3
- package/dist/clients/metrics.d.ts +10 -3
- package/dist/diagnostic.d.ts +9 -2
- package/dist/diagnostic.js +11 -2
- package/dist/protocol/reader.d.ts +1 -0
- package/dist/protocol/reader.js +6 -0
- package/dist/symbols.d.ts +2 -0
- package/dist/symbols.js +2 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -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](
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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.#
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
#
|
|
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
|
}
|