@platformatic/kafka 2.0.1 → 2.2.2
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/clients/base/base.d.ts +2 -2
- package/dist/clients/base/base.js +30 -12
- package/dist/clients/base/options.d.ts +3 -0
- package/dist/clients/base/options.js +1 -0
- package/dist/clients/base/types.d.ts +1 -0
- package/dist/clients/consumer/consumer.d.ts +3 -0
- package/dist/clients/consumer/consumer.js +168 -28
- package/dist/clients/consumer/messages-stream.js +97 -34
- package/dist/clients/consumer/types.d.ts +4 -0
- package/dist/clients/consumer/utils.d.ts +1 -0
- package/dist/clients/consumer/utils.js +3 -0
- package/dist/clients/producer/producer.d.ts +1 -1
- package/dist/clients/producer/producer.js +42 -20
- package/dist/network/connection.js +11 -4
- package/dist/registries/confluent-schema-registry.js +3 -2
- package/dist/symbols.d.ts +1 -0
- package/dist/symbols.js +1 -0
- package/dist/typescript-4/dist/clients/base/base.d.ts +2 -2
- package/dist/typescript-4/dist/clients/base/options.d.ts +3 -0
- package/dist/typescript-4/dist/clients/base/types.d.ts +1 -0
- package/dist/typescript-4/dist/clients/consumer/consumer.d.ts +3 -0
- package/dist/typescript-4/dist/clients/consumer/types.d.ts +4 -0
- package/dist/typescript-4/dist/clients/consumer/utils.d.ts +1 -0
- package/dist/typescript-4/dist/clients/producer/producer.d.ts +1 -1
- package/dist/typescript-4/dist/symbols.d.ts +1 -0
- package/dist/version.js +1 -1
- package/package.json +9 -2
|
@@ -84,11 +84,11 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
|
|
|
84
84
|
[kMetadata](options: MetadataOptions, callback: CallbackWithPromise<ClusterMetadata>): void;
|
|
85
85
|
[kCheckNotClosed](callback: CallbackWithPromise<any>): boolean;
|
|
86
86
|
clearMetadata(): void;
|
|
87
|
-
[kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType
|
|
87
|
+
[kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>, attempt: number) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
|
|
88
88
|
[kPerformDeduplicated]<ReturnType>(operationId: string, operation: (callback: CallbackWithPromise<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>): void | Promise<ReturnType>;
|
|
89
89
|
[kGetApi]<RequestArguments extends Array<unknown>, ResponseType>(name: string, callback: Callback<API<RequestArguments, ResponseType>>): void;
|
|
90
90
|
[kGetConnection](broker: Broker, callback: Callback<Connection>): void;
|
|
91
|
-
[kGetBootstrapConnection](callback: Callback<Connection
|
|
91
|
+
[kGetBootstrapConnection](callback: Callback<Connection>, attempt?: number): void;
|
|
92
92
|
[kValidateOptions](target: unknown, validator: ValidateFunction<unknown>, targetName: string, throwOnErrors?: boolean): Error | null;
|
|
93
93
|
[kInspect](...args: unknown[]): void;
|
|
94
94
|
[kFormatValidationErrors](validator: ValidateFunction<unknown>, targetName: string): string;
|
|
@@ -55,7 +55,12 @@ export class Base extends TypedEventEmitter {
|
|
|
55
55
|
this[kApis] = [];
|
|
56
56
|
this[kContext] = options.context;
|
|
57
57
|
// Validate options
|
|
58
|
-
this[kOptions] = Object.assign({}, defaultBaseOptions
|
|
58
|
+
this[kOptions] = Object.assign({}, defaultBaseOptions);
|
|
59
|
+
for (const [key, value] of Object.entries(options)) {
|
|
60
|
+
if (value !== undefined) {
|
|
61
|
+
this[kOptions][key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
59
64
|
this[kValidateOptions](this[kOptions], baseOptionsValidator, '/options');
|
|
60
65
|
this[kClientId] = options.clientId;
|
|
61
66
|
if (typeof this[kOptions].retries === 'boolean') {
|
|
@@ -201,7 +206,7 @@ export class Base extends TypedEventEmitter {
|
|
|
201
206
|
}
|
|
202
207
|
[kListApis](callback) {
|
|
203
208
|
this[kPerformDeduplicated]('listApis', deduplicateCallback => {
|
|
204
|
-
this[kPerformWithRetry]('listApis', retryCallback => {
|
|
209
|
+
this[kPerformWithRetry]('listApis', (retryCallback, attempt) => {
|
|
205
210
|
this[kGetBootstrapConnection]((error, connection) => {
|
|
206
211
|
if (error) {
|
|
207
212
|
retryCallback(error);
|
|
@@ -209,7 +214,7 @@ export class Base extends TypedEventEmitter {
|
|
|
209
214
|
}
|
|
210
215
|
// We use V3 to be able to get APIS from Kafka 2.4.0+
|
|
211
216
|
apiVersionsV3(connection, clientSoftwareName, clientSoftwareVersion, retryCallback);
|
|
212
|
-
});
|
|
217
|
+
}, attempt);
|
|
213
218
|
}, (error, metadata) => {
|
|
214
219
|
if (error) {
|
|
215
220
|
deduplicateCallback(error);
|
|
@@ -255,7 +260,13 @@ export class Base extends TypedEventEmitter {
|
|
|
255
260
|
this.emitWithDebug('client', 'performWithRetry:retry', operationId, attempt, retries, delay);
|
|
256
261
|
const timeout = setTimeout(() => {
|
|
257
262
|
this.removeListener('client:close', onClose);
|
|
258
|
-
|
|
263
|
+
try {
|
|
264
|
+
this[kPerformWithRetry](operationId, operation, callback, attempt + 1, errors, shouldSkipRetry);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
errors.push(error);
|
|
268
|
+
callback(new MultipleErrors(`${operationId} failed ${attempt + 1} times.`, errors));
|
|
269
|
+
}
|
|
259
270
|
}, delay);
|
|
260
271
|
this.once('client:close', onClose);
|
|
261
272
|
}
|
|
@@ -274,7 +285,7 @@ export class Base extends TypedEventEmitter {
|
|
|
274
285
|
errors.splice(0, errors.length);
|
|
275
286
|
}
|
|
276
287
|
callback(null, result);
|
|
277
|
-
});
|
|
288
|
+
}, attempt);
|
|
278
289
|
return callback[kCallbackPromise];
|
|
279
290
|
}
|
|
280
291
|
[kPerformDeduplicated](operationId, operation, callback) {
|
|
@@ -329,13 +340,20 @@ export class Base extends TypedEventEmitter {
|
|
|
329
340
|
[kGetConnection](broker, callback) {
|
|
330
341
|
this[kConnections].get(broker, callback);
|
|
331
342
|
}
|
|
332
|
-
[kGetBootstrapConnection](callback) {
|
|
343
|
+
[kGetBootstrapConnection](callback, attempt = 0) {
|
|
344
|
+
let brokers;
|
|
333
345
|
if (!this.#metadata) {
|
|
334
|
-
this[
|
|
335
|
-
|
|
346
|
+
brokers = this[kBootstrapBrokers];
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const discovered = Array.from(this.#metadata.brokers.values());
|
|
350
|
+
brokers = [...this[kBootstrapBrokers], ...discovered];
|
|
336
351
|
}
|
|
337
|
-
|
|
338
|
-
|
|
352
|
+
if (attempt > 0 && brokers.length > 1) {
|
|
353
|
+
const offset = attempt % brokers.length;
|
|
354
|
+
brokers = [...brokers.slice(offset), ...brokers.slice(0, offset)];
|
|
355
|
+
}
|
|
356
|
+
this[kConnections].getFirstAvailable(brokers, callback);
|
|
339
357
|
}
|
|
340
358
|
[kValidateOptions](target, validator, targetName, throwOnErrors = true) {
|
|
341
359
|
if (!this[kOptions].strict) {
|
|
@@ -390,7 +408,7 @@ export class Base extends TypedEventEmitter {
|
|
|
390
408
|
this[kPerformDeduplicated](
|
|
391
409
|
// Unique key to avoid mixing callbacks
|
|
392
410
|
`metadata-${topics.sort().join(',')}-${autocreateTopics}-${options.forceUpdate}`, deduplicateCallback => {
|
|
393
|
-
this[kPerformWithRetry]('metadata', retryCallback => {
|
|
411
|
+
this[kPerformWithRetry]('metadata', (retryCallback, attempt) => {
|
|
394
412
|
this[kGetBootstrapConnection]((error, connection) => {
|
|
395
413
|
if (error) {
|
|
396
414
|
retryCallback(error);
|
|
@@ -403,7 +421,7 @@ export class Base extends TypedEventEmitter {
|
|
|
403
421
|
}
|
|
404
422
|
api(connection, topicsToFetch, autocreateTopics, true, retryCallback);
|
|
405
423
|
});
|
|
406
|
-
});
|
|
424
|
+
}, attempt);
|
|
407
425
|
}, (error, metadata) => {
|
|
408
426
|
if (error) {
|
|
409
427
|
const unknownTopicError = error.findBy('apiCode', 3);
|
|
@@ -32,6 +32,7 @@ export interface ClusterMetadata {
|
|
|
32
32
|
export type RetryDelayGetter<Owner = object> = (client: Owner, operationId: string, attempt: number, retries: number, error: Error) => number;
|
|
33
33
|
export interface BaseOptions extends ConnectionOptions {
|
|
34
34
|
clientId: string;
|
|
35
|
+
clientRack?: string;
|
|
35
36
|
bootstrapBrokers: Broker[] | string[];
|
|
36
37
|
context?: unknown;
|
|
37
38
|
timeout?: number;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type CallbackWithPromise } from '../../apis/callbacks.ts';
|
|
2
2
|
import { type FetchResponse } from '../../apis/consumer/fetch-v17.ts';
|
|
3
3
|
import { type ConnectionPool } from '../../network/connection-pool.ts';
|
|
4
|
+
import { kGetFetchNode } from '../../symbols.ts';
|
|
4
5
|
import { Base, type BaseEvents, kCreateConnectionPool } from '../base/base.ts';
|
|
6
|
+
import { type ClusterMetadata } from '../base/types.ts';
|
|
5
7
|
import { MessagesStream } from './messages-stream.ts';
|
|
6
8
|
import { TopicsMap } from './topics-map.ts';
|
|
7
9
|
import { type CommitOptions, type ConsumeOptions, type ConsumerGroupJoinPayload, type ConsumerGroupLeavePayload, type ConsumerGroupRebalancePayload, type ConsumerHeartbeatErrorPayload, type ConsumerHeartbeatPayload, type ConsumerOptions, type FetchOptions, type GetLagOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from './types.ts';
|
|
@@ -56,4 +58,5 @@ export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
|
|
|
56
58
|
joinGroup(options?: GroupOptions): Promise<string>;
|
|
57
59
|
leaveGroup(force?: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
|
|
58
60
|
leaveGroup(force?: boolean): Promise<void>;
|
|
61
|
+
[kGetFetchNode](metadata: ClusterMetadata, topic: string, partition: number, now: number): number;
|
|
59
62
|
}
|
|
@@ -7,7 +7,7 @@ import { INT32_SIZE } from "../../protocol/definitions.js";
|
|
|
7
7
|
import { Reader } from "../../protocol/reader.js";
|
|
8
8
|
import { IS_CONTROL } from "../../protocol/records.js";
|
|
9
9
|
import { Writer } from "../../protocol/writer.js";
|
|
10
|
-
import { kAutocommit, kRefreshOffsetsAndFetch } from "../../symbols.js";
|
|
10
|
+
import { kAutocommit, kGetFetchNode, kRefreshOffsetsAndFetch } from "../../symbols.js";
|
|
11
11
|
import { emitExperimentalApiWarning } from "../../utils.js";
|
|
12
12
|
import { Base, kAfterCreate, kCheckNotClosed, kClosed, kConnections, kCreateConnectionPool, kFormatValidationErrors, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
13
13
|
import { ensureMetric } from "../metrics.js";
|
|
@@ -15,6 +15,7 @@ import { MessagesStream } from "./messages-stream.js";
|
|
|
15
15
|
import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, getLagOptionsValidator, groupIdAndOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
|
|
16
16
|
import { roundRobinAssigner } from "./partitions-assigners.js";
|
|
17
17
|
import { TopicsMap } from "./topics-map.js";
|
|
18
|
+
import { partitionKey } from "./utils.js";
|
|
18
19
|
export class Consumer extends Base {
|
|
19
20
|
groupId;
|
|
20
21
|
groupInstanceId;
|
|
@@ -34,6 +35,8 @@ export class Consumer extends Base {
|
|
|
34
35
|
#useConsumerGroupProtocol;
|
|
35
36
|
#memberEpoch;
|
|
36
37
|
#groupRemoteAssignor;
|
|
38
|
+
#clientRack;
|
|
39
|
+
#preferredReadReplicas;
|
|
37
40
|
#streams;
|
|
38
41
|
#lagMonitoring;
|
|
39
42
|
#streamContext;
|
|
@@ -78,6 +81,8 @@ export class Consumer extends Base {
|
|
|
78
81
|
this.#memberEpoch = 0;
|
|
79
82
|
this.#useConsumerGroupProtocol = this[kOptions].groupProtocol === 'consumer';
|
|
80
83
|
this.#groupRemoteAssignor = this[kOptions].groupRemoteAssignor ?? null;
|
|
84
|
+
this.#clientRack = this[kOptions].clientRack ?? '';
|
|
85
|
+
this.#preferredReadReplicas = new Map();
|
|
81
86
|
this.#streamContext = options.streamContext ?? options.context;
|
|
82
87
|
this.#validateGroupOptions(this[kOptions], groupIdAndOptionsValidator);
|
|
83
88
|
if (this[kPrometheus]) {
|
|
@@ -354,7 +359,7 @@ export class Consumer extends Base {
|
|
|
354
359
|
if (this[kCheckNotClosed](callback)) {
|
|
355
360
|
return callback[kCallbackPromise];
|
|
356
361
|
}
|
|
357
|
-
if (this.#coordinatorId) {
|
|
362
|
+
if (this.#coordinatorId !== null) {
|
|
358
363
|
callback(null, this.#coordinatorId);
|
|
359
364
|
return callback[kCallbackPromise];
|
|
360
365
|
}
|
|
@@ -422,6 +427,52 @@ export class Consumer extends Base {
|
|
|
422
427
|
#consume(options, callback) {
|
|
423
428
|
consumerConsumesChannel.traceCallback(this.#performConsume, 2, createDiagnosticContext({ client: this, operation: 'consume', options }), this, options, true, callback);
|
|
424
429
|
}
|
|
430
|
+
[kGetFetchNode](metadata, topic, partition, now) {
|
|
431
|
+
const partitionMetadata = metadata.topics.get(topic).partitions[partition];
|
|
432
|
+
const key = partitionKey(topic, partition);
|
|
433
|
+
const preferredReadReplica = this.#preferredReadReplicas.get(key);
|
|
434
|
+
if (preferredReadReplica === undefined) {
|
|
435
|
+
return partitionMetadata.leader;
|
|
436
|
+
}
|
|
437
|
+
if (now > preferredReadReplica.expiresAt) {
|
|
438
|
+
this.#preferredReadReplicas.delete(key);
|
|
439
|
+
return partitionMetadata.leader;
|
|
440
|
+
}
|
|
441
|
+
if (metadata.brokers.has(preferredReadReplica.node) &&
|
|
442
|
+
partitionMetadata.replicas.includes(preferredReadReplica.node) &&
|
|
443
|
+
!partitionMetadata.offlineReplicas.includes(preferredReadReplica.node)) {
|
|
444
|
+
return preferredReadReplica.node;
|
|
445
|
+
}
|
|
446
|
+
this.#preferredReadReplicas.delete(key);
|
|
447
|
+
this.clearMetadata();
|
|
448
|
+
return partitionMetadata.leader;
|
|
449
|
+
}
|
|
450
|
+
#topicIdsById(metadata) {
|
|
451
|
+
const topicIds = new Map();
|
|
452
|
+
for (const [topic, { id }] of metadata.topics) {
|
|
453
|
+
topicIds.set(id, topic);
|
|
454
|
+
}
|
|
455
|
+
return topicIds;
|
|
456
|
+
}
|
|
457
|
+
#fetchNodeForRequest(metadata, fallbackNode, topics, topicIds, now) {
|
|
458
|
+
let requestNode;
|
|
459
|
+
for (const topicRequest of topics) {
|
|
460
|
+
const topic = topicIds.get(topicRequest.topicId);
|
|
461
|
+
if (!topic) {
|
|
462
|
+
return fallbackNode;
|
|
463
|
+
}
|
|
464
|
+
for (const { partition } of topicRequest.partitions) {
|
|
465
|
+
const node = this[kGetFetchNode](metadata, topic, partition, now);
|
|
466
|
+
if (requestNode === undefined) {
|
|
467
|
+
requestNode = node;
|
|
468
|
+
}
|
|
469
|
+
else if (requestNode !== node) {
|
|
470
|
+
return fallbackNode;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return requestNode ?? fallbackNode;
|
|
475
|
+
}
|
|
425
476
|
#fetch(options, callback) {
|
|
426
477
|
const isolationLevel = options.isolationLevel ?? this[kOptions].isolationLevel;
|
|
427
478
|
this[kPerformWithRetry]('fetch', retryCallback => {
|
|
@@ -430,14 +481,17 @@ export class Consumer extends Base {
|
|
|
430
481
|
retryCallback(error);
|
|
431
482
|
return;
|
|
432
483
|
}
|
|
433
|
-
const
|
|
484
|
+
const topicIds = this.#topicIdsById(metadata);
|
|
485
|
+
const node = this.#fetchNodeForRequest(metadata, options.node, options.topics, topicIds, Date.now());
|
|
486
|
+
const broker = metadata.brokers.get(node);
|
|
434
487
|
if (!broker) {
|
|
435
|
-
retryCallback(new UserError(`Cannot find broker with node id ${
|
|
488
|
+
retryCallback(new UserError(`Cannot find broker with node id ${node}`));
|
|
436
489
|
return;
|
|
437
490
|
}
|
|
438
491
|
const pool = options.connectionPool ?? this[kConnections];
|
|
439
492
|
pool.get(broker, (error, connection) => {
|
|
440
493
|
if (error) {
|
|
494
|
+
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
441
495
|
// When a connection was not available (either interrupted or not available) we
|
|
442
496
|
// reset the leader epoch in the options so that when connection is re-established again we can continue
|
|
443
497
|
for (const topic of options.topics) {
|
|
@@ -454,8 +508,9 @@ export class Consumer extends Base {
|
|
|
454
508
|
retryCallback(error);
|
|
455
509
|
return;
|
|
456
510
|
}
|
|
457
|
-
api(connection, options.maxWaitTime ?? this[kOptions].maxWaitTime, options.minBytes ?? this[kOptions].minBytes, options.maxBytes ?? this[kOptions].maxBytes, isolationLevel, 0, 0, options.topics, [],
|
|
511
|
+
api(connection, options.maxWaitTime ?? this[kOptions].maxWaitTime, options.minBytes ?? this[kOptions].minBytes, options.maxBytes ?? this[kOptions].maxBytes, isolationLevel, 0, 0, options.topics, [], this.#clientRack, (error, result) => {
|
|
458
512
|
if (error) {
|
|
513
|
+
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
459
514
|
const genericError = error;
|
|
460
515
|
if (genericError.findBy?.('apiId', 'FENCED_LEADER_EPOCH')) {
|
|
461
516
|
this.clearMetadata();
|
|
@@ -467,6 +522,9 @@ export class Consumer extends Base {
|
|
|
467
522
|
}
|
|
468
523
|
}
|
|
469
524
|
}
|
|
525
|
+
else {
|
|
526
|
+
this.#updatePreferredReadReplicas(metadata, topicIds, result);
|
|
527
|
+
}
|
|
470
528
|
retryCallback(error, result);
|
|
471
529
|
});
|
|
472
530
|
});
|
|
@@ -581,7 +639,7 @@ export class Consumer extends Base {
|
|
|
581
639
|
}, concurrentCallback, 0);
|
|
582
640
|
}, (error, responses) => {
|
|
583
641
|
if (error) {
|
|
584
|
-
callback(this.#
|
|
642
|
+
callback(this.#handleError(error));
|
|
585
643
|
return;
|
|
586
644
|
}
|
|
587
645
|
let offsets = new Map();
|
|
@@ -646,14 +704,14 @@ export class Consumer extends Base {
|
|
|
646
704
|
error.findBy?.('apiId', 'STALE_MEMBER_EPOCH')) {
|
|
647
705
|
this.#consumerGroupHeartbeat(this[kOptions], heartbeatError => {
|
|
648
706
|
if (heartbeatError) {
|
|
649
|
-
callback(this.#
|
|
707
|
+
callback(this.#handleError(heartbeatError));
|
|
650
708
|
return;
|
|
651
709
|
}
|
|
652
710
|
this.#listCommittedOffsets(options, callback, staleMemberEpochRetries + 1);
|
|
653
711
|
});
|
|
654
712
|
return;
|
|
655
713
|
}
|
|
656
|
-
callback(this.#
|
|
714
|
+
callback(this.#handleError(error));
|
|
657
715
|
return;
|
|
658
716
|
}
|
|
659
717
|
const committed = new Map();
|
|
@@ -671,7 +729,7 @@ export class Consumer extends Base {
|
|
|
671
729
|
});
|
|
672
730
|
}
|
|
673
731
|
#findGroupCoordinator(callback) {
|
|
674
|
-
if (this.#coordinatorId) {
|
|
732
|
+
if (this.#coordinatorId !== null) {
|
|
675
733
|
callback(null, this.#coordinatorId);
|
|
676
734
|
return;
|
|
677
735
|
}
|
|
@@ -748,8 +806,7 @@ export class Consumer extends Base {
|
|
|
748
806
|
return;
|
|
749
807
|
}
|
|
750
808
|
const memberId = this.#getConsumerGroupHeartbeatMemberId(api.version);
|
|
751
|
-
api(connection, this.groupId, memberId, this.#memberEpoch, this.groupInstanceId, null,
|
|
752
|
-
options.rebalanceTimeout, this.topics.current, null, this.#groupRemoteAssignor, this.#assignments, groupCallback);
|
|
809
|
+
api(connection, this.groupId, memberId, this.#memberEpoch, this.groupInstanceId, this.#clientRack || null, options.rebalanceTimeout, this.topics.current, null, this.#groupRemoteAssignor, this.#assignments, groupCallback);
|
|
753
810
|
});
|
|
754
811
|
}, (error, response) => {
|
|
755
812
|
if (this[kClosed]) {
|
|
@@ -764,6 +821,7 @@ export class Consumer extends Base {
|
|
|
764
821
|
if (fenced) {
|
|
765
822
|
this.#assignments = [];
|
|
766
823
|
this.assignments = [];
|
|
824
|
+
this.#syncPreferredReadReplicas();
|
|
767
825
|
this.#memberEpoch = 0;
|
|
768
826
|
this.#consumerGroupHeartbeat(options, () => { });
|
|
769
827
|
callback(error);
|
|
@@ -878,6 +936,7 @@ export class Consumer extends Base {
|
|
|
878
936
|
}));
|
|
879
937
|
this.#assignments = newAssignments;
|
|
880
938
|
this.assignments = assignments;
|
|
939
|
+
this.#syncPreferredReadReplicas();
|
|
881
940
|
callback(null);
|
|
882
941
|
});
|
|
883
942
|
}
|
|
@@ -903,8 +962,7 @@ export class Consumer extends Base {
|
|
|
903
962
|
}
|
|
904
963
|
const memberId = this.#getConsumerGroupHeartbeatMemberId(api.version);
|
|
905
964
|
api(connection, this.groupId, memberId, -1, // memberEpoch = -1 signals leave
|
|
906
|
-
this.groupInstanceId, null, //
|
|
907
|
-
0, // rebalanceTimeout
|
|
965
|
+
this.groupInstanceId, this.#clientRack || null, 0, // rebalanceTimeout
|
|
908
966
|
[], // subscribedTopicNames
|
|
909
967
|
null, // subscribedTopicRegex
|
|
910
968
|
this.#groupRemoteAssignor, [], // topicPartitions
|
|
@@ -916,6 +974,7 @@ export class Consumer extends Base {
|
|
|
916
974
|
this.#memberEpoch = -1;
|
|
917
975
|
this.#assignments = [];
|
|
918
976
|
this.assignments = [];
|
|
977
|
+
this.#syncPreferredReadReplicas();
|
|
919
978
|
callback(null);
|
|
920
979
|
});
|
|
921
980
|
}
|
|
@@ -1057,6 +1116,7 @@ export class Consumer extends Base {
|
|
|
1057
1116
|
return;
|
|
1058
1117
|
}
|
|
1059
1118
|
this.assignments = response;
|
|
1119
|
+
this.#syncPreferredReadReplicas();
|
|
1060
1120
|
this.#cancelHeartbeat();
|
|
1061
1121
|
this.#heartbeatInterval = setTimeout(() => {
|
|
1062
1122
|
this.#heartbeat(options);
|
|
@@ -1126,6 +1186,7 @@ export class Consumer extends Base {
|
|
|
1126
1186
|
this.memberId = null;
|
|
1127
1187
|
this.generationId = 0;
|
|
1128
1188
|
this.assignments = null;
|
|
1189
|
+
this.#syncPreferredReadReplicas();
|
|
1129
1190
|
callback(null);
|
|
1130
1191
|
});
|
|
1131
1192
|
}
|
|
@@ -1150,7 +1211,7 @@ export class Consumer extends Base {
|
|
|
1150
1211
|
}
|
|
1151
1212
|
this[kMetadata]({ topics: Array.from(topicsSubscriptions.keys()) }, (error, metadata) => {
|
|
1152
1213
|
if (error) {
|
|
1153
|
-
callback(this.#
|
|
1214
|
+
callback(this.#handleError(error));
|
|
1154
1215
|
return;
|
|
1155
1216
|
}
|
|
1156
1217
|
this.#performSyncGroup(partitionsAssigner, this.#createAssignments(partitionsAssigner, metadata), callback);
|
|
@@ -1188,26 +1249,30 @@ export class Consumer extends Base {
|
|
|
1188
1249
|
}, callback);
|
|
1189
1250
|
}
|
|
1190
1251
|
#performGroupOperation(operationId, operation, callback) {
|
|
1191
|
-
this
|
|
1192
|
-
|
|
1193
|
-
callback(error);
|
|
1194
|
-
return;
|
|
1195
|
-
}
|
|
1196
|
-
this[kMetadata]({ topics: this.topics.current }, (error, metadata) => {
|
|
1252
|
+
this[kPerformWithRetry](operationId, retryCallback => {
|
|
1253
|
+
this.#findGroupCoordinator((error, coordinatorId) => {
|
|
1197
1254
|
if (error) {
|
|
1198
|
-
|
|
1255
|
+
retryCallback(error);
|
|
1199
1256
|
return;
|
|
1200
1257
|
}
|
|
1201
|
-
this[
|
|
1258
|
+
this[kMetadata]({ topics: this.topics.current }, (error, metadata) => {
|
|
1259
|
+
if (error) {
|
|
1260
|
+
retryCallback(this.#handleError(error));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1202
1263
|
this[kGetConnection](metadata.brokers.get(coordinatorId), (error, connection) => {
|
|
1203
1264
|
if (error) {
|
|
1204
|
-
retryCallback(error);
|
|
1265
|
+
retryCallback(this.#handleError(error));
|
|
1205
1266
|
return;
|
|
1206
1267
|
}
|
|
1207
|
-
operation(connection,
|
|
1268
|
+
operation(connection, (error, result) => {
|
|
1269
|
+
retryCallback(this.#handleError(error), result);
|
|
1270
|
+
});
|
|
1208
1271
|
});
|
|
1209
|
-
}
|
|
1272
|
+
});
|
|
1210
1273
|
});
|
|
1274
|
+
}, (error, result) => {
|
|
1275
|
+
callback(this.#handleError(error), result);
|
|
1211
1276
|
});
|
|
1212
1277
|
}
|
|
1213
1278
|
#validateGroupOptions(options, validator) {
|
|
@@ -1345,9 +1410,84 @@ export class Consumer extends Base {
|
|
|
1345
1410
|
}
|
|
1346
1411
|
return protocolError;
|
|
1347
1412
|
}
|
|
1348
|
-
#
|
|
1349
|
-
|
|
1350
|
-
|
|
1413
|
+
#updatePreferredReadReplicas(metadata, topicIds, response) {
|
|
1414
|
+
for (const topicResponse of response.responses) {
|
|
1415
|
+
const topic = topicIds.get(topicResponse.topicId);
|
|
1416
|
+
if (!topic) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
for (const { partitionIndex: partition, preferredReadReplica } of topicResponse.partitions) {
|
|
1420
|
+
if (preferredReadReplica < 0) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
const partitionMetadata = metadata.topics.get(topic).partitions[partition];
|
|
1424
|
+
const key = partitionKey(topic, partition);
|
|
1425
|
+
if (metadata.brokers.has(preferredReadReplica) &&
|
|
1426
|
+
partitionMetadata.replicas.includes(preferredReadReplica) &&
|
|
1427
|
+
!partitionMetadata.offlineReplicas.includes(preferredReadReplica)) {
|
|
1428
|
+
const cachedPreferredReadReplica = this.#preferredReadReplicas.get(key);
|
|
1429
|
+
if (cachedPreferredReadReplica?.node !== preferredReadReplica) {
|
|
1430
|
+
this.#preferredReadReplicas.set(key, {
|
|
1431
|
+
node: preferredReadReplica,
|
|
1432
|
+
expiresAt: Date.now() + this[kOptions].metadataMaxAge
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
this.#preferredReadReplicas.delete(key);
|
|
1438
|
+
this.clearMetadata();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
#clearPreferredReadReplicas(topics, topicIds) {
|
|
1444
|
+
let cleared = false;
|
|
1445
|
+
for (const topicRequest of topics) {
|
|
1446
|
+
const topic = topicIds.get(topicRequest.topicId);
|
|
1447
|
+
if (!topic) {
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
for (const { partition } of topicRequest.partitions) {
|
|
1451
|
+
cleared = this.#preferredReadReplicas.delete(partitionKey(topic, partition)) || cleared;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return cleared;
|
|
1455
|
+
}
|
|
1456
|
+
// Drops cached preferred replicas for partitions that are no longer assigned.
|
|
1457
|
+
// Called whenever `this.assignments` changes so the cache does not retain
|
|
1458
|
+
// entries for partitions the consumer has lost during a rebalance.
|
|
1459
|
+
#syncPreferredReadReplicas() {
|
|
1460
|
+
if (this.#preferredReadReplicas.size === 0) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (!this.assignments?.length) {
|
|
1464
|
+
this.#preferredReadReplicas.clear();
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const assignedKeys = new Set();
|
|
1468
|
+
for (const { topic, partitions } of this.assignments) {
|
|
1469
|
+
for (const partition of partitions) {
|
|
1470
|
+
assignedKeys.add(partitionKey(topic, partition));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
for (const key of this.#preferredReadReplicas.keys()) {
|
|
1474
|
+
if (!assignedKeys.has(key)) {
|
|
1475
|
+
this.#preferredReadReplicas.delete(key);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
#handleError(error) {
|
|
1480
|
+
const kafkaError = error;
|
|
1481
|
+
if (kafkaError) {
|
|
1482
|
+
if (kafkaError.findBy('hasStaleMetadata', true)) {
|
|
1483
|
+
this.clearMetadata();
|
|
1484
|
+
}
|
|
1485
|
+
else if (kafkaError.findBy('code', 'PLT_KFK_NETWORK') ||
|
|
1486
|
+
kafkaError.findBy('apiId', 'COORDINATOR_LOAD_IN_PROGRESS') ||
|
|
1487
|
+
kafkaError.findBy('apiId', 'COORDINATOR_NOT_AVAILABLE') ||
|
|
1488
|
+
kafkaError.findBy('apiId', 'NOT_COORDINATOR')) {
|
|
1489
|
+
this.#coordinatorId = null;
|
|
1490
|
+
}
|
|
1351
1491
|
}
|
|
1352
1492
|
return error;
|
|
1353
1493
|
}
|
|
@@ -5,11 +5,12 @@ import { consumerReceivesChannel, createDiagnosticContext, notifyCreation } from
|
|
|
5
5
|
import { protocolErrors, UserError } from "../../errors.js";
|
|
6
6
|
import { IS_CONTROL } from "../../protocol/records.js";
|
|
7
7
|
import { runAsyncSeries } from "../../registries/abstract.js";
|
|
8
|
-
import { kAutocommit, kInstance, kRefreshOffsetsAndFetch } from "../../symbols.js";
|
|
8
|
+
import { kAutocommit, kGetFetchNode, kInstance, kRefreshOffsetsAndFetch } from "../../symbols.js";
|
|
9
9
|
import { kConnections, kCreateConnectionPool, kInspect, kPrometheus } from "../base/base.js";
|
|
10
10
|
import { ensureMetric } from "../metrics.js";
|
|
11
11
|
import { defaultConsumerOptions } from "./options.js";
|
|
12
12
|
import { MessagesStreamFallbackModes, MessagesStreamModes } from "./types.js";
|
|
13
|
+
import { partitionKey } from "./utils.js";
|
|
13
14
|
// Don't move this function as being in the same file will enable V8 to remove.
|
|
14
15
|
// For futher info, ask Matteo.
|
|
15
16
|
/* c8 ignore next 3 - Fallback deserializer, nothing to really test */
|
|
@@ -145,7 +146,7 @@ export class MessagesStream extends Readable {
|
|
|
145
146
|
this.#offsetsToFetch = new Map();
|
|
146
147
|
if (offsets) {
|
|
147
148
|
for (const { topic, partition, offset } of offsets) {
|
|
148
|
-
this.#offsetsToFetch.set(
|
|
149
|
+
this.#offsetsToFetch.set(partitionKey(topic, partition), offset);
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
// Clone the rest of the options so the user can never mutate them
|
|
@@ -364,21 +365,22 @@ export class MessagesStream extends Readable {
|
|
|
364
365
|
}
|
|
365
366
|
const partitions = assignment.partitions;
|
|
366
367
|
for (const partition of partitions) {
|
|
367
|
-
const
|
|
368
|
-
|
|
368
|
+
const targetNode = this.#consumer[kGetFetchNode](metadata, topic, partition, now);
|
|
369
|
+
const key = partitionKey(topic, partition);
|
|
370
|
+
if (this.#inflightNodes.has(targetNode)) {
|
|
369
371
|
continue;
|
|
370
372
|
}
|
|
371
|
-
let
|
|
372
|
-
if (!
|
|
373
|
-
|
|
374
|
-
requests.set(
|
|
373
|
+
let nodeRequests = requests.get(targetNode);
|
|
374
|
+
if (!nodeRequests) {
|
|
375
|
+
nodeRequests = [];
|
|
376
|
+
requests.set(targetNode, nodeRequests);
|
|
375
377
|
}
|
|
376
378
|
const topicId = metadata.topics.get(topic).id;
|
|
377
379
|
topicIds.set(topicId, topic);
|
|
378
|
-
const fetchOffset = this.#offsetsToFetch.get(
|
|
379
|
-
requestedOffsets.set(
|
|
380
|
-
const leaderEpoch = this.#partitionsEpochs.get(
|
|
381
|
-
|
|
380
|
+
const fetchOffset = this.#offsetsToFetch.get(key);
|
|
381
|
+
requestedOffsets.set(key, fetchOffset);
|
|
382
|
+
const leaderEpoch = this.#partitionsEpochs.get(key) ?? -1;
|
|
383
|
+
nodeRequests.push({
|
|
382
384
|
topicId,
|
|
383
385
|
partitions: [
|
|
384
386
|
{
|
|
@@ -395,10 +397,10 @@ export class MessagesStream extends Readable {
|
|
|
395
397
|
if (requests.size === 0) {
|
|
396
398
|
return;
|
|
397
399
|
}
|
|
398
|
-
for (const [
|
|
399
|
-
this.#inflightNodes.set(
|
|
400
|
-
this.#consumer.fetch({ ...this.#options, node
|
|
401
|
-
this.#inflightNodes.delete(
|
|
400
|
+
for (const [node, nodeRequests] of requests) {
|
|
401
|
+
this.#inflightNodes.set(node, Date.now());
|
|
402
|
+
this.#consumer.fetch({ ...this.#options, node, topics: nodeRequests, connectionPool: this[kConnections] }, (error, response) => {
|
|
403
|
+
this.#inflightNodes.delete(node);
|
|
402
404
|
this.emit('fetch');
|
|
403
405
|
if (error) {
|
|
404
406
|
// The stream has been closed, ignore the error
|
|
@@ -407,10 +409,22 @@ export class MessagesStream extends Readable {
|
|
|
407
409
|
this.push(null);
|
|
408
410
|
return;
|
|
409
411
|
}
|
|
410
|
-
if (this.#fallbackMode !== MessagesStreamFallbackModes.FAIL
|
|
411
|
-
this.#handleOffsetOutOfRange(error, topicIds)
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
if (this.#fallbackMode !== MessagesStreamFallbackModes.FAIL) {
|
|
413
|
+
this.#handleOffsetOutOfRange(error, topicIds, (recoveryError, recovered) => {
|
|
414
|
+
if (this.#closed || this.closed || this.destroyed) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (recoveryError) {
|
|
418
|
+
this.destroy(recoveryError);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (recovered) {
|
|
422
|
+
process.nextTick(() => {
|
|
423
|
+
this.#fetch();
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
this.destroy(error);
|
|
414
428
|
});
|
|
415
429
|
return;
|
|
416
430
|
}
|
|
@@ -430,34 +444,81 @@ export class MessagesStream extends Readable {
|
|
|
430
444
|
}
|
|
431
445
|
});
|
|
432
446
|
}
|
|
433
|
-
#handleOffsetOutOfRange(error, topicIds) {
|
|
447
|
+
#handleOffsetOutOfRange(error, topicIds, callback) {
|
|
434
448
|
if (!error.findBy?.('apiId', 'OFFSET_OUT_OF_RANGE')) {
|
|
435
|
-
|
|
449
|
+
callback(null, false);
|
|
450
|
+
return;
|
|
436
451
|
}
|
|
437
452
|
const response = error.response;
|
|
438
453
|
if (!response || response.errorCode !== 0) {
|
|
439
|
-
|
|
454
|
+
callback(null, false);
|
|
455
|
+
return;
|
|
440
456
|
}
|
|
441
457
|
const recoveredOffsets = [];
|
|
458
|
+
const partitionsToRefresh = new Map();
|
|
442
459
|
for (const topicResponse of response.responses) {
|
|
443
460
|
const topic = topicIds.get(topicResponse.topicId);
|
|
444
461
|
if (!topic) {
|
|
445
|
-
|
|
462
|
+
callback(null, false);
|
|
463
|
+
return;
|
|
446
464
|
}
|
|
447
465
|
for (const partitionResponse of topicResponse.partitions) {
|
|
448
466
|
if (partitionResponse.errorCode === 0) {
|
|
449
467
|
continue;
|
|
450
468
|
}
|
|
451
469
|
if (partitionResponse.errorCode !== protocolErrors.OFFSET_OUT_OF_RANGE.code) {
|
|
452
|
-
|
|
470
|
+
callback(null, false);
|
|
471
|
+
return;
|
|
453
472
|
}
|
|
454
|
-
const key = `${topic}:${partitionResponse.partitionIndex}`;
|
|
455
473
|
const offset = this.#fallbackMode === MessagesStreamFallbackModes.EARLIEST
|
|
456
474
|
? partitionResponse.logStartOffset
|
|
457
475
|
: partitionResponse.highWatermark;
|
|
458
|
-
|
|
476
|
+
if (offset >= 0n) {
|
|
477
|
+
recoveredOffsets.push([partitionKey(topic, partitionResponse.partitionIndex), offset]);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
let partitions = partitionsToRefresh.get(topic);
|
|
481
|
+
if (!partitions) {
|
|
482
|
+
partitions = [];
|
|
483
|
+
partitionsToRefresh.set(topic, partitions);
|
|
484
|
+
}
|
|
485
|
+
partitions.push(partitionResponse.partitionIndex);
|
|
459
486
|
}
|
|
460
487
|
}
|
|
488
|
+
if (partitionsToRefresh.size > 0) {
|
|
489
|
+
this.#refreshOutOfRangeOffsets(partitionsToRefresh, recoveredOffsets, callback);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
callback(null, this.#applyRecoveredOffsets(recoveredOffsets));
|
|
493
|
+
}
|
|
494
|
+
#refreshOutOfRangeOffsets(partitionsToRefresh, recoveredOffsets, callback) {
|
|
495
|
+
const partitions = Object.fromEntries(partitionsToRefresh);
|
|
496
|
+
this.#consumer.listOffsets({
|
|
497
|
+
topics: Array.from(partitionsToRefresh.keys()),
|
|
498
|
+
partitions,
|
|
499
|
+
timestamp: this.#fallbackMode === MessagesStreamFallbackModes.EARLIEST
|
|
500
|
+
? ListOffsetTimestamps.EARLIEST
|
|
501
|
+
: ListOffsetTimestamps.LATEST
|
|
502
|
+
}, (error, offsets) => {
|
|
503
|
+
if (error) {
|
|
504
|
+
callback(error);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
for (const [topic, refreshedPartitions] of partitionsToRefresh) {
|
|
508
|
+
const topicOffsets = offsets.get(topic);
|
|
509
|
+
for (const partition of refreshedPartitions) {
|
|
510
|
+
const offset = topicOffsets?.[partition];
|
|
511
|
+
if (typeof offset !== 'bigint' || offset < 0n) {
|
|
512
|
+
callback(new UserError(`Cannot recover offset out of range for topic ${topic} partition ${partition}.`));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
recoveredOffsets.push([partitionKey(topic, partition), offset]);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
callback(null, this.#applyRecoveredOffsets(recoveredOffsets));
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
#applyRecoveredOffsets(recoveredOffsets) {
|
|
461
522
|
for (const [key, offset] of recoveredOffsets) {
|
|
462
523
|
this.#offsetsToFetch.set(key, offset);
|
|
463
524
|
this.#offsetsCommitted.set(key, offset);
|
|
@@ -487,6 +548,7 @@ export class MessagesStream extends Readable {
|
|
|
487
548
|
for (const topicResponse of response.responses) {
|
|
488
549
|
const topic = topicIds.get(topicResponse.topicId);
|
|
489
550
|
for (const { records: recordsBatches, partitionIndex: partition } of topicResponse.partitions) {
|
|
551
|
+
const key = partitionKey(topic, partition);
|
|
490
552
|
if (!recordsBatches) {
|
|
491
553
|
continue;
|
|
492
554
|
}
|
|
@@ -494,15 +556,15 @@ export class MessagesStream extends Readable {
|
|
|
494
556
|
const firstTimestamp = batch.firstTimestamp;
|
|
495
557
|
const firstOffset = batch.firstOffset;
|
|
496
558
|
const leaderEpoch = metadata.topics.get(topic).partitions[partition].leaderEpoch;
|
|
497
|
-
this.#partitionsEpochs.set(
|
|
559
|
+
this.#partitionsEpochs.set(key, leaderEpoch);
|
|
498
560
|
// Track offsets
|
|
499
561
|
if (batch === recordsBatches[recordsBatches.length - 1]) {
|
|
500
562
|
// Track the last read offset
|
|
501
563
|
const lastOffset = batch.firstOffset + BigInt(batch.lastOffsetDelta);
|
|
502
|
-
this.#offsetsToFetch.set(
|
|
564
|
+
this.#offsetsToFetch.set(key, lastOffset + 1n);
|
|
503
565
|
// Autocommit if needed
|
|
504
566
|
if (autocommit) {
|
|
505
|
-
this.#offsetsToCommit.set(
|
|
567
|
+
this.#offsetsToCommit.set(key, {
|
|
506
568
|
topic,
|
|
507
569
|
partition,
|
|
508
570
|
offset: lastOffset + 1n,
|
|
@@ -518,7 +580,7 @@ export class MessagesStream extends Readable {
|
|
|
518
580
|
for (const record of batch.records) {
|
|
519
581
|
const messageToConsume = { ...record, topic, partition };
|
|
520
582
|
const offset = batch.firstOffset + BigInt(record.offsetDelta);
|
|
521
|
-
if (offset < requestedOffsets.get(
|
|
583
|
+
if (offset < requestedOffsets.get(key)) {
|
|
522
584
|
// Thi is a duplicate message, ignore it
|
|
523
585
|
continue;
|
|
524
586
|
}
|
|
@@ -605,7 +667,7 @@ export class MessagesStream extends Readable {
|
|
|
605
667
|
}
|
|
606
668
|
}
|
|
607
669
|
#updateCommittedOffset(topic, partition, offset) {
|
|
608
|
-
const key =
|
|
670
|
+
const key = partitionKey(topic, partition);
|
|
609
671
|
const previous = this.#offsetsCommitted.get(key);
|
|
610
672
|
if (typeof previous === 'undefined' || previous < offset) {
|
|
611
673
|
this.#offsetsCommitted.set(key, offset);
|
|
@@ -767,8 +829,9 @@ export class MessagesStream extends Readable {
|
|
|
767
829
|
}
|
|
768
830
|
const partitions = assignment.partitions;
|
|
769
831
|
for (const partition of partitions) {
|
|
770
|
-
const
|
|
771
|
-
this.#
|
|
832
|
+
const key = partitionKey(topic, partition);
|
|
833
|
+
const committed = this.#offsetsToFetch.get(key);
|
|
834
|
+
this.#offsetsCommitted.set(key, committed);
|
|
772
835
|
}
|
|
773
836
|
}
|
|
774
837
|
this.emit('offsets');
|
|
@@ -27,6 +27,10 @@ export type OffsetsWithTimestamps = Map<string, Map<number, {
|
|
|
27
27
|
offset: bigint;
|
|
28
28
|
timestamp: bigint;
|
|
29
29
|
}>>;
|
|
30
|
+
export interface PreferredReadReplica {
|
|
31
|
+
node: number;
|
|
32
|
+
expiresAt: number;
|
|
33
|
+
}
|
|
30
34
|
export type CorruptedMessageHandler = (record: KafkaRecord, topic: string, partition: number, firstTimestamp: bigint, firstOffset: bigint, commit: Message['commit']) => boolean;
|
|
31
35
|
export type GroupPartitionsAssigner = (current: string, members: Map<string, ExtendedGroupProtocolSubscription>, topics: Set<string>, metadata: ClusterMetadata) => GroupPartitionsAssignments[];
|
|
32
36
|
export declare const MessagesStreamModes: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function partitionKey(topic: string, partition: number): string;
|
|
@@ -13,7 +13,7 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
|
|
|
13
13
|
get producerId(): bigint | undefined;
|
|
14
14
|
get producerEpoch(): number | undefined;
|
|
15
15
|
get transaction(): Transaction<Key, Value, HeaderKey, HeaderValue> | undefined;
|
|
16
|
-
get coordinatorId(): number;
|
|
16
|
+
get coordinatorId(): number | undefined;
|
|
17
17
|
get streamsCount(): number;
|
|
18
18
|
close(force: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
|
|
19
19
|
close(force?: boolean): Promise<void>;
|
|
@@ -383,12 +383,12 @@ export class Producer extends Base {
|
|
|
383
383
|
this[kPerformWithRetry]('addPartitionsToTransaction', retryCallback => {
|
|
384
384
|
this.#getCoordinatorConnection((error, connection) => {
|
|
385
385
|
if (error) {
|
|
386
|
-
retryCallback(error);
|
|
386
|
+
retryCallback(this.#handleError(error));
|
|
387
387
|
return;
|
|
388
388
|
}
|
|
389
389
|
this[kGetApi]('AddPartitionsToTxn', (error, api) => {
|
|
390
390
|
if (error) {
|
|
391
|
-
retryCallback(error);
|
|
391
|
+
retryCallback(this.#handleError(error));
|
|
392
392
|
return;
|
|
393
393
|
}
|
|
394
394
|
api(connection, [
|
|
@@ -401,7 +401,7 @@ export class Producer extends Base {
|
|
|
401
401
|
}
|
|
402
402
|
], error => {
|
|
403
403
|
if (error) {
|
|
404
|
-
retryCallback(error);
|
|
404
|
+
retryCallback(this.#handleError(error));
|
|
405
405
|
return;
|
|
406
406
|
}
|
|
407
407
|
retryCallback(null);
|
|
@@ -420,17 +420,17 @@ export class Producer extends Base {
|
|
|
420
420
|
this[kPerformWithRetry]('addOffsetsToTransaction', retryCallback => {
|
|
421
421
|
this.#getCoordinatorConnection((error, connection) => {
|
|
422
422
|
if (error) {
|
|
423
|
-
retryCallback(error);
|
|
423
|
+
retryCallback(this.#handleError(error));
|
|
424
424
|
return;
|
|
425
425
|
}
|
|
426
426
|
this[kGetApi]('AddOffsetsToTxn', (error, api) => {
|
|
427
427
|
if (error) {
|
|
428
|
-
retryCallback(error);
|
|
428
|
+
retryCallback(this.#handleError(error));
|
|
429
429
|
return;
|
|
430
430
|
}
|
|
431
431
|
api(connection, this.#transaction.id, this.#producerInfo.producerId, this.#producerInfo.producerEpoch, groupId, error => {
|
|
432
432
|
if (error) {
|
|
433
|
-
retryCallback(error);
|
|
433
|
+
retryCallback(this.#handleError(error));
|
|
434
434
|
return;
|
|
435
435
|
}
|
|
436
436
|
retryCallback(null);
|
|
@@ -498,18 +498,18 @@ export class Producer extends Base {
|
|
|
498
498
|
this[kPerformWithRetry]('endTransaction', retryCallback => {
|
|
499
499
|
this.#getCoordinatorConnection((error, connection) => {
|
|
500
500
|
if (error) {
|
|
501
|
-
retryCallback(error);
|
|
501
|
+
retryCallback(this.#handleError(error));
|
|
502
502
|
return;
|
|
503
503
|
}
|
|
504
504
|
this[kGetApi]('EndTxn', (error, api) => {
|
|
505
505
|
if (error) {
|
|
506
|
-
retryCallback(error);
|
|
506
|
+
retryCallback(this.#handleError(error));
|
|
507
507
|
return;
|
|
508
508
|
}
|
|
509
509
|
api(connection, this.#transaction.id, this.#producerInfo.producerId, this.#producerInfo.producerEpoch, commit, error => {
|
|
510
510
|
if (error) {
|
|
511
511
|
this.#handleFencingError(error);
|
|
512
|
-
retryCallback(error);
|
|
512
|
+
retryCallback(this.#handleError(error));
|
|
513
513
|
return;
|
|
514
514
|
}
|
|
515
515
|
this.#transaction = undefined;
|
|
@@ -545,13 +545,15 @@ export class Producer extends Base {
|
|
|
545
545
|
retryCallback(error);
|
|
546
546
|
return;
|
|
547
547
|
}
|
|
548
|
-
api(connection, transactionalId, this[kOptions].timeout, options.producerId ?? this[kOptions].producerId ?? 0n, options.producerEpoch ?? this[kOptions].producerEpoch ?? 0,
|
|
548
|
+
api(connection, transactionalId, this[kOptions].timeout, options.producerId ?? this[kOptions].producerId ?? 0n, options.producerEpoch ?? this[kOptions].producerEpoch ?? 0, (error, response) => {
|
|
549
|
+
retryCallback(this.#handleError(error), response);
|
|
550
|
+
});
|
|
549
551
|
});
|
|
550
552
|
});
|
|
551
553
|
}, (error, response) => {
|
|
552
554
|
if (error) {
|
|
553
555
|
this.#handleFencingError(error);
|
|
554
|
-
deduplicateCallback(error);
|
|
556
|
+
deduplicateCallback(this.#handleError(error));
|
|
555
557
|
return;
|
|
556
558
|
}
|
|
557
559
|
this.#producerInfo = {
|
|
@@ -777,21 +779,29 @@ export class Producer extends Base {
|
|
|
777
779
|
});
|
|
778
780
|
}
|
|
779
781
|
#getCoordinatorConnection(callback) {
|
|
782
|
+
if (this.#coordinatorId === undefined) {
|
|
783
|
+
this[kTransactionFindCoordinator](error => {
|
|
784
|
+
if (error) {
|
|
785
|
+
callback(error);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
this.#getCoordinatorConnection(callback);
|
|
789
|
+
});
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
780
792
|
// Get a connection to the coordinator
|
|
781
793
|
this[kMetadata]({}, (error, metadata) => {
|
|
782
794
|
if (error) {
|
|
783
795
|
callback(error);
|
|
784
796
|
return;
|
|
785
797
|
}
|
|
786
|
-
this[
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
});
|
|
794
|
-
}, callback);
|
|
798
|
+
this[kGetConnection](metadata.brokers.get(this.#coordinatorId), (error, connection) => {
|
|
799
|
+
if (error) {
|
|
800
|
+
callback(this.#handleError(error));
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
callback(null, connection);
|
|
804
|
+
});
|
|
795
805
|
});
|
|
796
806
|
}
|
|
797
807
|
#handleFencingError(error) {
|
|
@@ -800,6 +810,18 @@ export class Producer extends Base {
|
|
|
800
810
|
this.#transaction = undefined;
|
|
801
811
|
}
|
|
802
812
|
}
|
|
813
|
+
#handleError(error) {
|
|
814
|
+
const kafkaError = error;
|
|
815
|
+
if (kafkaError) {
|
|
816
|
+
if (kafkaError.findBy('code', 'PLT_KFK_NETWORK') ||
|
|
817
|
+
kafkaError.findBy('apiId', 'COORDINATOR_LOAD_IN_PROGRESS') ||
|
|
818
|
+
kafkaError.findBy('apiId', 'COORDINATOR_NOT_AVAILABLE') ||
|
|
819
|
+
kafkaError.findBy('apiId', 'NOT_COORDINATOR')) {
|
|
820
|
+
this.#coordinatorId = undefined;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return error;
|
|
824
|
+
}
|
|
803
825
|
#beforeSerialization(hook, options, callback) {
|
|
804
826
|
// Create the pre-serialization requests
|
|
805
827
|
const requests = [];
|
|
@@ -55,7 +55,12 @@ export class Connection extends TypedEventEmitter {
|
|
|
55
55
|
super();
|
|
56
56
|
this.setMaxListeners(0);
|
|
57
57
|
this.#instanceId = currentInstance++;
|
|
58
|
-
this.#options = Object.assign({}, defaultOptions
|
|
58
|
+
this.#options = Object.assign({}, defaultOptions);
|
|
59
|
+
for (const [key, value] of Object.entries(options)) {
|
|
60
|
+
if (value !== undefined) {
|
|
61
|
+
this.#options[key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
59
64
|
this.#options.tls ??= this.#options.ssl;
|
|
60
65
|
this.#status = ConnectionStatuses.NONE;
|
|
61
66
|
this.#clientId = clientId;
|
|
@@ -124,7 +129,7 @@ export class Connection extends TypedEventEmitter {
|
|
|
124
129
|
}
|
|
125
130
|
/* c8 ignore next 13 - Hard to test */
|
|
126
131
|
const connectingSocketTimeoutHandler = () => {
|
|
127
|
-
const error = new TimeoutError(`Connection to ${host}:${port} timed out
|
|
132
|
+
const error = new TimeoutError(`Connection to ${host}:${port} timed out.`, { canRetry: true });
|
|
128
133
|
diagnosticContext.error = error;
|
|
129
134
|
this.#socket.destroy();
|
|
130
135
|
this.#status = ConnectionStatuses.ERROR;
|
|
@@ -214,7 +219,7 @@ export class Connection extends TypedEventEmitter {
|
|
|
214
219
|
this.#socket?.destroy();
|
|
215
220
|
callback(new TimeoutError(this.#host
|
|
216
221
|
? `Connection to ${this.#host}:${this.#port} timed out.`
|
|
217
|
-
: `Connection ready timed out after ${this.#options.connectTimeout}ms
|
|
222
|
+
: `Connection ready timed out after ${this.#options.connectTimeout}ms.`, { canRetry: true }));
|
|
218
223
|
}, this.#options.connectTimeout);
|
|
219
224
|
this.once('connect', onConnect);
|
|
220
225
|
this.once('error', onError);
|
|
@@ -253,7 +258,9 @@ export class Connection extends TypedEventEmitter {
|
|
|
253
258
|
return callback[kCallbackPromise];
|
|
254
259
|
}
|
|
255
260
|
send(apiKey, apiVersion, createPayload, responseParser, hasRequestHeaderTaggedFields, hasResponseHeaderTaggedFields, callback) {
|
|
256
|
-
|
|
261
|
+
// Correlation ID is a 32-bit integer in the protocol, so we need to wrap around after 2^31 - 1
|
|
262
|
+
const correlationId = (this.#correlationId + 1) & 0x7FFFFFFF;
|
|
263
|
+
this.#correlationId = correlationId;
|
|
257
264
|
const diagnostic = createDiagnosticContext({
|
|
258
265
|
connection: this,
|
|
259
266
|
operation: 'send',
|
|
@@ -60,8 +60,9 @@ export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
|
|
|
60
60
|
if (!response.ok) {
|
|
61
61
|
throw new UserError(`Failed to fetch a schema: [HTTP ${response.status}]`, { response: await response.text() });
|
|
62
62
|
}
|
|
63
|
-
const responseBody = await response.json();
|
|
64
|
-
const { schema
|
|
63
|
+
const responseBody = (await response.json());
|
|
64
|
+
const { schema } = responseBody;
|
|
65
|
+
const schemaType = responseBody.schemaType ?? 'AVRO';
|
|
65
66
|
switch (schemaType) {
|
|
66
67
|
case 'AVRO':
|
|
67
68
|
this.#schemas.set(id, { id, type: 'avro', schema: avro.Type.forSchema(JSON.parse(schema)) });
|
package/dist/symbols.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const kInstance: unique symbol;
|
|
2
2
|
export declare const kRefreshOffsetsAndFetch: unique symbol;
|
|
3
3
|
export declare const kAutocommit: unique symbol;
|
|
4
|
+
export declare const kGetFetchNode: unique symbol;
|
|
4
5
|
export declare const kTransaction: unique symbol;
|
|
5
6
|
export declare const kTransactionPrepare: unique symbol;
|
|
6
7
|
export declare const kTransactionAddPartitions: unique symbol;
|
package/dist/symbols.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export const kInstance = Symbol('plt.kafka.base.instance');
|
|
3
3
|
export const kRefreshOffsetsAndFetch = Symbol('plt.kafka.messagesStream.refreshOffsetsAndFetch');
|
|
4
4
|
export const kAutocommit = Symbol('plt.kafka.messagesStream.autocommit');
|
|
5
|
+
export const kGetFetchNode = Symbol('plt.kafka.consumer.getFetchNode');
|
|
5
6
|
export const kTransaction = Symbol('plt.kafka.producer.transactions');
|
|
6
7
|
export const kTransactionPrepare = Symbol('plt.kafka.producer.transactions.prepare');
|
|
7
8
|
export const kTransactionAddPartitions = Symbol('plt.kafka.producer.transactions.addPartitions');
|
|
@@ -84,11 +84,11 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
|
|
|
84
84
|
[kMetadata](options: MetadataOptions, callback: CallbackWithPromise<ClusterMetadata>): void;
|
|
85
85
|
[kCheckNotClosed](callback: CallbackWithPromise<any>): boolean;
|
|
86
86
|
clearMetadata(): void;
|
|
87
|
-
[kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType
|
|
87
|
+
[kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>, attempt: number) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
|
|
88
88
|
[kPerformDeduplicated]<ReturnType>(operationId: string, operation: (callback: CallbackWithPromise<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>): void | Promise<ReturnType>;
|
|
89
89
|
[kGetApi]<RequestArguments extends Array<unknown>, ResponseType>(name: string, callback: Callback<API<RequestArguments, ResponseType>>): void;
|
|
90
90
|
[kGetConnection](broker: Broker, callback: Callback<Connection>): void;
|
|
91
|
-
[kGetBootstrapConnection](callback: Callback<Connection
|
|
91
|
+
[kGetBootstrapConnection](callback: Callback<Connection>, attempt?: number): void;
|
|
92
92
|
[kValidateOptions](target: unknown, validator: ValidateFunction<unknown>, targetName: string, throwOnErrors?: boolean): Error | null;
|
|
93
93
|
[kInspect](...args: unknown[]): void;
|
|
94
94
|
[kFormatValidationErrors](validator: ValidateFunction<unknown>, targetName: string): string;
|
|
@@ -32,6 +32,7 @@ export interface ClusterMetadata {
|
|
|
32
32
|
export type RetryDelayGetter<Owner = object> = (client: Owner, operationId: string, attempt: number, retries: number, error: Error) => number;
|
|
33
33
|
export interface BaseOptions extends ConnectionOptions {
|
|
34
34
|
clientId: string;
|
|
35
|
+
clientRack?: string;
|
|
35
36
|
bootstrapBrokers: Broker[] | string[];
|
|
36
37
|
context?: unknown;
|
|
37
38
|
timeout?: number;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type CallbackWithPromise } from "../../apis/callbacks";
|
|
2
2
|
import { type FetchResponse } from "../../apis/consumer/fetch-v17";
|
|
3
3
|
import { type ConnectionPool } from "../../network/connection-pool";
|
|
4
|
+
import { kGetFetchNode } from "../../symbols";
|
|
4
5
|
import { Base, type BaseEvents, kCreateConnectionPool } from "../base/base";
|
|
6
|
+
import { type ClusterMetadata } from "../base/types";
|
|
5
7
|
import { MessagesStream } from "./messages-stream";
|
|
6
8
|
import { TopicsMap } from "./topics-map";
|
|
7
9
|
import { type CommitOptions, type ConsumeOptions, type ConsumerGroupJoinPayload, type ConsumerGroupLeavePayload, type ConsumerGroupRebalancePayload, type ConsumerHeartbeatErrorPayload, type ConsumerHeartbeatPayload, type ConsumerOptions, type FetchOptions, type GetLagOptions, type GroupAssignment, type GroupOptions, type ListCommitsOptions, type ListOffsetsOptions, type Offsets, type OffsetsWithTimestamps } from "./types";
|
|
@@ -56,4 +58,5 @@ export declare class Consumer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
|
|
|
56
58
|
joinGroup(options?: GroupOptions): Promise<string>;
|
|
57
59
|
leaveGroup(force?: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
|
|
58
60
|
leaveGroup(force?: boolean): Promise<void>;
|
|
61
|
+
[kGetFetchNode](metadata: ClusterMetadata, topic: string, partition: number, now: number): number;
|
|
59
62
|
}
|
|
@@ -27,6 +27,10 @@ export type OffsetsWithTimestamps = Map<string, Map<number, {
|
|
|
27
27
|
offset: bigint;
|
|
28
28
|
timestamp: bigint;
|
|
29
29
|
}>>;
|
|
30
|
+
export interface PreferredReadReplica {
|
|
31
|
+
node: number;
|
|
32
|
+
expiresAt: number;
|
|
33
|
+
}
|
|
30
34
|
export type CorruptedMessageHandler = (record: KafkaRecord, topic: string, partition: number, firstTimestamp: bigint, firstOffset: bigint, commit: Message['commit']) => boolean;
|
|
31
35
|
export type GroupPartitionsAssigner = (current: string, members: Map<string, ExtendedGroupProtocolSubscription>, topics: Set<string>, metadata: ClusterMetadata) => GroupPartitionsAssignments[];
|
|
32
36
|
export declare const MessagesStreamModes: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function partitionKey(topic: string, partition: number): string;
|
|
@@ -13,7 +13,7 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
|
|
|
13
13
|
get producerId(): bigint | undefined;
|
|
14
14
|
get producerEpoch(): number | undefined;
|
|
15
15
|
get transaction(): Transaction<Key, Value, HeaderKey, HeaderValue> | undefined;
|
|
16
|
-
get coordinatorId(): number;
|
|
16
|
+
get coordinatorId(): number | undefined;
|
|
17
17
|
get streamsCount(): number;
|
|
18
18
|
close(force: boolean | CallbackWithPromise<void>, callback?: CallbackWithPromise<void>): void;
|
|
19
19
|
close(force?: boolean): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const kInstance: unique symbol;
|
|
2
2
|
export declare const kRefreshOffsetsAndFetch: unique symbol;
|
|
3
3
|
export declare const kAutocommit: unique symbol;
|
|
4
|
+
export declare const kGetFetchNode: unique symbol;
|
|
4
5
|
export declare const kTransaction: unique symbol;
|
|
5
6
|
export declare const kTransactionPrepare: unique symbol;
|
|
6
7
|
export declare const kTransactionAddPartitions: unique symbol;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const name = "@platformatic/kafka";
|
|
2
|
-
export const version = "2.
|
|
2
|
+
export const version = "2.2.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Modern and performant client for Apache Kafka",
|
|
5
5
|
"homepage": "https://github.com/platformatic/kafka",
|
|
6
6
|
"author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",
|
|
@@ -89,12 +89,19 @@
|
|
|
89
89
|
"postbuild": "./scripts/node scripts/postbuild.ts",
|
|
90
90
|
"lint": "eslint --cache",
|
|
91
91
|
"typecheck": "tsc -p . --noEmit",
|
|
92
|
-
"format": "prettier -w benchmarks playground src test",
|
|
92
|
+
"format": "prettier -w benchmarks playground regression src test",
|
|
93
93
|
"test": "c8 -c test/config/c8-local.json ./scripts/node --test --test-reporter=cleaner-spec-reporter 'test/*.test.ts' 'test/*/*.test.ts' 'test/*/*/*.test.ts'",
|
|
94
94
|
"test:ci": "c8 -c test/config/c8-ci.json ./scripts/node --test --test-reporter=cleaner-spec-reporter 'test/*.test.ts' 'test/*/*.test.ts' 'test/*/*/*.test.ts'",
|
|
95
|
+
"test:integrity": "./scripts/node --test --test-reporter=cleaner-spec-reporter 'regression/integrity/*.test.ts'",
|
|
96
|
+
"test:performance": "./scripts/node --test --test-reporter=cleaner-spec-reporter 'regression/performance/*.test.ts'",
|
|
97
|
+
"test:regression": "npm run test:integrity",
|
|
98
|
+
"test:regression:all": "npm run test:integrity && npm run test:performance",
|
|
99
|
+
"test:all": "npm test && npm run test:regression:all",
|
|
95
100
|
"test:memory": "NODE_OPTIONS='--expose-gc' ./scripts/node --test --test-reporter=cleaner-spec-reporter 'test/**/*.memory-test.ts'",
|
|
96
101
|
"test:docker:up": "docker-compose up -d --wait",
|
|
97
102
|
"test:docker:down": "docker-compose down",
|
|
103
|
+
"regression:baseline:compare": "npm run test:performance",
|
|
104
|
+
"regression:baseline:update": "./scripts/node regression/helpers/run-benchmarks.ts",
|
|
98
105
|
"ci": "npm run build && npm run lint && npm run test:ci",
|
|
99
106
|
"generate:apis": "node scripts/generate-apis.ts",
|
|
100
107
|
"generate:errors": "node scripts/generate-errors.ts",
|