@platformatic/kafka 2.3.0 → 2.3.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.
|
@@ -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";
|
|
@@ -512,10 +512,64 @@ export class Consumer extends Base {
|
|
|
512
512
|
retryCallback(error);
|
|
513
513
|
return;
|
|
514
514
|
}
|
|
515
|
-
|
|
515
|
+
let session = this.#fetchSessions.get(node);
|
|
516
|
+
if (session && session.pool !== pool) {
|
|
517
|
+
session = undefined;
|
|
518
|
+
}
|
|
516
519
|
const sessionId = session?.id ?? 0;
|
|
517
520
|
const sessionEpoch = session?.epoch ?? 0;
|
|
518
|
-
|
|
521
|
+
// The desired state of the session: options.topics always lists all the wanted partitions
|
|
522
|
+
const desired = new Map();
|
|
523
|
+
for (const topic of options.topics) {
|
|
524
|
+
let partitions = desired.get(topic.topicId);
|
|
525
|
+
if (!partitions) {
|
|
526
|
+
partitions = new Map();
|
|
527
|
+
desired.set(topic.topicId, partitions);
|
|
528
|
+
}
|
|
529
|
+
for (const partition of topic.partitions) {
|
|
530
|
+
// Copy the partition since the data is mutated in the error paths
|
|
531
|
+
partitions.set(partition.partition, { ...partition });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
let requestTopics = options.topics;
|
|
535
|
+
const forgottenTopicsData = [];
|
|
536
|
+
if (session) {
|
|
537
|
+
// Only send the partitions which are new or whose state changed: the broker
|
|
538
|
+
// keeps serving the unchanged ones from the session (KIP-227)
|
|
539
|
+
requestTopics = [];
|
|
540
|
+
for (const [topicId, partitions] of desired) {
|
|
541
|
+
const cached = session.partitions.get(topicId);
|
|
542
|
+
const changed = [];
|
|
543
|
+
for (const partition of partitions.values()) {
|
|
544
|
+
const previous = cached?.get(partition.partition);
|
|
545
|
+
if (!previous ||
|
|
546
|
+
previous.fetchOffset !== partition.fetchOffset ||
|
|
547
|
+
previous.currentLeaderEpoch !== partition.currentLeaderEpoch ||
|
|
548
|
+
previous.lastFetchedEpoch !== partition.lastFetchedEpoch ||
|
|
549
|
+
previous.partitionMaxBytes !== partition.partitionMaxBytes) {
|
|
550
|
+
changed.push(partition);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (changed.length > 0) {
|
|
554
|
+
requestTopics.push({ topicId, partitions: changed });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Ask the broker to remove from the session the partitions which are no
|
|
558
|
+
// longer wanted, otherwise it keeps serving them forever
|
|
559
|
+
for (const [topicId, partitions] of session.partitions) {
|
|
560
|
+
const stillDesired = desired.get(topicId);
|
|
561
|
+
const forgotten = [];
|
|
562
|
+
for (const partition of partitions.keys()) {
|
|
563
|
+
if (!stillDesired?.has(partition)) {
|
|
564
|
+
forgotten.push(partition);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (forgotten.length > 0) {
|
|
568
|
+
forgottenTopicsData.push({ topic: topicId, partitions: forgotten });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
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
573
|
if (error) {
|
|
520
574
|
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
521
575
|
const genericError = error;
|
|
@@ -537,7 +591,7 @@ export class Consumer extends Base {
|
|
|
537
591
|
}
|
|
538
592
|
else {
|
|
539
593
|
this.#updatePreferredReadReplicas(metadata, topicIds, result);
|
|
540
|
-
this.#updateFetchSession(node, sessionEpoch, result.sessionId);
|
|
594
|
+
this.#updateFetchSession(node, pool, sessionEpoch, result.sessionId, desired);
|
|
541
595
|
}
|
|
542
596
|
retryCallback(error, result);
|
|
543
597
|
});
|
|
@@ -556,12 +610,19 @@ export class Consumer extends Base {
|
|
|
556
610
|
callback(error, response);
|
|
557
611
|
}, 0);
|
|
558
612
|
}
|
|
559
|
-
#updateFetchSession(node, sentEpoch, responseSessionId) {
|
|
613
|
+
#updateFetchSession(node, pool, sentEpoch, responseSessionId, desired) {
|
|
560
614
|
if (!responseSessionId) {
|
|
561
615
|
this.#fetchSessions.delete(node);
|
|
562
616
|
return;
|
|
563
617
|
}
|
|
564
|
-
|
|
618
|
+
// After a successful fetch the broker session contains exactly the desired partitions:
|
|
619
|
+
// unchanged ones already matched it, changed ones were sent and removed ones were forgotten
|
|
620
|
+
this.#fetchSessions.set(node, {
|
|
621
|
+
id: responseSessionId,
|
|
622
|
+
epoch: nextFetchEpoch(sentEpoch),
|
|
623
|
+
pool,
|
|
624
|
+
partitions: desired
|
|
625
|
+
});
|
|
565
626
|
}
|
|
566
627
|
#commit(options, callback, rejoinAttempts = 0) {
|
|
567
628
|
this.#performGroupOperation('commit', (connection, groupCallback) => {
|
|
@@ -1230,11 +1291,17 @@ export class Consumer extends Base {
|
|
|
1230
1291
|
topicSubscriptions.push(subscription);
|
|
1231
1292
|
}
|
|
1232
1293
|
}
|
|
1233
|
-
|
|
1294
|
+
const subscribedTopics = Array.from(topicsSubscriptions.keys());
|
|
1295
|
+
this[kMetadata]({ topics: subscribedTopics }, (error, metadata) => {
|
|
1234
1296
|
if (error) {
|
|
1235
1297
|
callback(this.#handleError(error));
|
|
1236
1298
|
return;
|
|
1237
1299
|
}
|
|
1300
|
+
const missingTopic = subscribedTopics.find(topic => !metadata.topics.has(topic));
|
|
1301
|
+
if (missingTopic) {
|
|
1302
|
+
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 })));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1238
1305
|
this.#performSyncGroup(partitionsAssigner, this.#createAssignments(partitionsAssigner, metadata), callback);
|
|
1239
1306
|
});
|
|
1240
1307
|
return;
|
|
@@ -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]);
|
|
@@ -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) {
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const name = "@platformatic/kafka";
|
|
2
|
-
export const version = "2.3.
|
|
2
|
+
export const version = "2.3.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.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)",
|