@platformatic/kafka 2.11.0 → 2.12.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/README.md +6 -0
- package/dist/apis/definitions.d.ts +1 -1
- package/dist/apis/producer/produce-v10.d.ts +1 -0
- package/dist/apis/producer/produce-v10.js +26 -1
- package/dist/apis/producer/produce-v11.d.ts +1 -0
- package/dist/apis/producer/produce-v11.js +26 -1
- package/dist/apis/producer/produce-v7.d.ts +1 -0
- package/dist/apis/producer/produce-v7.js +23 -1
- package/dist/apis/producer/produce-v8.d.ts +1 -0
- package/dist/apis/producer/produce-v8.js +23 -1
- package/dist/apis/producer/produce-v9.d.ts +1 -0
- package/dist/apis/producer/produce-v9.js +26 -1
- package/dist/apis/producer/utils.d.ts +11 -0
- package/dist/apis/producer/utils.js +31 -0
- package/dist/clients/admin/admin.js +10 -13
- package/dist/clients/base/base.d.ts +1 -1
- package/dist/clients/base/base.js +6 -6
- package/dist/clients/base/options.d.ts +4 -0
- package/dist/clients/base/options.js +2 -1
- package/dist/clients/base/types.d.ts +1 -0
- package/dist/clients/consumer/consumer-protocol.d.ts +32 -0
- package/dist/clients/consumer/consumer-protocol.js +114 -0
- package/dist/clients/consumer/consumer.d.ts +3 -2
- package/dist/clients/consumer/consumer.js +305 -93
- package/dist/clients/consumer/index.d.ts +2 -0
- package/dist/clients/consumer/index.js +2 -0
- package/dist/clients/consumer/messages-stream.d.ts +4 -2
- package/dist/clients/consumer/messages-stream.js +126 -16
- package/dist/clients/consumer/partitions-assigners.d.ts +3 -0
- package/dist/clients/consumer/partitions-assigners.js +259 -0
- package/dist/clients/consumer/types.d.ts +11 -0
- package/dist/clients/producer/producer.d.ts +1 -1
- package/dist/clients/producer/producer.js +1 -1
- package/dist/errors.js +12 -1
- package/dist/network/connection.d.ts +1 -1
- package/dist/network/connection.js +83 -40
- package/dist/protocol/compression.d.ts +6 -0
- package/dist/protocol/compression.js +14 -1
- package/dist/protocol/records.d.ts +1 -0
- package/dist/protocol/records.js +40 -17
- package/dist/registries/confluent-schema-registry.d.ts +2 -1
- package/dist/registries/confluent-schema-registry.js +2 -1
- package/dist/symbols.d.ts +1 -0
- package/dist/symbols.js +1 -0
- package/dist/typescript-4/dist/apis/definitions.d.ts +1 -1
- package/dist/typescript-4/dist/apis/producer/produce-v10.d.ts +1 -0
- package/dist/typescript-4/dist/apis/producer/produce-v11.d.ts +1 -0
- package/dist/typescript-4/dist/apis/producer/produce-v7.d.ts +1 -0
- package/dist/typescript-4/dist/apis/producer/produce-v8.d.ts +1 -0
- package/dist/typescript-4/dist/apis/producer/produce-v9.d.ts +1 -0
- package/dist/typescript-4/dist/apis/producer/utils.d.ts +11 -0
- package/dist/typescript-4/dist/clients/base/base.d.ts +1 -1
- package/dist/typescript-4/dist/clients/base/options.d.ts +4 -0
- package/dist/typescript-4/dist/clients/base/types.d.ts +1 -0
- package/dist/typescript-4/dist/clients/consumer/consumer-protocol.d.ts +32 -0
- package/dist/typescript-4/dist/clients/consumer/consumer.d.ts +3 -2
- package/dist/typescript-4/dist/clients/consumer/index.d.ts +2 -0
- package/dist/typescript-4/dist/clients/consumer/messages-stream.d.ts +4 -2
- package/dist/typescript-4/dist/clients/consumer/partitions-assigners.d.ts +4 -1
- package/dist/typescript-4/dist/clients/consumer/types.d.ts +11 -0
- package/dist/typescript-4/dist/clients/producer/producer.d.ts +1 -1
- package/dist/typescript-4/dist/network/connection.d.ts +1 -1
- package/dist/typescript-4/dist/protocol/compression.d.ts +7 -1
- package/dist/typescript-4/dist/protocol/records.d.ts +1 -0
- package/dist/typescript-4/dist/registries/confluent-schema-registry.d.ts +2 -1
- package/dist/typescript-4/dist/symbols.d.ts +1 -0
- package/dist/version.js +1 -1
- package/package.json +5 -2
|
@@ -2,18 +2,17 @@ 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 { findErrorBy, NetworkError, protocolErrors, UserError } from "../../errors.js";
|
|
6
|
-
import { INT32_SIZE } from "../../protocol/definitions.js";
|
|
7
|
-
import { Reader } from "../../protocol/reader.js";
|
|
5
|
+
import { findErrorBy, MultipleErrors, NetworkError, protocolErrors, UserError } from "../../errors.js";
|
|
8
6
|
import { IS_CONTROL } from "../../protocol/records.js";
|
|
9
7
|
import { Writer } from "../../protocol/writer.js";
|
|
10
|
-
import { kAutocommit, kGetFetchNode, kRefreshOffsetsAndFetch } from "../../symbols.js";
|
|
8
|
+
import { kAutocommit, kGetFetchNode, kRefreshOffsetsAndFetch, kUpdateCommittedOffset } from "../../symbols.js";
|
|
11
9
|
import { emitExperimentalApiWarning } from "../../utils.js";
|
|
12
10
|
import { Base, kAfterCreate, kCheckNotClosed, kClosed, kConnections, kCreateConnectionPool, kFormatValidationErrors, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kPrometheus, kValidateOptions } from "../base/base.js";
|
|
13
11
|
import { ensureMetric } from "../metrics.js";
|
|
14
12
|
import { MessagesStream } from "./messages-stream.js";
|
|
13
|
+
import { CONSUMER_PROTOCOL_HIGHEST_VERSION, decodeCooperativeStickyGeneration, decodeConsumerProtocolAssignment, decodeConsumerProtocolSubscription, encodeConsumerProtocolAssignment, encodeConsumerProtocolSubscription } from "./consumer-protocol.js";
|
|
15
14
|
import { commitOptionsValidator, consumeOptionsValidator, consumerOptionsValidator, defaultConsumerOptions, fetchOptionsValidator, getLagOptionsValidator, groupIdAndOptionsValidator, groupOptionsValidator, listCommitsOptionsValidator, listOffsetsOptionsValidator } from "./options.js";
|
|
16
|
-
import { roundRobinAssigner } from "./partitions-assigners.js";
|
|
15
|
+
import { COOPERATIVE_STICKY_ASSIGNOR, cooperativeStickyAssigner, roundRobinAssigner } from "./partitions-assigners.js";
|
|
17
16
|
import { TopicsMap } from "./topics-map.js";
|
|
18
17
|
import { nextFetchEpoch, partitionKey } from "./utils.js";
|
|
19
18
|
export class Consumer extends Base {
|
|
@@ -170,7 +169,7 @@ export class Consumer extends Base {
|
|
|
170
169
|
return false;
|
|
171
170
|
}
|
|
172
171
|
if (this.#useConsumerGroupProtocol) {
|
|
173
|
-
return !!this.memberId && this.#memberEpoch
|
|
172
|
+
return !!this.memberId && this.#memberEpoch > 0;
|
|
174
173
|
}
|
|
175
174
|
// We consider the group ready if we have a groupId, a memberId and heartbeat interval
|
|
176
175
|
return this.#membershipActive && Boolean(this.groupId) && Boolean(this.memberId) && this.#heartbeatInterval !== null;
|
|
@@ -637,7 +636,14 @@ export class Consumer extends Base {
|
|
|
637
636
|
findErrorBy(error, 'apiId', 'FETCH_SESSION_TOPIC_ID_ERROR')) {
|
|
638
637
|
this.#fetchSessions.delete(node);
|
|
639
638
|
}
|
|
640
|
-
|
|
639
|
+
// NOT_LEADER_OR_FOLLOWER/LEADER_NOT_AVAILABLE mean leadership just moved (broker
|
|
640
|
+
// rebalance/leader election); without a refresh here, kPerformWithRetry repeats
|
|
641
|
+
// the same request against the stale cached leader until retries exhaust.
|
|
642
|
+
// UNKNOWN_TOPIC_OR_PARTITION also carries hasStaleMetadata but is deliberately
|
|
643
|
+
// excluded: broadening this to it breaks deleted-topic handling (see git history).
|
|
644
|
+
if (findErrorBy(error, 'apiId', 'FENCED_LEADER_EPOCH') ||
|
|
645
|
+
findErrorBy(error, 'apiId', 'NOT_LEADER_OR_FOLLOWER') ||
|
|
646
|
+
findErrorBy(error, 'apiId', 'LEADER_NOT_AVAILABLE')) {
|
|
641
647
|
this.#fetchSessions.delete(node);
|
|
642
648
|
this.clearMetadata();
|
|
643
649
|
for (const topic of options.topics) {
|
|
@@ -717,6 +723,16 @@ export class Consumer extends Base {
|
|
|
717
723
|
});
|
|
718
724
|
return;
|
|
719
725
|
}
|
|
726
|
+
if (!error) {
|
|
727
|
+
for (const { topic, partition, offset } of options.offsets) {
|
|
728
|
+
const key = partitionKey(topic, partition);
|
|
729
|
+
for (const stream of this.#streams) {
|
|
730
|
+
if (stream.offsetsCommitted.has(key)) {
|
|
731
|
+
stream[kUpdateCommittedOffset](topic, partition, offset);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
720
736
|
callback(error);
|
|
721
737
|
});
|
|
722
738
|
}
|
|
@@ -780,7 +796,16 @@ export class Consumer extends Base {
|
|
|
780
796
|
}, concurrentCallback, 0);
|
|
781
797
|
}, (error, responses) => {
|
|
782
798
|
if (error) {
|
|
783
|
-
|
|
799
|
+
// Mirrors the fetch() handling: right after a topic is created or its leadership moves,
|
|
800
|
+
// the cached leader is briefly stale and the broker answers with one of these three
|
|
801
|
+
// errors. Retrying once against fresh metadata clears the race instead of surfacing it.
|
|
802
|
+
// UNKNOWN_TOPIC_OR_PARTITION also carries hasStaleMetadata but is deliberately excluded,
|
|
803
|
+
// same as in fetch(), to avoid masking deleted-topic handling.
|
|
804
|
+
if (!forceUpdateMetadata &&
|
|
805
|
+
(NetworkError.isRetryable(error) ||
|
|
806
|
+
findErrorBy(error, 'apiId', 'FENCED_LEADER_EPOCH') ||
|
|
807
|
+
findErrorBy(error, 'apiId', 'NOT_LEADER_OR_FOLLOWER') ||
|
|
808
|
+
findErrorBy(error, 'apiId', 'LEADER_NOT_AVAILABLE'))) {
|
|
784
809
|
this.clearMetadata();
|
|
785
810
|
this.#listOffsets(withTimestamps, options, callback, true);
|
|
786
811
|
return;
|
|
@@ -931,9 +956,7 @@ export class Consumer extends Base {
|
|
|
931
956
|
// - a rejoin that never completes still surfaces as a stall
|
|
932
957
|
this.#resetHeartbeatStall();
|
|
933
958
|
this.#trackHeartbeatStall(options, error, options.rebalanceTimeout + this.#heartbeatStallTimeoutFor(options));
|
|
934
|
-
this
|
|
935
|
-
this.#joinGroup(options, retryCallback);
|
|
936
|
-
}, error => {
|
|
959
|
+
this.#joinGroup(options, error => {
|
|
937
960
|
if (error) {
|
|
938
961
|
this.emitWithDebug(null, 'error', error);
|
|
939
962
|
if (!this.#heartbeatStalled) {
|
|
@@ -949,7 +972,7 @@ export class Consumer extends Base {
|
|
|
949
972
|
this.#resetHeartbeatStall();
|
|
950
973
|
}
|
|
951
974
|
this.emitWithDebug('consumer', 'rejoin');
|
|
952
|
-
}
|
|
975
|
+
});
|
|
953
976
|
return;
|
|
954
977
|
}
|
|
955
978
|
// The error was not group related, so we schedule another heartbeat.
|
|
@@ -1107,6 +1130,22 @@ export class Consumer extends Base {
|
|
|
1107
1130
|
}
|
|
1108
1131
|
return result;
|
|
1109
1132
|
}
|
|
1133
|
+
#diffGroupAssignments(A, B) {
|
|
1134
|
+
const result = [];
|
|
1135
|
+
for (const a of A) {
|
|
1136
|
+
const b = B.find(assignment => assignment.topic === a.topic);
|
|
1137
|
+
if (!b) {
|
|
1138
|
+
result.push({ topic: a.topic, partitions: [...a.partitions] });
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
const diff = a.partitions.filter(partition => !b.partitions.includes(partition));
|
|
1142
|
+
if (diff.length > 0) {
|
|
1143
|
+
result.push({ topic: a.topic, partitions: diff });
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return result;
|
|
1148
|
+
}
|
|
1110
1149
|
#revokePartitions(newAssignment) {
|
|
1111
1150
|
const toRevoke = this.#diffAssignments(this.#assignments, newAssignment);
|
|
1112
1151
|
if (toRevoke.length === 0) {
|
|
@@ -1132,6 +1171,14 @@ export class Consumer extends Base {
|
|
|
1132
1171
|
#assignPartitions(newAssignment) {
|
|
1133
1172
|
const toAssign = this.#diffAssignments(newAssignment, this.#assignments);
|
|
1134
1173
|
if (toAssign.length === 0) {
|
|
1174
|
+
// An explicit empty initial assignment confirms an idle member, even though
|
|
1175
|
+
// there are no partitions to add. An omitted assignment must remain unknown.
|
|
1176
|
+
if (this.assignments === null && newAssignment.length === 0) {
|
|
1177
|
+
this.assignments = [];
|
|
1178
|
+
for (const stream of this.#streams) {
|
|
1179
|
+
stream[kRefreshOffsetsAndFetch]();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1135
1182
|
return;
|
|
1136
1183
|
}
|
|
1137
1184
|
this.#updateAssignments(newAssignment, error => {
|
|
@@ -1210,9 +1257,18 @@ export class Consumer extends Base {
|
|
|
1210
1257
|
});
|
|
1211
1258
|
return;
|
|
1212
1259
|
}
|
|
1260
|
+
const finish = () => {
|
|
1261
|
+
this.emitWithDebug('consumer', 'group:leave', { groupId: this.groupId, memberId: this.memberId });
|
|
1262
|
+
this.memberId = null;
|
|
1263
|
+
this.#memberEpoch = -1;
|
|
1264
|
+
this.#assignments = [];
|
|
1265
|
+
this.assignments = [];
|
|
1266
|
+
this.#syncPreferredReadReplicas();
|
|
1267
|
+
callback(null);
|
|
1268
|
+
};
|
|
1213
1269
|
// Leave by sending a heartbeat with memberEpoch = -1
|
|
1214
1270
|
this.#cancelHeartbeat();
|
|
1215
|
-
|
|
1271
|
+
const operation = (connection, groupCallback) => {
|
|
1216
1272
|
this[kGetApi]('ConsumerGroupHeartbeat', (error, api) => {
|
|
1217
1273
|
if (error) {
|
|
1218
1274
|
groupCallback(error);
|
|
@@ -1226,15 +1282,18 @@ export class Consumer extends Base {
|
|
|
1226
1282
|
this.#groupRemoteAssignor, [], // topicPartitions
|
|
1227
1283
|
groupCallback);
|
|
1228
1284
|
});
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1285
|
+
};
|
|
1286
|
+
if (this[kClosed]) {
|
|
1287
|
+
const connection = this.#getGroupCoordinatorConnection();
|
|
1288
|
+
if (connection) {
|
|
1289
|
+
operation(connection, finish);
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
finish();
|
|
1293
|
+
}
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
this.#performDeduplicateGroupOperaton('leaveGroupConsumerProtocol', operation, finish);
|
|
1238
1297
|
}
|
|
1239
1298
|
#getConsumerGroupHeartbeatMemberId(apiVersion) {
|
|
1240
1299
|
if (this.memberId) {
|
|
@@ -1322,6 +1381,13 @@ export class Consumer extends Base {
|
|
|
1322
1381
|
}, callback);
|
|
1323
1382
|
}
|
|
1324
1383
|
#performJoinGroup(options, callback) {
|
|
1384
|
+
if (!this.#membershipActive) {
|
|
1385
|
+
callback(null);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
this.#performJoinGroupAttempt(options, callback, 0, []);
|
|
1389
|
+
}
|
|
1390
|
+
#performJoinGroupAttempt(options, callback, attempt, attemptErrors) {
|
|
1325
1391
|
if (!this.#membershipActive) {
|
|
1326
1392
|
callback(null);
|
|
1327
1393
|
return;
|
|
@@ -1329,7 +1395,7 @@ export class Consumer extends Base {
|
|
|
1329
1395
|
this.#cancelHeartbeat();
|
|
1330
1396
|
this.#createJoinGroupProtocols(options, (error, protocols) => {
|
|
1331
1397
|
if (error) {
|
|
1332
|
-
callback
|
|
1398
|
+
this.#retryJoinGroup(options, callback, attempt, attemptErrors, error);
|
|
1333
1399
|
return;
|
|
1334
1400
|
}
|
|
1335
1401
|
this.#performDeduplicateGroupOperaton('joinGroup', (connection, groupCallback) => {
|
|
@@ -1346,11 +1412,7 @@ export class Consumer extends Base {
|
|
|
1346
1412
|
return;
|
|
1347
1413
|
}
|
|
1348
1414
|
if (error) {
|
|
1349
|
-
|
|
1350
|
-
this.#performJoinGroup(options, callback);
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
callback(error);
|
|
1415
|
+
this.#retryJoinGroup(options, callback, attempt, attemptErrors, error);
|
|
1354
1416
|
return;
|
|
1355
1417
|
}
|
|
1356
1418
|
// This is for Azure Event Hubs compatibility, which does not respond with an error on the first join
|
|
@@ -1369,31 +1431,120 @@ export class Consumer extends Base {
|
|
|
1369
1431
|
return;
|
|
1370
1432
|
}
|
|
1371
1433
|
if (error) {
|
|
1372
|
-
|
|
1373
|
-
this.#performJoinGroup(options, callback);
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
callback(error);
|
|
1434
|
+
this.#retryJoinGroup(options, callback, attempt, attemptErrors, error);
|
|
1377
1435
|
return;
|
|
1378
1436
|
}
|
|
1379
|
-
this
|
|
1380
|
-
this.#syncPreferredReadReplicas();
|
|
1381
|
-
this.#cancelHeartbeat();
|
|
1382
|
-
this.#heartbeatInterval = setTimeout(() => {
|
|
1383
|
-
this.#heartbeat(options);
|
|
1384
|
-
}, options.heartbeatInterval);
|
|
1385
|
-
this.emitWithDebug('consumer', 'group:join', {
|
|
1386
|
-
groupId: this.groupId,
|
|
1387
|
-
memberId: this.memberId,
|
|
1388
|
-
generationId: this.generationId,
|
|
1389
|
-
isLeader: this.#isLeader,
|
|
1390
|
-
assignments: this.assignments
|
|
1391
|
-
});
|
|
1392
|
-
callback(null, this.memberId);
|
|
1437
|
+
this.#completeClassicSyncGroup(options, response, callback);
|
|
1393
1438
|
});
|
|
1394
1439
|
});
|
|
1395
1440
|
});
|
|
1396
1441
|
}
|
|
1442
|
+
#retryJoinGroup(options, callback, attempt, attemptErrors, error) {
|
|
1443
|
+
const retries = this[kOptions].retries;
|
|
1444
|
+
const memberId = this.memberId;
|
|
1445
|
+
const rejoinError = this.#getRejoinError(error);
|
|
1446
|
+
// MEMBER_ID_REQUIRED is the normal first-join handshake. The broker assigns a member ID and
|
|
1447
|
+
// expects the JoinGroup request to be sent again; this progress must not consume a retry.
|
|
1448
|
+
if (rejoinError?.apiId === 'MEMBER_ID_REQUIRED' && this.memberId !== memberId) {
|
|
1449
|
+
this.#performJoinGroupAttempt(options, callback, attempt, attemptErrors);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
attemptErrors.push(error);
|
|
1453
|
+
if (!rejoinError) {
|
|
1454
|
+
if (error instanceof MultipleErrors && error.errors.length > 0) {
|
|
1455
|
+
const nestedMessages = error.errors.map(error => error.message).join(' ');
|
|
1456
|
+
callback(new MultipleErrors(`${error.message} ${nestedMessages}`, error.errors));
|
|
1457
|
+
}
|
|
1458
|
+
else {
|
|
1459
|
+
callback(error);
|
|
1460
|
+
}
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (attempt >= retries) {
|
|
1464
|
+
callback(attempt > 0 ? new MultipleErrors(`joinGroup failed ${attempt + 1} times.`, attemptErrors) : error);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
let delay = this[kOptions].retryDelay;
|
|
1468
|
+
if (typeof delay === 'function') {
|
|
1469
|
+
delay = delay(this, 'joinGroup', attempt + 1, retries, error);
|
|
1470
|
+
}
|
|
1471
|
+
function onClose() {
|
|
1472
|
+
clearTimeout(timeout);
|
|
1473
|
+
attemptErrors.push(new UserError('Client closed while retrying joinGroup.'));
|
|
1474
|
+
callback(new MultipleErrors(`joinGroup failed ${attempt + 1} times.`, attemptErrors));
|
|
1475
|
+
}
|
|
1476
|
+
const timeout = setTimeout(() => {
|
|
1477
|
+
this.removeListener('client:close', onClose);
|
|
1478
|
+
this.#performJoinGroupAttempt(options, callback, attempt + 1, attemptErrors);
|
|
1479
|
+
}, delay);
|
|
1480
|
+
this.once('client:close', onClose);
|
|
1481
|
+
}
|
|
1482
|
+
#completeClassicSyncGroup(options, assignments, callback) {
|
|
1483
|
+
if (this.#isCooperativeProtocol()) {
|
|
1484
|
+
const previousAssignments = this.assignments ?? [];
|
|
1485
|
+
const revoked = this.#diffGroupAssignments(previousAssignments, assignments);
|
|
1486
|
+
const addedOnRevokeRound = this.#diffGroupAssignments(assignments, previousAssignments);
|
|
1487
|
+
if (revoked.length > 0) {
|
|
1488
|
+
for (const stream of this.#streams) {
|
|
1489
|
+
stream.pause();
|
|
1490
|
+
}
|
|
1491
|
+
// Snapshot the set: a failing autocommit destroys its stream, which removes it from
|
|
1492
|
+
// #streams while the concurrent callbacks are still iterating.
|
|
1493
|
+
runConcurrentCallbacks('Autocommit before cooperative rebalance failed.', [...this.#streams], (stream, concurrentCallback) => {
|
|
1494
|
+
// A destroyed stream never settles an autocommit, so do not wait on one.
|
|
1495
|
+
if (stream.destroyed || stream.closed) {
|
|
1496
|
+
concurrentCallback(null);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
stream[kAutocommit](concurrentCallback);
|
|
1500
|
+
}, autocommitError => {
|
|
1501
|
+
// Mirror Kafka's reference client: capture autocommit errors but
|
|
1502
|
+
// continue the rebalance so the group does not get stuck mid-cycle.
|
|
1503
|
+
// Per-stream autocommit errors are already surfaced through the
|
|
1504
|
+
// `autocommit` event on each MessagesStream.
|
|
1505
|
+
if (autocommitError) {
|
|
1506
|
+
this.emitWithDebug('consumer', 'group:autocommit:error', {
|
|
1507
|
+
groupId: this.groupId,
|
|
1508
|
+
error: autocommitError
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
this.assignments = assignments;
|
|
1512
|
+
this.#syncPreferredReadReplicas();
|
|
1513
|
+
for (const stream of this.#streams) {
|
|
1514
|
+
stream.resume();
|
|
1515
|
+
}
|
|
1516
|
+
if (addedOnRevokeRound.length > 0) {
|
|
1517
|
+
for (const stream of this.#streams) {
|
|
1518
|
+
stream[kRefreshOffsetsAndFetch]();
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
this.emitWithDebug('consumer', 'group:rebalance', { groupId: this.groupId });
|
|
1522
|
+
this.#performJoinGroup(options, callback);
|
|
1523
|
+
});
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const added = this.#diffGroupAssignments(assignments, this.assignments ?? []);
|
|
1528
|
+
this.assignments = assignments;
|
|
1529
|
+
this.#syncPreferredReadReplicas();
|
|
1530
|
+
this.#cancelHeartbeat();
|
|
1531
|
+
this.#heartbeatInterval = setTimeout(() => {
|
|
1532
|
+
this.#heartbeat(options);
|
|
1533
|
+
}, options.heartbeatInterval);
|
|
1534
|
+
this.emitWithDebug('consumer', 'group:join', {
|
|
1535
|
+
groupId: this.groupId,
|
|
1536
|
+
memberId: this.memberId,
|
|
1537
|
+
generationId: this.generationId,
|
|
1538
|
+
isLeader: this.#isLeader,
|
|
1539
|
+
assignments: this.assignments
|
|
1540
|
+
});
|
|
1541
|
+
if (this.#isCooperativeProtocol() && added.length > 0) {
|
|
1542
|
+
for (const stream of this.#streams) {
|
|
1543
|
+
stream[kRefreshOffsetsAndFetch]();
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
callback(null, this.memberId);
|
|
1547
|
+
}
|
|
1397
1548
|
#performLeaveGroup(force, callback) {
|
|
1398
1549
|
if (!this.memberId) {
|
|
1399
1550
|
callback(null);
|
|
@@ -1422,8 +1573,20 @@ export class Consumer extends Base {
|
|
|
1422
1573
|
});
|
|
1423
1574
|
return;
|
|
1424
1575
|
}
|
|
1576
|
+
const finish = () => {
|
|
1577
|
+
this.emitWithDebug('consumer', 'group:leave', {
|
|
1578
|
+
groupId: this.groupId,
|
|
1579
|
+
memberId: this.memberId,
|
|
1580
|
+
generationId: this.generationId
|
|
1581
|
+
});
|
|
1582
|
+
this.memberId = null;
|
|
1583
|
+
this.generationId = 0;
|
|
1584
|
+
this.assignments = null;
|
|
1585
|
+
this.#syncPreferredReadReplicas();
|
|
1586
|
+
callback(null);
|
|
1587
|
+
};
|
|
1425
1588
|
this.#cancelHeartbeat();
|
|
1426
|
-
|
|
1589
|
+
const operation = (connection, groupCallback) => {
|
|
1427
1590
|
this[kGetApi]('LeaveGroup', (error, api) => {
|
|
1428
1591
|
if (error) {
|
|
1429
1592
|
groupCallback(error);
|
|
@@ -1431,26 +1594,30 @@ export class Consumer extends Base {
|
|
|
1431
1594
|
}
|
|
1432
1595
|
api(connection, this.groupId, [{ memberId: this.memberId }], groupCallback);
|
|
1433
1596
|
});
|
|
1434
|
-
}
|
|
1597
|
+
};
|
|
1598
|
+
const onLeave = (error) => {
|
|
1435
1599
|
if (error) {
|
|
1436
1600
|
const unknownMemberError = findErrorBy(error, 'unknownMemberId', true);
|
|
1601
|
+
const unavailableBrokerWhileClosing = this[kClosed] && NetworkError.isRetryable(error);
|
|
1437
1602
|
// This is to avoid throwing an error if a group join was cancelled.
|
|
1438
|
-
if (!unknownMemberError) {
|
|
1603
|
+
if (!unknownMemberError && !unavailableBrokerWhileClosing) {
|
|
1439
1604
|
callback(error);
|
|
1440
1605
|
return;
|
|
1441
1606
|
}
|
|
1442
1607
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1608
|
+
finish();
|
|
1609
|
+
};
|
|
1610
|
+
if (this[kClosed]) {
|
|
1611
|
+
const connection = this.#getGroupCoordinatorConnection();
|
|
1612
|
+
if (connection) {
|
|
1613
|
+
operation(connection, onLeave);
|
|
1614
|
+
}
|
|
1615
|
+
else {
|
|
1616
|
+
finish();
|
|
1617
|
+
}
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
this.#performDeduplicateGroupOperaton('leaveGroup', operation, onLeave);
|
|
1454
1621
|
}
|
|
1455
1622
|
#performSyncGroup(partitionsAssigner, assignments, callback) {
|
|
1456
1623
|
if (!this.#membershipActive) {
|
|
@@ -1541,7 +1708,7 @@ export class Consumer extends Base {
|
|
|
1541
1708
|
});
|
|
1542
1709
|
}, (error, result) => {
|
|
1543
1710
|
callback(this.#handleError(error), result);
|
|
1544
|
-
});
|
|
1711
|
+
}, 0, [], () => this[kClosed]);
|
|
1545
1712
|
}
|
|
1546
1713
|
#validateGroupOptions(options, validator) {
|
|
1547
1714
|
validator ??= groupOptionsValidator;
|
|
@@ -1610,19 +1777,37 @@ export class Consumer extends Base {
|
|
|
1610
1777
|
https://github.com/apache/kafka/blob/trunk/clients/src/main/resources/common/message/ConsumerProtocolSubscription.json
|
|
1611
1778
|
*/
|
|
1612
1779
|
#encodeProtocolSubscriptionMetadata(metadata, topics) {
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
.
|
|
1780
|
+
const cooperative = metadata.name === COOPERATIVE_STICKY_ASSIGNOR;
|
|
1781
|
+
const ownedPartitions = cooperative ? this.#ownedPartitionsForSubscription(topics) : [];
|
|
1782
|
+
let userData = typeof metadata.metadata === 'string' ? Buffer.from(metadata.metadata) : metadata.metadata;
|
|
1783
|
+
if (cooperative && !userData && metadata.version < 2) {
|
|
1784
|
+
userData = Writer.create()
|
|
1785
|
+
.appendInt32(this.memberId && ownedPartitions.length > 0 ? this.generationId : -1)
|
|
1786
|
+
.buffer;
|
|
1787
|
+
}
|
|
1788
|
+
return encodeConsumerProtocolSubscription({
|
|
1789
|
+
version: metadata.version,
|
|
1790
|
+
topics,
|
|
1791
|
+
userData,
|
|
1792
|
+
ownedPartitions,
|
|
1793
|
+
generationId: cooperative && this.memberId && ownedPartitions.length > 0 ? this.generationId : -1,
|
|
1794
|
+
rackId: cooperative ? this.#clientRack || null : null
|
|
1795
|
+
});
|
|
1618
1796
|
}
|
|
1619
1797
|
#decodeProtocolSubscriptionMetadata(memberId, buffer) {
|
|
1620
|
-
const
|
|
1798
|
+
const subscription = decodeConsumerProtocolSubscription(buffer);
|
|
1799
|
+
const cooperative = this.#protocol === COOPERATIVE_STICKY_ASSIGNOR;
|
|
1800
|
+
const generationId = cooperative && subscription.version < 2
|
|
1801
|
+
? decodeCooperativeStickyGeneration(subscription.userData)
|
|
1802
|
+
: subscription.generationId;
|
|
1621
1803
|
return {
|
|
1622
1804
|
memberId,
|
|
1623
|
-
version:
|
|
1624
|
-
topics:
|
|
1625
|
-
metadata:
|
|
1805
|
+
version: subscription.version,
|
|
1806
|
+
topics: subscription.topics,
|
|
1807
|
+
metadata: subscription.userData,
|
|
1808
|
+
ownedPartitions: subscription.ownedPartitions,
|
|
1809
|
+
generationId,
|
|
1810
|
+
rackId: subscription.rackId
|
|
1626
1811
|
};
|
|
1627
1812
|
}
|
|
1628
1813
|
/*
|
|
@@ -1630,27 +1815,14 @@ export class Consumer extends Base {
|
|
|
1630
1815
|
https://github.com/apache/kafka/blob/trunk/clients/src/main/resources/common/message/ConsumerProtocolAssignment.json
|
|
1631
1816
|
*/
|
|
1632
1817
|
#encodeProtocolAssignment(assignments) {
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
.
|
|
1637
|
-
|
|
1638
|
-
}, false, false);
|
|
1639
|
-
writer.appendBytes(userData ?? null, false);
|
|
1640
|
-
return writer.buffer;
|
|
1818
|
+
return encodeConsumerProtocolAssignment({
|
|
1819
|
+
version: this.#isCooperativeProtocol() ? CONSUMER_PROTOCOL_HIGHEST_VERSION : 0,
|
|
1820
|
+
assignedPartitions: assignments,
|
|
1821
|
+
userData: this[kOptions].assignmentUserData ?? null
|
|
1822
|
+
});
|
|
1641
1823
|
}
|
|
1642
1824
|
#decodeProtocolAssignment(buffer) {
|
|
1643
|
-
|
|
1644
|
-
reader.skip(2); // Ignore Version information
|
|
1645
|
-
if (reader.remaining < INT32_SIZE) {
|
|
1646
|
-
return [];
|
|
1647
|
-
}
|
|
1648
|
-
return reader.readArray(r => {
|
|
1649
|
-
return {
|
|
1650
|
-
topic: r.readString(false),
|
|
1651
|
-
partitions: r.readArray(r => r.readInt32(), false, false)
|
|
1652
|
-
};
|
|
1653
|
-
}, false, false);
|
|
1825
|
+
return decodeConsumerProtocolAssignment(buffer).assignedPartitions;
|
|
1654
1826
|
}
|
|
1655
1827
|
#createAssignments(partitionsAssigner, metadata) {
|
|
1656
1828
|
const partitionTracker = new Map();
|
|
@@ -1673,7 +1845,7 @@ export class Consumer extends Base {
|
|
|
1673
1845
|
return [{ memberId: this.memberId, assignment: this.#encodeProtocolAssignment(assignments) }];
|
|
1674
1846
|
}
|
|
1675
1847
|
const encodedAssignments = [];
|
|
1676
|
-
partitionsAssigner ??= this
|
|
1848
|
+
partitionsAssigner ??= this.#defaultPartitionsAssigner();
|
|
1677
1849
|
for (const member of partitionsAssigner(this.memberId, this.#members, new Set(this.topics.current), metadata)) {
|
|
1678
1850
|
encodedAssignments.push({
|
|
1679
1851
|
memberId: member.memberId,
|
|
@@ -1682,6 +1854,28 @@ export class Consumer extends Base {
|
|
|
1682
1854
|
}
|
|
1683
1855
|
return encodedAssignments;
|
|
1684
1856
|
}
|
|
1857
|
+
#defaultPartitionsAssigner() {
|
|
1858
|
+
const configuredAssigner = this[kOptions].partitionAssigner;
|
|
1859
|
+
if (configuredAssigner) {
|
|
1860
|
+
return configuredAssigner;
|
|
1861
|
+
}
|
|
1862
|
+
if (this.#isCooperativeProtocol()) {
|
|
1863
|
+
return cooperativeStickyAssigner;
|
|
1864
|
+
}
|
|
1865
|
+
return roundRobinAssigner;
|
|
1866
|
+
}
|
|
1867
|
+
#isCooperativeProtocol() {
|
|
1868
|
+
return this.#protocol === COOPERATIVE_STICKY_ASSIGNOR;
|
|
1869
|
+
}
|
|
1870
|
+
#ownedPartitionsForSubscription(topics) {
|
|
1871
|
+
if (!this.assignments) {
|
|
1872
|
+
return [];
|
|
1873
|
+
}
|
|
1874
|
+
const currentTopics = new Set(topics);
|
|
1875
|
+
return this.assignments
|
|
1876
|
+
.filter(assignment => currentTopics.has(assignment.topic))
|
|
1877
|
+
.map(assignment => ({ topic: assignment.topic, partitions: [...assignment.partitions] }));
|
|
1878
|
+
}
|
|
1685
1879
|
#filterUncommittedMessages(response) {
|
|
1686
1880
|
for (const topic of response.responses) {
|
|
1687
1881
|
for (const partition of topic.partitions) {
|
|
@@ -1700,7 +1894,10 @@ export class Consumer extends Base {
|
|
|
1700
1894
|
// Now find last offsets
|
|
1701
1895
|
for (const batch of partition.records) {
|
|
1702
1896
|
if (batch.attributes & IS_CONTROL && batch.records[0].key?.readInt16BE(2) === 0) {
|
|
1703
|
-
abortedRanges.get(batch.producerId)
|
|
1897
|
+
const abortedRange = abortedRanges.get(batch.producerId);
|
|
1898
|
+
if (abortedRange) {
|
|
1899
|
+
abortedRange[1] = batch.firstOffset;
|
|
1900
|
+
}
|
|
1704
1901
|
}
|
|
1705
1902
|
}
|
|
1706
1903
|
// Filter all records in the aborted ranges
|
|
@@ -1722,7 +1919,14 @@ export class Consumer extends Base {
|
|
|
1722
1919
|
if (protocolError.rebalanceInProgress) {
|
|
1723
1920
|
this.emitWithDebug('consumer', 'group:rebalance', { groupId: this.groupId });
|
|
1724
1921
|
}
|
|
1725
|
-
|
|
1922
|
+
// A cooperative member which lost its membership or generation no longer owns anything:
|
|
1923
|
+
// clear the assignments so the next JoinGroup advertises an empty ownedPartitions set
|
|
1924
|
+
// instead of re-claiming partitions the group has already reassigned elsewhere.
|
|
1925
|
+
if (this.#isCooperativeProtocol() && ['UNKNOWN_MEMBER_ID', 'ILLEGAL_GENERATION'].includes(protocolError.apiId)) {
|
|
1926
|
+
this.assignments = [];
|
|
1927
|
+
this.#syncPreferredReadReplicas();
|
|
1928
|
+
}
|
|
1929
|
+
if (protocolError.unknownMemberId || protocolError.fencedInstanceId) {
|
|
1726
1930
|
this.memberId = null;
|
|
1727
1931
|
}
|
|
1728
1932
|
else if (protocolError.memberId && !this.memberId) {
|
|
@@ -1812,4 +2016,12 @@ export class Consumer extends Base {
|
|
|
1812
2016
|
}
|
|
1813
2017
|
return error;
|
|
1814
2018
|
}
|
|
2019
|
+
#getGroupCoordinatorConnection() {
|
|
2020
|
+
if (this.#coordinatorId === null) {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
const broker = this.currentMetadata?.brokers.get(this.#coordinatorId);
|
|
2024
|
+
const connection = broker ? this[kConnections].getEstablishedConnection(broker) : undefined;
|
|
2025
|
+
return connection?.isConnected() === true ? connection : null;
|
|
2026
|
+
}
|
|
1815
2027
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import { type CallbackWithPromise } from '../../apis/callbacks.ts';
|
|
3
|
+
import type { Callback } from '../../apis/definitions.ts';
|
|
3
4
|
import type { ConnectionPool } from '../../network/connection-pool.ts';
|
|
4
5
|
import { type Message } from '../../protocol/records.ts';
|
|
5
|
-
import { kAutocommit, kInstance, kRefreshOffsetsAndFetch } from '../../symbols.ts';
|
|
6
|
+
import { kAutocommit, kInstance, kRefreshOffsetsAndFetch, kUpdateCommittedOffset } from '../../symbols.ts';
|
|
6
7
|
import { kConnections, kInspect } from '../base/base.ts';
|
|
7
8
|
import type { Consumer } from './consumer.ts';
|
|
8
9
|
import { type CommitOptionsPartition, type ConsumeOptions } from './types.ts';
|
|
@@ -79,7 +80,8 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
|
|
|
79
80
|
_construct(callback: (error?: Error) => void): void;
|
|
80
81
|
_destroy(error: Error | null, callback: (error?: Error | null) => void): void;
|
|
81
82
|
_read(): void;
|
|
82
|
-
[
|
|
83
|
+
[kUpdateCommittedOffset](topic: string, partition: number, offset: bigint): void;
|
|
84
|
+
[kAutocommit](callback?: Callback<void>): void;
|
|
83
85
|
[kRefreshOffsetsAndFetch](): void;
|
|
84
86
|
[kInspect](...args: unknown[]): void;
|
|
85
87
|
}
|