@platformatic/kafka 2.3.0 → 2.4.0-alpha.1
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/consumer/consumer.js +180 -55
- package/dist/clients/consumer/messages-stream.js +12 -1
- package/dist/clients/consumer/options.d.ts +15 -0
- package/dist/clients/consumer/options.js +1 -0
- package/dist/clients/consumer/types.d.ts +3 -0
- package/dist/clients/producer/producer.js +14 -8
- package/dist/network/connection-pool.js +9 -1
- package/dist/typescript-4/dist/clients/consumer/options.d.ts +15 -0
- package/dist/typescript-4/dist/clients/consumer/types.d.ts +3 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../../apis/callbacks.js";
|
|
3
3
|
import { FetchIsolationLevels, FindCoordinatorKeyTypes } from "../../apis/enumerations.js";
|
|
4
4
|
import { consumerCommitsChannel, consumerConsumesChannel, consumerFetchesChannel, consumerGroupChannel, consumerHeartbeatChannel, consumerLagChannel, consumerOffsetsChannel, createDiagnosticContext } from "../../diagnostic.js";
|
|
5
|
-
import { protocolErrors, UserError } from "../../errors.js";
|
|
5
|
+
import { NetworkError, protocolErrors, UserError } from "../../errors.js";
|
|
6
6
|
import { INT32_SIZE } from "../../protocol/definitions.js";
|
|
7
7
|
import { Reader } from "../../protocol/reader.js";
|
|
8
8
|
import { IS_CONTROL } from "../../protocol/records.js";
|
|
@@ -395,6 +395,10 @@ export class Consumer extends Base {
|
|
|
395
395
|
options.rebalanceTimeout ??= this[kOptions].rebalanceTimeout;
|
|
396
396
|
options.heartbeatInterval ??= this[kOptions].heartbeatInterval;
|
|
397
397
|
options.protocols ??= this[kOptions].protocols;
|
|
398
|
+
const protocolsMetadata = this[kOptions].protocolsMetadata;
|
|
399
|
+
if (!options.protocolsMetadata && protocolsMetadata) {
|
|
400
|
+
options.protocolsMetadata = protocolsMetadata;
|
|
401
|
+
}
|
|
398
402
|
this.#validateGroupOptions(options);
|
|
399
403
|
this.#membershipActive = true;
|
|
400
404
|
this.#joinGroup(options, callback);
|
|
@@ -512,10 +516,64 @@ export class Consumer extends Base {
|
|
|
512
516
|
retryCallback(error);
|
|
513
517
|
return;
|
|
514
518
|
}
|
|
515
|
-
|
|
519
|
+
let session = this.#fetchSessions.get(node);
|
|
520
|
+
if (session && session.pool !== pool) {
|
|
521
|
+
session = undefined;
|
|
522
|
+
}
|
|
516
523
|
const sessionId = session?.id ?? 0;
|
|
517
524
|
const sessionEpoch = session?.epoch ?? 0;
|
|
518
|
-
|
|
525
|
+
// The desired state of the session: options.topics always lists all the wanted partitions
|
|
526
|
+
const desired = new Map();
|
|
527
|
+
for (const topic of options.topics) {
|
|
528
|
+
let partitions = desired.get(topic.topicId);
|
|
529
|
+
if (!partitions) {
|
|
530
|
+
partitions = new Map();
|
|
531
|
+
desired.set(topic.topicId, partitions);
|
|
532
|
+
}
|
|
533
|
+
for (const partition of topic.partitions) {
|
|
534
|
+
// Copy the partition since the data is mutated in the error paths
|
|
535
|
+
partitions.set(partition.partition, { ...partition });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
let requestTopics = options.topics;
|
|
539
|
+
const forgottenTopicsData = [];
|
|
540
|
+
if (session) {
|
|
541
|
+
// Only send the partitions which are new or whose state changed: the broker
|
|
542
|
+
// keeps serving the unchanged ones from the session (KIP-227)
|
|
543
|
+
requestTopics = [];
|
|
544
|
+
for (const [topicId, partitions] of desired) {
|
|
545
|
+
const cached = session.partitions.get(topicId);
|
|
546
|
+
const changed = [];
|
|
547
|
+
for (const partition of partitions.values()) {
|
|
548
|
+
const previous = cached?.get(partition.partition);
|
|
549
|
+
if (!previous ||
|
|
550
|
+
previous.fetchOffset !== partition.fetchOffset ||
|
|
551
|
+
previous.currentLeaderEpoch !== partition.currentLeaderEpoch ||
|
|
552
|
+
previous.lastFetchedEpoch !== partition.lastFetchedEpoch ||
|
|
553
|
+
previous.partitionMaxBytes !== partition.partitionMaxBytes) {
|
|
554
|
+
changed.push(partition);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (changed.length > 0) {
|
|
558
|
+
requestTopics.push({ topicId, partitions: changed });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Ask the broker to remove from the session the partitions which are no
|
|
562
|
+
// longer wanted, otherwise it keeps serving them forever
|
|
563
|
+
for (const [topicId, partitions] of session.partitions) {
|
|
564
|
+
const stillDesired = desired.get(topicId);
|
|
565
|
+
const forgotten = [];
|
|
566
|
+
for (const partition of partitions.keys()) {
|
|
567
|
+
if (!stillDesired?.has(partition)) {
|
|
568
|
+
forgotten.push(partition);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (forgotten.length > 0) {
|
|
572
|
+
forgottenTopicsData.push({ topic: topicId, partitions: forgotten });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
api(connection, options.maxWaitTime ?? this[kOptions].maxWaitTime, options.minBytes ?? this[kOptions].minBytes, options.maxBytes ?? this[kOptions].maxBytes, isolationLevel, sessionId, sessionEpoch, requestTopics, forgottenTopicsData, this.#clientRack, (error, result) => {
|
|
519
577
|
if (error) {
|
|
520
578
|
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
521
579
|
const genericError = error;
|
|
@@ -537,7 +595,7 @@ export class Consumer extends Base {
|
|
|
537
595
|
}
|
|
538
596
|
else {
|
|
539
597
|
this.#updatePreferredReadReplicas(metadata, topicIds, result);
|
|
540
|
-
this.#updateFetchSession(node, sessionEpoch, result.sessionId);
|
|
598
|
+
this.#updateFetchSession(node, pool, sessionEpoch, result.sessionId, desired);
|
|
541
599
|
}
|
|
542
600
|
retryCallback(error, result);
|
|
543
601
|
});
|
|
@@ -556,12 +614,19 @@ export class Consumer extends Base {
|
|
|
556
614
|
callback(error, response);
|
|
557
615
|
}, 0);
|
|
558
616
|
}
|
|
559
|
-
#updateFetchSession(node, sentEpoch, responseSessionId) {
|
|
617
|
+
#updateFetchSession(node, pool, sentEpoch, responseSessionId, desired) {
|
|
560
618
|
if (!responseSessionId) {
|
|
561
619
|
this.#fetchSessions.delete(node);
|
|
562
620
|
return;
|
|
563
621
|
}
|
|
564
|
-
|
|
622
|
+
// After a successful fetch the broker session contains exactly the desired partitions:
|
|
623
|
+
// unchanged ones already matched it, changed ones were sent and removed ones were forgotten
|
|
624
|
+
this.#fetchSessions.set(node, {
|
|
625
|
+
id: responseSessionId,
|
|
626
|
+
epoch: nextFetchEpoch(sentEpoch),
|
|
627
|
+
pool,
|
|
628
|
+
partitions: desired
|
|
629
|
+
});
|
|
565
630
|
}
|
|
566
631
|
#commit(options, callback, rejoinAttempts = 0) {
|
|
567
632
|
this.#performGroupOperation('commit', (connection, groupCallback) => {
|
|
@@ -1085,45 +1150,20 @@ export class Consumer extends Base {
|
|
|
1085
1150
|
return;
|
|
1086
1151
|
}
|
|
1087
1152
|
this.#cancelHeartbeat();
|
|
1088
|
-
|
|
1089
|
-
for (const protocol of options.protocols) {
|
|
1090
|
-
protocols.push({
|
|
1091
|
-
name: protocol.name,
|
|
1092
|
-
metadata: this.#encodeProtocolSubscriptionMetadata(protocol, this.topics.current)
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
this.#performDeduplicateGroupOperaton('joinGroup', (connection, groupCallback) => {
|
|
1096
|
-
this[kGetApi]('JoinGroup', (error, api) => {
|
|
1097
|
-
if (error) {
|
|
1098
|
-
groupCallback(error);
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
api(connection, this.groupId, options.sessionTimeout, options.rebalanceTimeout, this.memberId ?? '', this.groupInstanceId, 'consumer', protocols, '', groupCallback);
|
|
1102
|
-
});
|
|
1103
|
-
}, (error, response) => {
|
|
1104
|
-
if (!this.#membershipActive) {
|
|
1105
|
-
callback(null);
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1153
|
+
this.#createJoinGroupProtocols(options, (error, protocols) => {
|
|
1108
1154
|
if (error) {
|
|
1109
|
-
if (this.#getRejoinError(error)) {
|
|
1110
|
-
this.#performJoinGroup(options, callback);
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
1155
|
callback(error);
|
|
1114
1156
|
return;
|
|
1115
1157
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1125
|
-
// Send a syncGroup request
|
|
1126
|
-
this.#syncGroup(options.partitionAssigner, (error, response) => {
|
|
1158
|
+
this.#performDeduplicateGroupOperaton('joinGroup', (connection, groupCallback) => {
|
|
1159
|
+
this[kGetApi]('JoinGroup', (error, api) => {
|
|
1160
|
+
if (error) {
|
|
1161
|
+
groupCallback(error);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
api(connection, this.groupId, options.sessionTimeout, options.rebalanceTimeout, this.memberId ?? '', this.groupInstanceId, 'consumer', protocols, '', groupCallback);
|
|
1165
|
+
});
|
|
1166
|
+
}, (error, response) => {
|
|
1127
1167
|
if (!this.#membershipActive) {
|
|
1128
1168
|
callback(null);
|
|
1129
1169
|
return;
|
|
@@ -1136,20 +1176,44 @@ export class Consumer extends Base {
|
|
|
1136
1176
|
callback(error);
|
|
1137
1177
|
return;
|
|
1138
1178
|
}
|
|
1139
|
-
|
|
1140
|
-
this
|
|
1141
|
-
this
|
|
1142
|
-
this.#
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1179
|
+
// This is for Azure Event Hubs compatibility, which does not respond with an error on the first join
|
|
1180
|
+
this.memberId = response.memberId;
|
|
1181
|
+
this.generationId = response.generationId;
|
|
1182
|
+
this.#isLeader = response.leader === this.memberId;
|
|
1183
|
+
this.#protocol = response.protocolName;
|
|
1184
|
+
this.#members = new Map();
|
|
1185
|
+
for (const member of response.members) {
|
|
1186
|
+
this.#members.set(member.memberId, this.#decodeProtocolSubscriptionMetadata(member.memberId, member.metadata));
|
|
1187
|
+
}
|
|
1188
|
+
// Send a syncGroup request
|
|
1189
|
+
this.#syncGroup(options.partitionAssigner, (error, response) => {
|
|
1190
|
+
if (!this.#membershipActive) {
|
|
1191
|
+
callback(null);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (error) {
|
|
1195
|
+
if (this.#getRejoinError(error)) {
|
|
1196
|
+
this.#performJoinGroup(options, callback);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
callback(error);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
this.assignments = response;
|
|
1203
|
+
this.#syncPreferredReadReplicas();
|
|
1204
|
+
this.#cancelHeartbeat();
|
|
1205
|
+
this.#heartbeatInterval = setTimeout(() => {
|
|
1206
|
+
this.#heartbeat(options);
|
|
1207
|
+
}, options.heartbeatInterval);
|
|
1208
|
+
this.emitWithDebug('consumer', 'group:join', {
|
|
1209
|
+
groupId: this.groupId,
|
|
1210
|
+
memberId: this.memberId,
|
|
1211
|
+
generationId: this.generationId,
|
|
1212
|
+
isLeader: this.#isLeader,
|
|
1213
|
+
assignments: this.assignments
|
|
1214
|
+
});
|
|
1215
|
+
callback(null, this.memberId);
|
|
1151
1216
|
});
|
|
1152
|
-
callback(null, this.memberId);
|
|
1153
1217
|
});
|
|
1154
1218
|
});
|
|
1155
1219
|
}
|
|
@@ -1230,11 +1294,17 @@ export class Consumer extends Base {
|
|
|
1230
1294
|
topicSubscriptions.push(subscription);
|
|
1231
1295
|
}
|
|
1232
1296
|
}
|
|
1233
|
-
|
|
1297
|
+
const subscribedTopics = Array.from(topicsSubscriptions.keys());
|
|
1298
|
+
this[kMetadata]({ topics: subscribedTopics }, (error, metadata) => {
|
|
1234
1299
|
if (error) {
|
|
1235
1300
|
callback(this.#handleError(error));
|
|
1236
1301
|
return;
|
|
1237
1302
|
}
|
|
1303
|
+
const missingTopic = subscribedTopics.find(topic => !metadata.topics.has(topic));
|
|
1304
|
+
if (missingTopic) {
|
|
1305
|
+
callback(this.#handleError(new NetworkError(`Cannot compute group assignments: metadata for subscribed topic "${missingTopic}" is not available in the current cluster metadata.`, { canRetry: true, hasStaleMetadata: true })));
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1238
1308
|
this.#performSyncGroup(partitionsAssigner, this.#createAssignments(partitionsAssigner, metadata), callback);
|
|
1239
1309
|
});
|
|
1240
1310
|
return;
|
|
@@ -1303,6 +1373,61 @@ export class Consumer extends Base {
|
|
|
1303
1373
|
throw new UserError(this[kFormatValidationErrors](validator, '/options'));
|
|
1304
1374
|
}
|
|
1305
1375
|
}
|
|
1376
|
+
#createJoinGroupProtocols(options, callback) {
|
|
1377
|
+
if (typeof options.protocolsMetadata !== 'function') {
|
|
1378
|
+
callback(null, options.protocols.map(protocol => ({
|
|
1379
|
+
name: protocol.name,
|
|
1380
|
+
metadata: this.#encodeProtocolSubscriptionMetadata(protocol, this.topics.current)
|
|
1381
|
+
})));
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
this[kMetadata]({ topics: this.topics.current, forceUpdate: true }, (error, metadata) => {
|
|
1385
|
+
if (error) {
|
|
1386
|
+
callback(this.#handleError(error));
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
this.#resolveProtocolsMetadata(options.protocolsMetadata, options.protocols, this.topics.current, metadata, (error, userData) => {
|
|
1390
|
+
if (error) {
|
|
1391
|
+
callback(error);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
else if (!Buffer.isBuffer(userData)) {
|
|
1395
|
+
callback(new UserError('protocolsMetadata must resolve to a Buffer.'));
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
callback(null, options.protocols.map(protocol => ({
|
|
1399
|
+
name: protocol.name,
|
|
1400
|
+
metadata: this.#encodeProtocolSubscriptionMetadata({ ...protocol, metadata: userData }, this.topics.current)
|
|
1401
|
+
})));
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
#resolveProtocolsMetadata(protocolsMetadata, protocols, topics, metadata, callback) {
|
|
1406
|
+
let settled = false;
|
|
1407
|
+
function resolveCallback(error, userData) {
|
|
1408
|
+
if (settled) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
settled = true;
|
|
1412
|
+
callback(error, userData);
|
|
1413
|
+
}
|
|
1414
|
+
let result;
|
|
1415
|
+
try {
|
|
1416
|
+
result = protocolsMetadata(protocols, topics, metadata, resolveCallback);
|
|
1417
|
+
if (typeof result !== 'undefined') {
|
|
1418
|
+
if (typeof result.then === 'function') {
|
|
1419
|
+
;
|
|
1420
|
+
result.then(userData => process.nextTick(resolveCallback, null, userData), error => process.nextTick(resolveCallback, error));
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
resolveCallback(null, result);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
catch (error) {
|
|
1428
|
+
resolveCallback(error);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1306
1431
|
/*
|
|
1307
1432
|
The following two methods follow:
|
|
1308
1433
|
https://github.com/apache/kafka/blob/trunk/clients/src/main/resources/common/message/ConsumerProtocolSubscription.json
|
|
@@ -553,6 +553,11 @@ export class MessagesStream extends Readable {
|
|
|
553
553
|
const topic = topicIds.get(topicResponse.topicId);
|
|
554
554
|
for (const { records: recordsBatches, partitionIndex: partition } of topicResponse.partitions) {
|
|
555
555
|
const key = partitionKey(topic, partition);
|
|
556
|
+
// The broker can return partitions which were not requested when they linger
|
|
557
|
+
// in an incremental fetch session - their data is stale and must not rewind offsets
|
|
558
|
+
if (!requestedOffsets.has(key)) {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
556
561
|
if (!recordsBatches) {
|
|
557
562
|
continue;
|
|
558
563
|
}
|
|
@@ -860,7 +865,13 @@ export class MessagesStream extends Readable {
|
|
|
860
865
|
const requests = [];
|
|
861
866
|
// Create the pre-deserialization requests
|
|
862
867
|
for (const topicResponse of response.responses) {
|
|
868
|
+
const topic = topicIds.get(topicResponse.topicId);
|
|
863
869
|
for (const { records: recordsBatches, partitionIndex: partition } of topicResponse.partitions) {
|
|
870
|
+
// The broker can return partitions which were not requested when they linger
|
|
871
|
+
// in an incremental fetch session - do not run hooks on their stale records
|
|
872
|
+
if (!requestedOffsets.has(partitionKey(topic, partition))) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
864
875
|
/* c8 ignore next 3 - Hard to test */
|
|
865
876
|
if (!recordsBatches) {
|
|
866
877
|
continue;
|
|
@@ -872,7 +883,7 @@ export class MessagesStream extends Readable {
|
|
|
872
883
|
continue;
|
|
873
884
|
}
|
|
874
885
|
for (const message of batch.records) {
|
|
875
|
-
message.topic =
|
|
886
|
+
message.topic = topic;
|
|
876
887
|
message.partition = partition;
|
|
877
888
|
requests.push([message.key, 'key', message]);
|
|
878
889
|
requests.push([message.value, 'value', message]);
|
|
@@ -53,6 +53,9 @@ export declare const groupOptionsProperties: {
|
|
|
53
53
|
};
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
|
+
protocolsMetadata: {
|
|
57
|
+
function: boolean;
|
|
58
|
+
};
|
|
56
59
|
partitionAssigner: {
|
|
57
60
|
function: boolean;
|
|
58
61
|
};
|
|
@@ -195,6 +198,9 @@ export declare const groupOptionsSchema: {
|
|
|
195
198
|
};
|
|
196
199
|
};
|
|
197
200
|
};
|
|
201
|
+
protocolsMetadata: {
|
|
202
|
+
function: boolean;
|
|
203
|
+
};
|
|
198
204
|
partitionAssigner: {
|
|
199
205
|
function: boolean;
|
|
200
206
|
};
|
|
@@ -315,6 +321,9 @@ export declare const consumeOptionsSchema: {
|
|
|
315
321
|
};
|
|
316
322
|
};
|
|
317
323
|
};
|
|
324
|
+
protocolsMetadata: {
|
|
325
|
+
function: boolean;
|
|
326
|
+
};
|
|
318
327
|
partitionAssigner: {
|
|
319
328
|
function: boolean;
|
|
320
329
|
};
|
|
@@ -481,6 +490,9 @@ export declare const consumerOptionsSchema: {
|
|
|
481
490
|
};
|
|
482
491
|
};
|
|
483
492
|
};
|
|
493
|
+
protocolsMetadata: {
|
|
494
|
+
function: boolean;
|
|
495
|
+
};
|
|
484
496
|
partitionAssigner: {
|
|
485
497
|
function: boolean;
|
|
486
498
|
};
|
|
@@ -608,6 +620,9 @@ export declare const fetchOptionsSchema: {
|
|
|
608
620
|
};
|
|
609
621
|
};
|
|
610
622
|
};
|
|
623
|
+
protocolsMetadata: {
|
|
624
|
+
function: boolean;
|
|
625
|
+
};
|
|
611
626
|
partitionAssigner: {
|
|
612
627
|
function: boolean;
|
|
613
628
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Callback } from '../../apis/definitions.ts';
|
|
1
2
|
import { type FetchRequestTopic } from '../../apis/consumer/fetch-v17.ts';
|
|
2
3
|
import { type GroupProtocols } from '../../apis/enumerations.ts';
|
|
3
4
|
import { type ConnectionPool } from '../../network/connection-pool.ts';
|
|
@@ -33,6 +34,7 @@ export interface PreferredReadReplica {
|
|
|
33
34
|
}
|
|
34
35
|
export type CorruptedMessageHandler = (record: KafkaRecord, topic: string, partition: number, firstTimestamp: bigint, firstOffset: bigint, commit: Message['commit']) => boolean;
|
|
35
36
|
export type GroupPartitionsAssigner = (current: string, members: Map<string, ExtendedGroupProtocolSubscription>, topics: Set<string>, metadata: ClusterMetadata) => GroupPartitionsAssignments[];
|
|
37
|
+
export type GroupProtocolsMetadataCallback = (protocols: GroupProtocolSubscription[], topics: string[], metadata: ClusterMetadata, callback: Callback<Buffer>) => Buffer | Promise<Buffer> | undefined;
|
|
36
38
|
export declare const MessagesStreamModes: {
|
|
37
39
|
readonly LATEST: "latest";
|
|
38
40
|
readonly EARLIEST: "earliest";
|
|
@@ -57,6 +59,7 @@ export interface GroupOptions {
|
|
|
57
59
|
rebalanceTimeout?: number;
|
|
58
60
|
heartbeatInterval?: number;
|
|
59
61
|
protocols?: GroupProtocolSubscription[];
|
|
62
|
+
protocolsMetadata?: GroupProtocolsMetadataCallback;
|
|
60
63
|
partitionAssigner?: GroupPartitionsAssigner;
|
|
61
64
|
assignmentUserData?: Buffer;
|
|
62
65
|
}
|
|
@@ -342,7 +342,7 @@ export class Producer extends Base {
|
|
|
342
342
|
}
|
|
343
343
|
const transactionalId = this.#transaction.id;
|
|
344
344
|
this[kPerformDeduplicated]('findCoordinator', deduplicateCallback => {
|
|
345
|
-
this[kPerformWithRetry]('findCoordinator', retryCallback => {
|
|
345
|
+
this[kPerformWithRetry]('findCoordinator', (retryCallback, attempt) => {
|
|
346
346
|
this[kGetBootstrapConnection]((error, connection) => {
|
|
347
347
|
if (error) {
|
|
348
348
|
retryCallback(error);
|
|
@@ -355,7 +355,7 @@ export class Producer extends Base {
|
|
|
355
355
|
}
|
|
356
356
|
api(connection, FindCoordinatorKeyTypes.TRANSACTION, [transactionalId], retryCallback);
|
|
357
357
|
});
|
|
358
|
-
});
|
|
358
|
+
}, attempt);
|
|
359
359
|
}, (error, response) => {
|
|
360
360
|
if (error) {
|
|
361
361
|
deduplicateCallback(error);
|
|
@@ -531,11 +531,8 @@ export class Producer extends Base {
|
|
|
531
531
|
#initIdempotentProducer(options, callback) {
|
|
532
532
|
const transactionalId = this.#transaction?.id;
|
|
533
533
|
this[kPerformDeduplicated]('initProducerId', deduplicateCallback => {
|
|
534
|
-
this[kPerformWithRetry]('initProducerId', retryCallback => {
|
|
535
|
-
const
|
|
536
|
-
? this.#getCoordinatorConnection.bind(this)
|
|
537
|
-
: this[kGetBootstrapConnection].bind(this);
|
|
538
|
-
connector((error, connection) => {
|
|
534
|
+
this[kPerformWithRetry]('initProducerId', (retryCallback, attempt) => {
|
|
535
|
+
const onConnection = (error, connection) => {
|
|
539
536
|
if (error) {
|
|
540
537
|
retryCallback(error);
|
|
541
538
|
return;
|
|
@@ -549,7 +546,16 @@ export class Producer extends Base {
|
|
|
549
546
|
retryCallback(this.#handleError(error), response);
|
|
550
547
|
});
|
|
551
548
|
});
|
|
552
|
-
}
|
|
549
|
+
};
|
|
550
|
+
// The transactional path fails over inside #getCoordinatorConnection (via
|
|
551
|
+
// #findCoordinator -> kGetBootstrapConnection). The idempotent path must thread
|
|
552
|
+
// `attempt` so kGetBootstrapConnection rotates past a reachable-but-hung broker.
|
|
553
|
+
if (transactionalId) {
|
|
554
|
+
this.#getCoordinatorConnection(onConnection);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
this[kGetBootstrapConnection](onConnection, attempt);
|
|
558
|
+
}
|
|
553
559
|
}, (error, response) => {
|
|
554
560
|
if (error) {
|
|
555
561
|
this.#handleFencingError(error);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createPromisifiedCallback, kCallbackPromise, runConcurrentCallbacks } from "../apis/callbacks.js";
|
|
2
2
|
import { connectionsPoolGetsChannel, createDiagnosticContext, notifyCreation } from "../diagnostic.js";
|
|
3
|
-
import { MultipleErrors } from "../errors.js";
|
|
3
|
+
import { MultipleErrors, NetworkError } from "../errors.js";
|
|
4
4
|
import { TypedEventEmitter } from "../events.js";
|
|
5
5
|
import { Connection, ConnectionStatuses } from "./connection.js";
|
|
6
6
|
let currentInstance = 0;
|
|
@@ -95,6 +95,14 @@ export class ConnectionPool extends TypedEventEmitter {
|
|
|
95
95
|
callback(new Error('Connection pool is closed.'));
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
|
+
if (broker == null || broker.host == null || broker.port == null) {
|
|
99
|
+
callback(new NetworkError('Cannot get a connection: the broker is not available in the current cluster metadata.', {
|
|
100
|
+
canRetry: true,
|
|
101
|
+
hasStaleMetadata: true,
|
|
102
|
+
broker
|
|
103
|
+
}));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
98
106
|
const key = `${broker.host}:${broker.port}`;
|
|
99
107
|
const existing = this.#connections.get(key);
|
|
100
108
|
if (existing) {
|
|
@@ -53,6 +53,9 @@ export declare const groupOptionsProperties: {
|
|
|
53
53
|
};
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
|
+
protocolsMetadata: {
|
|
57
|
+
function: boolean;
|
|
58
|
+
};
|
|
56
59
|
partitionAssigner: {
|
|
57
60
|
function: boolean;
|
|
58
61
|
};
|
|
@@ -195,6 +198,9 @@ export declare const groupOptionsSchema: {
|
|
|
195
198
|
};
|
|
196
199
|
};
|
|
197
200
|
};
|
|
201
|
+
protocolsMetadata: {
|
|
202
|
+
function: boolean;
|
|
203
|
+
};
|
|
198
204
|
partitionAssigner: {
|
|
199
205
|
function: boolean;
|
|
200
206
|
};
|
|
@@ -315,6 +321,9 @@ export declare const consumeOptionsSchema: {
|
|
|
315
321
|
};
|
|
316
322
|
};
|
|
317
323
|
};
|
|
324
|
+
protocolsMetadata: {
|
|
325
|
+
function: boolean;
|
|
326
|
+
};
|
|
318
327
|
partitionAssigner: {
|
|
319
328
|
function: boolean;
|
|
320
329
|
};
|
|
@@ -481,6 +490,9 @@ export declare const consumerOptionsSchema: {
|
|
|
481
490
|
};
|
|
482
491
|
};
|
|
483
492
|
};
|
|
493
|
+
protocolsMetadata: {
|
|
494
|
+
function: boolean;
|
|
495
|
+
};
|
|
484
496
|
partitionAssigner: {
|
|
485
497
|
function: boolean;
|
|
486
498
|
};
|
|
@@ -608,6 +620,9 @@ export declare const fetchOptionsSchema: {
|
|
|
608
620
|
};
|
|
609
621
|
};
|
|
610
622
|
};
|
|
623
|
+
protocolsMetadata: {
|
|
624
|
+
function: boolean;
|
|
625
|
+
};
|
|
611
626
|
partitionAssigner: {
|
|
612
627
|
function: boolean;
|
|
613
628
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Callback } from "../../apis/definitions";
|
|
1
2
|
import { type FetchRequestTopic } from "../../apis/consumer/fetch-v17";
|
|
2
3
|
import { type GroupProtocols } from "../../apis/enumerations";
|
|
3
4
|
import { type ConnectionPool } from "../../network/connection-pool";
|
|
@@ -33,6 +34,7 @@ export interface PreferredReadReplica {
|
|
|
33
34
|
}
|
|
34
35
|
export type CorruptedMessageHandler = (record: KafkaRecord, topic: string, partition: number, firstTimestamp: bigint, firstOffset: bigint, commit: Message['commit']) => boolean;
|
|
35
36
|
export type GroupPartitionsAssigner = (current: string, members: Map<string, ExtendedGroupProtocolSubscription>, topics: Set<string>, metadata: ClusterMetadata) => GroupPartitionsAssignments[];
|
|
37
|
+
export type GroupProtocolsMetadataCallback = (protocols: GroupProtocolSubscription[], topics: string[], metadata: ClusterMetadata, callback: Callback<Buffer>) => Buffer | Promise<Buffer> | undefined;
|
|
36
38
|
export declare const MessagesStreamModes: {
|
|
37
39
|
readonly LATEST: "latest";
|
|
38
40
|
readonly EARLIEST: "earliest";
|
|
@@ -57,6 +59,7 @@ export interface GroupOptions {
|
|
|
57
59
|
rebalanceTimeout?: number;
|
|
58
60
|
heartbeatInterval?: number;
|
|
59
61
|
protocols?: GroupProtocolSubscription[];
|
|
62
|
+
protocolsMetadata?: GroupProtocolsMetadataCallback;
|
|
60
63
|
partitionAssigner?: GroupPartitionsAssigner;
|
|
61
64
|
assignmentUserData?: Buffer;
|
|
62
65
|
}
|
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.4.0-alpha.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0-alpha.1",
|
|
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)",
|