@platformatic/kafka 2.0.0 → 2.1.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/clients/base/base.js +6 -1
- 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 +169 -31
- package/dist/clients/consumer/messages-stream.js +72 -24
- 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 +6 -1
- 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/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
|
@@ -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') {
|
|
@@ -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) {
|
|
@@ -1248,9 +1313,7 @@ export class Consumer extends Base {
|
|
|
1248
1313
|
.appendArray(assignments, (w, { topic, partitions }) => {
|
|
1249
1314
|
w.appendString(topic, false).appendArray(partitions, (w, a) => w.appendInt32(a), false, false);
|
|
1250
1315
|
}, false, false);
|
|
1251
|
-
|
|
1252
|
-
writer.append(userData);
|
|
1253
|
-
}
|
|
1316
|
+
writer.appendBytes(userData ?? null, false);
|
|
1254
1317
|
return writer.buffer;
|
|
1255
1318
|
}
|
|
1256
1319
|
#decodeProtocolAssignment(buffer) {
|
|
@@ -1347,9 +1410,84 @@ export class Consumer extends Base {
|
|
|
1347
1410
|
}
|
|
1348
1411
|
return protocolError;
|
|
1349
1412
|
}
|
|
1350
|
-
#
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
+
}
|
|
1353
1491
|
}
|
|
1354
1492
|
return error;
|
|
1355
1493
|
}
|
|
@@ -2,14 +2,15 @@ import { Readable } from 'node:stream';
|
|
|
2
2
|
import { createPromisifiedCallback, kCallbackPromise, noopCallback } from "../../apis/callbacks.js";
|
|
3
3
|
import { ListOffsetTimestamps } from "../../apis/enumerations.js";
|
|
4
4
|
import { consumerReceivesChannel, createDiagnosticContext, notifyCreation } from "../../diagnostic.js";
|
|
5
|
-
import { UserError } from "../../errors.js";
|
|
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,6 +409,13 @@ export class MessagesStream extends Readable {
|
|
|
407
409
|
this.push(null);
|
|
408
410
|
return;
|
|
409
411
|
}
|
|
412
|
+
if (this.#fallbackMode !== MessagesStreamFallbackModes.FAIL &&
|
|
413
|
+
this.#handleOffsetOutOfRange(error, topicIds)) {
|
|
414
|
+
process.nextTick(() => {
|
|
415
|
+
this.#fetch();
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
410
419
|
this.destroy(error);
|
|
411
420
|
return;
|
|
412
421
|
}
|
|
@@ -423,6 +432,43 @@ export class MessagesStream extends Readable {
|
|
|
423
432
|
}
|
|
424
433
|
});
|
|
425
434
|
}
|
|
435
|
+
#handleOffsetOutOfRange(error, topicIds) {
|
|
436
|
+
if (!error.findBy?.('apiId', 'OFFSET_OUT_OF_RANGE')) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const response = error.response;
|
|
440
|
+
if (!response || response.errorCode !== 0) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
const recoveredOffsets = [];
|
|
444
|
+
for (const topicResponse of response.responses) {
|
|
445
|
+
const topic = topicIds.get(topicResponse.topicId);
|
|
446
|
+
if (!topic) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
for (const partitionResponse of topicResponse.partitions) {
|
|
450
|
+
if (partitionResponse.errorCode === 0) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (partitionResponse.errorCode !== protocolErrors.OFFSET_OUT_OF_RANGE.code) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
const key = `${topic}:${partitionResponse.partitionIndex}`;
|
|
457
|
+
const offset = this.#fallbackMode === MessagesStreamFallbackModes.EARLIEST
|
|
458
|
+
? partitionResponse.logStartOffset
|
|
459
|
+
: partitionResponse.highWatermark;
|
|
460
|
+
recoveredOffsets.push([key, offset]);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const [key, offset] of recoveredOffsets) {
|
|
464
|
+
this.#offsetsToFetch.set(key, offset);
|
|
465
|
+
this.#offsetsCommitted.set(key, offset);
|
|
466
|
+
}
|
|
467
|
+
if (recoveredOffsets.length > 0) {
|
|
468
|
+
this.emit('offsets');
|
|
469
|
+
}
|
|
470
|
+
return recoveredOffsets.length > 0;
|
|
471
|
+
}
|
|
426
472
|
#pushRecords(metadata, topicIds, response, requestedOffsets) {
|
|
427
473
|
const autocommit = this.#autocommitEnabled;
|
|
428
474
|
const keyDeserializer = this.#keyDeserializer;
|
|
@@ -443,6 +489,7 @@ export class MessagesStream extends Readable {
|
|
|
443
489
|
for (const topicResponse of response.responses) {
|
|
444
490
|
const topic = topicIds.get(topicResponse.topicId);
|
|
445
491
|
for (const { records: recordsBatches, partitionIndex: partition } of topicResponse.partitions) {
|
|
492
|
+
const key = partitionKey(topic, partition);
|
|
446
493
|
if (!recordsBatches) {
|
|
447
494
|
continue;
|
|
448
495
|
}
|
|
@@ -450,15 +497,15 @@ export class MessagesStream extends Readable {
|
|
|
450
497
|
const firstTimestamp = batch.firstTimestamp;
|
|
451
498
|
const firstOffset = batch.firstOffset;
|
|
452
499
|
const leaderEpoch = metadata.topics.get(topic).partitions[partition].leaderEpoch;
|
|
453
|
-
this.#partitionsEpochs.set(
|
|
500
|
+
this.#partitionsEpochs.set(key, leaderEpoch);
|
|
454
501
|
// Track offsets
|
|
455
502
|
if (batch === recordsBatches[recordsBatches.length - 1]) {
|
|
456
503
|
// Track the last read offset
|
|
457
504
|
const lastOffset = batch.firstOffset + BigInt(batch.lastOffsetDelta);
|
|
458
|
-
this.#offsetsToFetch.set(
|
|
505
|
+
this.#offsetsToFetch.set(key, lastOffset + 1n);
|
|
459
506
|
// Autocommit if needed
|
|
460
507
|
if (autocommit) {
|
|
461
|
-
this.#offsetsToCommit.set(
|
|
508
|
+
this.#offsetsToCommit.set(key, {
|
|
462
509
|
topic,
|
|
463
510
|
partition,
|
|
464
511
|
offset: lastOffset + 1n,
|
|
@@ -474,7 +521,7 @@ export class MessagesStream extends Readable {
|
|
|
474
521
|
for (const record of batch.records) {
|
|
475
522
|
const messageToConsume = { ...record, topic, partition };
|
|
476
523
|
const offset = batch.firstOffset + BigInt(record.offsetDelta);
|
|
477
|
-
if (offset < requestedOffsets.get(
|
|
524
|
+
if (offset < requestedOffsets.get(key)) {
|
|
478
525
|
// Thi is a duplicate message, ignore it
|
|
479
526
|
continue;
|
|
480
527
|
}
|
|
@@ -561,7 +608,7 @@ export class MessagesStream extends Readable {
|
|
|
561
608
|
}
|
|
562
609
|
}
|
|
563
610
|
#updateCommittedOffset(topic, partition, offset) {
|
|
564
|
-
const key =
|
|
611
|
+
const key = partitionKey(topic, partition);
|
|
565
612
|
const previous = this.#offsetsCommitted.get(key);
|
|
566
613
|
if (typeof previous === 'undefined' || previous < offset) {
|
|
567
614
|
this.#offsetsCommitted.set(key, offset);
|
|
@@ -723,8 +770,9 @@ export class MessagesStream extends Readable {
|
|
|
723
770
|
}
|
|
724
771
|
const partitions = assignment.partitions;
|
|
725
772
|
for (const partition of partitions) {
|
|
726
|
-
const
|
|
727
|
-
this.#
|
|
773
|
+
const key = partitionKey(topic, partition);
|
|
774
|
+
const committed = this.#offsetsToFetch.get(key);
|
|
775
|
+
this.#offsetsCommitted.set(key, committed);
|
|
728
776
|
}
|
|
729
777
|
}
|
|
730
778
|
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;
|
|
@@ -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');
|
|
@@ -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.1.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|