@platformatic/kafka 1.17.0 → 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 +328 -22
- 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,14 +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
|
+
import { INT32_SIZE } from "../../protocol/definitions.js";
|
|
5
6
|
import { Reader } from "../../protocol/reader.js";
|
|
6
7
|
import { Writer } from "../../protocol/writer.js";
|
|
8
|
+
import { kAutocommit, kRefreshOffsetsAndFetch } from "../../symbols.js";
|
|
7
9
|
import { Base, kAfterCreate, kCheckNotClosed, kClearMetadata, kClosed, kCreateConnectionPool, kFetchConnections, kFormatValidationErrors, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
8
10
|
import { defaultBaseOptions } from "../base/options.js";
|
|
9
11
|
import { ensureMetric } from "../metrics.js";
|
|
10
12
|
import { MessagesStream } from "./messages-stream.js";
|
|
11
|
-
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";
|
|
12
14
|
import { roundRobinAssigner } from "./partitions-assigners.js";
|
|
13
15
|
import { TopicsMap } from "./topics-map.js";
|
|
14
16
|
export class Consumer extends Base {
|
|
@@ -17,15 +19,20 @@ export class Consumer extends Base {
|
|
|
17
19
|
memberId;
|
|
18
20
|
topics;
|
|
19
21
|
assignments;
|
|
22
|
+
#assignments;
|
|
20
23
|
#members;
|
|
21
24
|
#membershipActive;
|
|
22
25
|
#isLeader;
|
|
23
26
|
#protocol;
|
|
24
27
|
#coordinatorId;
|
|
25
28
|
#heartbeatInterval;
|
|
29
|
+
#lastHeartbeatIntervalMs;
|
|
26
30
|
#lastHeartbeat;
|
|
31
|
+
#useConsumerGroupProtocol;
|
|
32
|
+
#memberEpoch;
|
|
33
|
+
#groupRemoteAssignor;
|
|
27
34
|
#streams;
|
|
28
|
-
#
|
|
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,15 +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();
|
|
63
|
-
this.#
|
|
73
|
+
this.#lagMonitoring = null;
|
|
74
|
+
this.#memberEpoch = 0;
|
|
75
|
+
this.#useConsumerGroupProtocol = this[kOptions].groupProtocol === 'consumer';
|
|
76
|
+
this.#groupRemoteAssignor = this[kOptions].groupRemoteAssignor ?? null;
|
|
64
77
|
this.#validateGroupOptions(this[kOptions], groupIdAndOptionsValidator);
|
|
65
78
|
// Initialize connection pool
|
|
66
79
|
this[kFetchConnections] = this[kCreateConnectionPool]();
|
|
@@ -68,6 +81,7 @@ export class Consumer extends Base {
|
|
|
68
81
|
ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers', 'Number of active Kafka consumers').inc();
|
|
69
82
|
this.#metricActiveStreams = ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers_streams', 'Number of active Kafka consumers streams');
|
|
70
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');
|
|
71
85
|
}
|
|
72
86
|
this[kAfterCreate]('consumer');
|
|
73
87
|
}
|
|
@@ -90,11 +104,19 @@ export class Consumer extends Base {
|
|
|
90
104
|
return callback[kCallbackPromise];
|
|
91
105
|
}
|
|
92
106
|
this[kClosed] = true;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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) {
|
|
96
117
|
callback(null);
|
|
97
118
|
};
|
|
119
|
+
}
|
|
98
120
|
closer(force, error => {
|
|
99
121
|
if (error) {
|
|
100
122
|
this[kClosed] = false;
|
|
@@ -128,6 +150,9 @@ export class Consumer extends Base {
|
|
|
128
150
|
if (!baseReady) {
|
|
129
151
|
return false;
|
|
130
152
|
}
|
|
153
|
+
if (this.#useConsumerGroupProtocol) {
|
|
154
|
+
return !!this.memberId && this.#memberEpoch >= 0;
|
|
155
|
+
}
|
|
131
156
|
// We consider the group ready if we have a groupId, a memberId and heartbeat interval
|
|
132
157
|
return this.#membershipActive && Boolean(this.groupId) && Boolean(this.memberId) && this.#heartbeatInterval !== null;
|
|
133
158
|
}
|
|
@@ -225,6 +250,79 @@ export class Consumer extends Base {
|
|
|
225
250
|
consumerOffsetsChannel.traceCallback(this.#listCommittedOffsets, 1, createDiagnosticContext({ client: this, operation: 'listCommittedOffsets', options }), this, options, callback);
|
|
226
251
|
return callback[kCallbackPromise];
|
|
227
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
|
+
}
|
|
228
326
|
findGroupCoordinator(callback) {
|
|
229
327
|
if (!callback) {
|
|
230
328
|
callback = createPromisifiedCallback();
|
|
@@ -246,6 +344,10 @@ export class Consumer extends Base {
|
|
|
246
344
|
if (this[kCheckNotClosed](callback)) {
|
|
247
345
|
return callback[kCallbackPromise];
|
|
248
346
|
}
|
|
347
|
+
if (this.#useConsumerGroupProtocol) {
|
|
348
|
+
callback(null, '');
|
|
349
|
+
return callback[kCallbackPromise];
|
|
350
|
+
}
|
|
249
351
|
const validationError = this[kValidateOptions](options, groupOptionsValidator, '/options', false);
|
|
250
352
|
if (validationError) {
|
|
251
353
|
callback(validationError, undefined);
|
|
@@ -271,8 +373,12 @@ export class Consumer extends Base {
|
|
|
271
373
|
if (this[kCheckNotClosed](callback)) {
|
|
272
374
|
return callback[kCallbackPromise];
|
|
273
375
|
}
|
|
376
|
+
if (this.#useConsumerGroupProtocol) {
|
|
377
|
+
callback(null);
|
|
378
|
+
return callback[kCallbackPromise];
|
|
379
|
+
}
|
|
274
380
|
this.#membershipActive = false;
|
|
275
|
-
this.#
|
|
381
|
+
this.#leaveGroupClassicProtocol(force, error => {
|
|
276
382
|
if (error) {
|
|
277
383
|
this.#membershipActive = true;
|
|
278
384
|
callback(error);
|
|
@@ -335,7 +441,7 @@ export class Consumer extends Base {
|
|
|
335
441
|
groupCallback(error, undefined);
|
|
336
442
|
return;
|
|
337
443
|
}
|
|
338
|
-
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);
|
|
339
445
|
});
|
|
340
446
|
}, error => {
|
|
341
447
|
callback(error);
|
|
@@ -446,9 +552,14 @@ export class Consumer extends Base {
|
|
|
446
552
|
groupCallback(error, undefined);
|
|
447
553
|
return;
|
|
448
554
|
}
|
|
449
|
-
api(connection,
|
|
450
|
-
|
|
451
|
-
|
|
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);
|
|
452
563
|
});
|
|
453
564
|
}, (error, response) => {
|
|
454
565
|
if (error) {
|
|
@@ -479,11 +590,11 @@ export class Consumer extends Base {
|
|
|
479
590
|
#joinGroup(options, callback) {
|
|
480
591
|
consumerGroupChannel.traceCallback(this.#performJoinGroup, 1, createDiagnosticContext({ client: this, operation: 'joinGroup', options }), this, options, callback);
|
|
481
592
|
}
|
|
482
|
-
#
|
|
593
|
+
#leaveGroupClassicProtocol(force, callback) {
|
|
483
594
|
consumerGroupChannel.traceCallback(this.#performLeaveGroup, 1, createDiagnosticContext({ client: this, operation: 'leaveGroup', force }), this, force, callback);
|
|
484
595
|
}
|
|
485
|
-
#syncGroup(callback) {
|
|
486
|
-
consumerGroupChannel.traceCallback(this.#performSyncGroup,
|
|
596
|
+
#syncGroup(partitionsAssigner, callback) {
|
|
597
|
+
consumerGroupChannel.traceCallback(this.#performSyncGroup, 2, createDiagnosticContext({ client: this, operation: 'syncGroup' }), this, partitionsAssigner, null, callback);
|
|
487
598
|
}
|
|
488
599
|
#heartbeat(options) {
|
|
489
600
|
const eventPayload = { groupId: this.groupId, memberId: this.memberId, generationId: this.generationId };
|
|
@@ -534,6 +645,186 @@ export class Consumer extends Base {
|
|
|
534
645
|
clearTimeout(this.#heartbeatInterval);
|
|
535
646
|
this.#heartbeatInterval = null;
|
|
536
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
|
+
}
|
|
537
828
|
#performConsume(options, trackTopics, callback) {
|
|
538
829
|
// Subscribe all topics
|
|
539
830
|
let joinNeeded = this.memberId === null;
|
|
@@ -546,6 +837,17 @@ export class Consumer extends Base {
|
|
|
546
837
|
}
|
|
547
838
|
// If we need to (re)join the group, do that first and then try again
|
|
548
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
|
|
549
851
|
this.joinGroup(options, error => {
|
|
550
852
|
if (error) {
|
|
551
853
|
callback(error, undefined);
|
|
@@ -635,7 +937,7 @@ export class Consumer extends Base {
|
|
|
635
937
|
this.#members.set(member.memberId, this.#decodeProtocolSubscriptionMetadata(member.memberId, member.metadata));
|
|
636
938
|
}
|
|
637
939
|
// Send a syncGroup request
|
|
638
|
-
this.#syncGroup((error, response) => {
|
|
940
|
+
this.#syncGroup(options.partitionAssigner, (error, response) => {
|
|
639
941
|
if (!this.#membershipActive) {
|
|
640
942
|
callback(null, undefined);
|
|
641
943
|
return;
|
|
@@ -721,7 +1023,7 @@ export class Consumer extends Base {
|
|
|
721
1023
|
callback(null);
|
|
722
1024
|
});
|
|
723
1025
|
}
|
|
724
|
-
#performSyncGroup(assignments, callback) {
|
|
1026
|
+
#performSyncGroup(partitionsAssigner, assignments, callback) {
|
|
725
1027
|
if (!this.#membershipActive) {
|
|
726
1028
|
callback(null, []);
|
|
727
1029
|
return;
|
|
@@ -745,7 +1047,7 @@ export class Consumer extends Base {
|
|
|
745
1047
|
callback(this.#handleMetadataError(error), undefined);
|
|
746
1048
|
return;
|
|
747
1049
|
}
|
|
748
|
-
this.#performSyncGroup(this.#createAssignments(metadata), callback);
|
|
1050
|
+
this.#performSyncGroup(partitionsAssigner, this.#createAssignments(partitionsAssigner, metadata), callback);
|
|
749
1051
|
});
|
|
750
1052
|
return;
|
|
751
1053
|
}
|
|
@@ -844,6 +1146,9 @@ export class Consumer extends Base {
|
|
|
844
1146
|
#decodeProtocolAssignment(buffer) {
|
|
845
1147
|
const reader = Reader.from(buffer);
|
|
846
1148
|
reader.skip(2); // Ignore Version information
|
|
1149
|
+
if (reader.remaining < INT32_SIZE) {
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
847
1152
|
return reader.readArray(r => {
|
|
848
1153
|
return {
|
|
849
1154
|
topic: r.readString(false),
|
|
@@ -851,7 +1156,7 @@ export class Consumer extends Base {
|
|
|
851
1156
|
};
|
|
852
1157
|
}, false, false);
|
|
853
1158
|
}
|
|
854
|
-
#createAssignments(metadata) {
|
|
1159
|
+
#createAssignments(partitionsAssigner, metadata) {
|
|
855
1160
|
const partitionTracker = new Map();
|
|
856
1161
|
// First of all, layout topics-partitions in a list
|
|
857
1162
|
for (const [topic, partitions] of metadata.topics) {
|
|
@@ -872,7 +1177,8 @@ export class Consumer extends Base {
|
|
|
872
1177
|
return [{ memberId: this.memberId, assignment: this.#encodeProtocolAssignment(assignments) }];
|
|
873
1178
|
}
|
|
874
1179
|
const encodedAssignments = [];
|
|
875
|
-
|
|
1180
|
+
partitionsAssigner ??= this[kOptions].partitionAssigner ?? roundRobinAssigner;
|
|
1181
|
+
for (const member of partitionsAssigner(this.memberId, this.#members, new Set(this.topics.current), metadata)) {
|
|
876
1182
|
encodedAssignments.push({
|
|
877
1183
|
memberId: member.memberId,
|
|
878
1184
|
assignment: this.#encodeProtocolAssignment(Array.from(member.assignments.values()))
|
|
@@ -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
|
}
|