@platformatic/kafka 2.2.3 → 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.
- package/README.md +3 -1
- package/dist/apis/admin/create-topics-v7.js +1 -0
- package/dist/apis/callbacks.d.ts +1 -3
- package/dist/apis/callbacks.js +26 -11
- package/dist/clients/admin/admin.js +330 -356
- package/dist/clients/base/base.js +8 -1
- package/dist/clients/consumer/consumer.js +92 -4
- package/dist/clients/consumer/messages-stream.js +12 -1
- package/dist/clients/consumer/utils.d.ts +1 -0
- package/dist/clients/consumer/utils.js +6 -0
- package/dist/clients/producer/producer.js +67 -13
- package/dist/clients/serde.d.ts +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +6 -0
- package/dist/network/connection-pool.js +9 -1
- package/dist/protocol/reader.js +4 -3
- package/dist/registries/abstract.d.ts +2 -2
- package/dist/registries/confluent-schema-registry.d.ts +1 -1
- package/dist/registries/confluent-schema-registry.js +3 -0
- package/dist/typescript-4/dist/apis/callbacks.d.ts +1 -3
- package/dist/typescript-4/dist/clients/consumer/utils.d.ts +2 -1
- package/dist/typescript-4/dist/clients/serde.d.ts +1 -1
- package/dist/typescript-4/dist/errors.d.ts +2 -0
- package/dist/typescript-4/dist/registries/abstract.d.ts +2 -2
- package/dist/typescript-4/dist/registries/confluent-schema-registry.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -419,7 +419,13 @@ export class Base extends TypedEventEmitter {
|
|
|
419
419
|
retryCallback(error);
|
|
420
420
|
return;
|
|
421
421
|
}
|
|
422
|
-
|
|
422
|
+
// includeTopicAuthorizedOperations: false. The response's
|
|
423
|
+
// topic_authorized_operations bitfield is parsed but never read
|
|
424
|
+
// by the producer/consumer, and requesting it makes IAM brokers
|
|
425
|
+
// (e.g. MSK) authorize every topic operation and emit a CloudTrail
|
|
426
|
+
// AccessDenied per denied op (CreateTopic/DeleteTopic/...) — pure
|
|
427
|
+
// audit-log noise for permissions the client never exercises.
|
|
428
|
+
api(connection, topicsToFetch, autocreateTopics, false, retryCallback);
|
|
423
429
|
});
|
|
424
430
|
}, attempt);
|
|
425
431
|
}, (error, metadata) => {
|
|
@@ -455,6 +461,7 @@ export class Base extends TypedEventEmitter {
|
|
|
455
461
|
}
|
|
456
462
|
else {
|
|
457
463
|
this.#metadata.lastUpdate = lastUpdate;
|
|
464
|
+
this.#metadata.controllerId = metadata.controllerId;
|
|
458
465
|
}
|
|
459
466
|
const brokers = new Map();
|
|
460
467
|
// This should never change, but we act defensively here
|
|
@@ -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";
|
|
@@ -15,7 +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
|
+
import { nextFetchEpoch, partitionKey } from "./utils.js";
|
|
19
19
|
export class Consumer extends Base {
|
|
20
20
|
groupId;
|
|
21
21
|
groupInstanceId;
|
|
@@ -36,6 +36,7 @@ export class Consumer extends Base {
|
|
|
36
36
|
#memberEpoch;
|
|
37
37
|
#groupRemoteAssignor;
|
|
38
38
|
#clientRack;
|
|
39
|
+
#fetchSessions;
|
|
39
40
|
#preferredReadReplicas;
|
|
40
41
|
#streams;
|
|
41
42
|
#lagMonitoring;
|
|
@@ -82,6 +83,7 @@ export class Consumer extends Base {
|
|
|
82
83
|
this.#useConsumerGroupProtocol = this[kOptions].groupProtocol === 'consumer';
|
|
83
84
|
this.#groupRemoteAssignor = this[kOptions].groupRemoteAssignor ?? null;
|
|
84
85
|
this.#clientRack = this[kOptions].clientRack ?? '';
|
|
86
|
+
this.#fetchSessions = new Map();
|
|
85
87
|
this.#preferredReadReplicas = new Map();
|
|
86
88
|
this.#streamContext = options.streamContext ?? options.context;
|
|
87
89
|
this.#validateGroupOptions(this[kOptions], groupIdAndOptionsValidator);
|
|
@@ -144,6 +146,7 @@ export class Consumer extends Base {
|
|
|
144
146
|
return;
|
|
145
147
|
}
|
|
146
148
|
this.topics.clear();
|
|
149
|
+
this.#fetchSessions.clear();
|
|
147
150
|
if (this[kPrometheus]) {
|
|
148
151
|
ensureMetric(this[kPrometheus], 'Gauge', 'kafka_consumers', 'Number of active Kafka consumers').dec();
|
|
149
152
|
}
|
|
@@ -492,6 +495,7 @@ export class Consumer extends Base {
|
|
|
492
495
|
pool.get(broker, (error, connection) => {
|
|
493
496
|
if (error) {
|
|
494
497
|
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
498
|
+
this.#fetchSessions.delete(node);
|
|
495
499
|
// When a connection was not available (either interrupted or not available) we
|
|
496
500
|
// reset the leader epoch in the options so that when connection is re-established again we can continue
|
|
497
501
|
for (const topic of options.topics) {
|
|
@@ -508,11 +512,74 @@ export class Consumer extends Base {
|
|
|
508
512
|
retryCallback(error);
|
|
509
513
|
return;
|
|
510
514
|
}
|
|
511
|
-
|
|
515
|
+
let session = this.#fetchSessions.get(node);
|
|
516
|
+
if (session && session.pool !== pool) {
|
|
517
|
+
session = undefined;
|
|
518
|
+
}
|
|
519
|
+
const sessionId = session?.id ?? 0;
|
|
520
|
+
const sessionEpoch = session?.epoch ?? 0;
|
|
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) => {
|
|
512
573
|
if (error) {
|
|
513
574
|
this.#clearPreferredReadReplicas(options.topics, topicIds);
|
|
514
575
|
const genericError = error;
|
|
576
|
+
if (genericError.findBy?.('apiId', 'FETCH_SESSION_ID_NOT_FOUND') ||
|
|
577
|
+
genericError.findBy?.('apiId', 'INVALID_FETCH_SESSION_EPOCH') ||
|
|
578
|
+
genericError.findBy?.('apiId', 'FETCH_SESSION_TOPIC_ID_ERROR')) {
|
|
579
|
+
this.#fetchSessions.delete(node);
|
|
580
|
+
}
|
|
515
581
|
if (genericError.findBy?.('apiId', 'FENCED_LEADER_EPOCH')) {
|
|
582
|
+
this.#fetchSessions.delete(node);
|
|
516
583
|
this.clearMetadata();
|
|
517
584
|
for (const topic of options.topics) {
|
|
518
585
|
for (const partition of topic.partitions) {
|
|
@@ -524,6 +591,7 @@ export class Consumer extends Base {
|
|
|
524
591
|
}
|
|
525
592
|
else {
|
|
526
593
|
this.#updatePreferredReadReplicas(metadata, topicIds, result);
|
|
594
|
+
this.#updateFetchSession(node, pool, sessionEpoch, result.sessionId, desired);
|
|
527
595
|
}
|
|
528
596
|
retryCallback(error, result);
|
|
529
597
|
});
|
|
@@ -542,6 +610,20 @@ export class Consumer extends Base {
|
|
|
542
610
|
callback(error, response);
|
|
543
611
|
}, 0);
|
|
544
612
|
}
|
|
613
|
+
#updateFetchSession(node, pool, sentEpoch, responseSessionId, desired) {
|
|
614
|
+
if (!responseSessionId) {
|
|
615
|
+
this.#fetchSessions.delete(node);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
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
|
+
});
|
|
626
|
+
}
|
|
545
627
|
#commit(options, callback, rejoinAttempts = 0) {
|
|
546
628
|
this.#performGroupOperation('commit', (connection, groupCallback) => {
|
|
547
629
|
const topics = new Map();
|
|
@@ -1209,11 +1291,17 @@ export class Consumer extends Base {
|
|
|
1209
1291
|
topicSubscriptions.push(subscription);
|
|
1210
1292
|
}
|
|
1211
1293
|
}
|
|
1212
|
-
|
|
1294
|
+
const subscribedTopics = Array.from(topicsSubscriptions.keys());
|
|
1295
|
+
this[kMetadata]({ topics: subscribedTopics }, (error, metadata) => {
|
|
1213
1296
|
if (error) {
|
|
1214
1297
|
callback(this.#handleError(error));
|
|
1215
1298
|
return;
|
|
1216
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
|
+
}
|
|
1217
1305
|
this.#performSyncGroup(partitionsAssigner, this.#createAssignments(partitionsAssigner, metadata), callback);
|
|
1218
1306
|
});
|
|
1219
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);
|
|
@@ -685,9 +691,16 @@ export class Producer extends Base {
|
|
|
685
691
|
}
|
|
686
692
|
// Track nodes so that we can get their ID for delayed write reporting
|
|
687
693
|
const nodes = [];
|
|
694
|
+
const sequenceSnapshot = produceOptions.sequences ? new NumericMap() : undefined;
|
|
695
|
+
if (sequenceSnapshot) {
|
|
696
|
+
for (const [partitionKey, messagesCount] of messagesPerPartition) {
|
|
697
|
+
sequenceSnapshot.set(partitionKey, this.#sequences.postIncrement(partitionKey, messagesCount, 0));
|
|
698
|
+
}
|
|
699
|
+
produceOptions = { ...produceOptions, sequences: sequenceSnapshot };
|
|
700
|
+
}
|
|
688
701
|
runConcurrentCallbacks('Producing messages failed.', messagesByDestination, ([destination, destinationMessages], concurrentCallback) => {
|
|
689
702
|
nodes.push(destination);
|
|
690
|
-
this.#performSingleDestinationSend(topics, destinationMessages, this[kOptions].timeout, sendOptions.acks, sendOptions.autocreateTopics, sendOptions.repeatOnStaleMetadata, produceOptions, concurrentCallback);
|
|
703
|
+
this.#performSingleDestinationSend(topics, destinationMessages, this[kOptions].timeout, sendOptions.acks, sendOptions.autocreateTopics, sendOptions.repeatOnStaleMetadata, produceOptions, true, concurrentCallback);
|
|
691
704
|
}, (error, apiResults) => {
|
|
692
705
|
this.#metricsProducedMessages?.inc(messages.length);
|
|
693
706
|
const results = {};
|
|
@@ -710,14 +723,16 @@ export class Producer extends Base {
|
|
|
710
723
|
partition: partitionResponse.index,
|
|
711
724
|
offset: partitionResponse.baseOffset
|
|
712
725
|
});
|
|
713
|
-
const partitionKey = `${name}:${partitionResponse.index}`;
|
|
714
|
-
this.#sequences.postIncrement(partitionKey, messagesPerPartition.get(partitionKey), 0);
|
|
715
726
|
}
|
|
716
727
|
}
|
|
717
728
|
results.offsets = topics;
|
|
718
729
|
}
|
|
719
730
|
}
|
|
720
731
|
if (error) {
|
|
732
|
+
if (sendOptions.idempotent && !this.#transaction) {
|
|
733
|
+
this.#producerInfo = undefined;
|
|
734
|
+
this.#sequences.clear();
|
|
735
|
+
}
|
|
721
736
|
;
|
|
722
737
|
error.produced = results;
|
|
723
738
|
callback(error);
|
|
@@ -728,7 +743,7 @@ export class Producer extends Base {
|
|
|
728
743
|
});
|
|
729
744
|
});
|
|
730
745
|
}
|
|
731
|
-
#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, repeatOnStaleMetadata, produceOptions, callback) {
|
|
746
|
+
#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, repeatOnStaleMetadata, produceOptions, recoverOnProducerStateLoss, callback) {
|
|
732
747
|
// Get the metadata with the topic/partitions informations
|
|
733
748
|
this[kMetadata]({ topics, autocreateTopics }, (error, metadata) => {
|
|
734
749
|
if (error) {
|
|
@@ -762,7 +777,17 @@ export class Producer extends Base {
|
|
|
762
777
|
const hasStaleMetadata = kafkaError?.findBy('hasStaleMetadata', true);
|
|
763
778
|
if (hasStaleMetadata && repeatOnStaleMetadata) {
|
|
764
779
|
this.clearMetadata();
|
|
765
|
-
this.#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, false, produceOptions, callback);
|
|
780
|
+
this.#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, false, produceOptions, recoverOnProducerStateLoss, callback);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (recoverOnProducerStateLoss && !this.#transaction && this.#isProducerStateLost(error)) {
|
|
784
|
+
this.#recoverIdempotentProducer(messages, produceOptions, (error, recoveredProduceOptions) => {
|
|
785
|
+
if (error) {
|
|
786
|
+
callback(error);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
this.#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, repeatOnStaleMetadata, recoveredProduceOptions, false, callback);
|
|
790
|
+
});
|
|
766
791
|
return;
|
|
767
792
|
}
|
|
768
793
|
callback(error, results);
|
|
@@ -778,6 +803,35 @@ export class Producer extends Base {
|
|
|
778
803
|
});
|
|
779
804
|
});
|
|
780
805
|
}
|
|
806
|
+
#isProducerStateLost(error) {
|
|
807
|
+
const kafkaError = error;
|
|
808
|
+
return !!(GenericError.isGenericError(error) &&
|
|
809
|
+
(kafkaError.findBy('apiId', 'UNKNOWN_PRODUCER_ID') || kafkaError.findBy('apiId', 'OUT_OF_ORDER_SEQUENCE_NUMBER')));
|
|
810
|
+
}
|
|
811
|
+
#recoverIdempotentProducer(messages, produceOptions, callback) {
|
|
812
|
+
this.#producerInfo = undefined;
|
|
813
|
+
this.#sequences.clear();
|
|
814
|
+
this.initIdempotentProducer({}, (error, producerInfo) => {
|
|
815
|
+
if (error) {
|
|
816
|
+
callback(error);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const sequences = new NumericMap();
|
|
820
|
+
const messagesPerPartition = new NumericMap();
|
|
821
|
+
for (const { topic, partition } of messages) {
|
|
822
|
+
messagesPerPartition.postIncrement(`${topic}:${partition}`, 1, 0);
|
|
823
|
+
}
|
|
824
|
+
for (const [partitionKey, messagesCount] of messagesPerPartition) {
|
|
825
|
+
sequences.set(partitionKey, this.#sequences.postIncrement(partitionKey, messagesCount, 0));
|
|
826
|
+
}
|
|
827
|
+
callback(null, {
|
|
828
|
+
...produceOptions,
|
|
829
|
+
producerId: producerInfo.producerId,
|
|
830
|
+
producerEpoch: producerInfo.producerEpoch,
|
|
831
|
+
sequences
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
}
|
|
781
835
|
#getCoordinatorConnection(callback) {
|
|
782
836
|
if (this.#coordinatorId === undefined) {
|
|
783
837
|
this[kTransactionFindCoordinator](error => {
|
package/dist/clients/serde.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface Deserializers<Key, Value, HeaderKey, HeaderValue> {
|
|
|
17
17
|
headerValue: Deserializer<HeaderValue>;
|
|
18
18
|
}
|
|
19
19
|
export type BeforeHookPayloadType = 'key' | 'value' | 'headerKey' | 'headerValue';
|
|
20
|
-
export type BeforeDeserializationHook = (payload: Buffer, type: BeforeHookPayloadType, message: MessageToConsume, callback: Callback<void>) => void | Promise<void>;
|
|
20
|
+
export type BeforeDeserializationHook = (payload: Buffer | null, type: BeforeHookPayloadType, message: MessageToConsume, callback: Callback<void>) => void | Promise<void>;
|
|
21
21
|
export type BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue> = (payload: unknown, type: BeforeHookPayloadType, message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>, callback: Callback<void>) => void | Promise<void>;
|
|
22
22
|
export declare function stringSerializer(data?: string): Buffer | undefined;
|
|
23
23
|
export declare function stringDeserializer(data?: string | Buffer): string | undefined;
|
package/dist/errors.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class GenericError extends Error {
|
|
|
14
14
|
static isGenericError(error: Error): error is GenericError | MultipleErrors;
|
|
15
15
|
constructor(code: ErrorCode, message: string, { cause, ...rest }?: ErrorProperties);
|
|
16
16
|
findBy<ErrorType extends GenericError = GenericError>(property: string, value: unknown): ErrorType | null;
|
|
17
|
+
toString(): string;
|
|
17
18
|
}
|
|
18
19
|
export declare class MultipleErrors extends AggregateError {
|
|
19
20
|
code: string;
|
|
@@ -25,6 +26,7 @@ export declare class MultipleErrors extends AggregateError {
|
|
|
25
26
|
static isMultipleErrors(error: Error): error is MultipleErrors;
|
|
26
27
|
constructor(message: string, errors: Error[], { cause, ...rest }?: ErrorProperties);
|
|
27
28
|
findBy<ErrorType extends GenericError | MultipleErrors = MultipleErrors>(property: string, value: unknown): ErrorType | null;
|
|
29
|
+
toString(): string;
|
|
28
30
|
}
|
|
29
31
|
export * from './protocol/errors.ts';
|
|
30
32
|
export declare class AuthenticationError extends GenericError {
|
package/dist/errors.js
CHANGED
|
@@ -45,6 +45,9 @@ export class GenericError extends Error {
|
|
|
45
45
|
}
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
|
+
toString() {
|
|
49
|
+
return `Error ${this.code}: ${this.message}`;
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
export class MultipleErrors extends AggregateError {
|
|
50
53
|
code;
|
|
@@ -91,6 +94,9 @@ export class MultipleErrors extends AggregateError {
|
|
|
91
94
|
}
|
|
92
95
|
return null;
|
|
93
96
|
}
|
|
97
|
+
toString() {
|
|
98
|
+
return `Error ${this.code}: ${this.message} (caused by: ${this.errors.map(error => error.toString()).join(', ')})`;
|
|
99
|
+
}
|
|
94
100
|
}
|
|
95
101
|
export * from "./protocol/errors.js";
|
|
96
102
|
export class AuthenticationError extends GenericError {
|
|
@@ -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/protocol/reader.js
CHANGED
|
@@ -302,9 +302,10 @@ export class Reader {
|
|
|
302
302
|
}
|
|
303
303
|
// TODO: Tagged fields are not supported yet
|
|
304
304
|
readTaggedFields() {
|
|
305
|
-
const length = this.
|
|
306
|
-
|
|
307
|
-
this.
|
|
305
|
+
const length = this.readUnsignedVarInt();
|
|
306
|
+
for (let i = 0; i < length; i++) {
|
|
307
|
+
this.readUnsignedVarInt();
|
|
308
|
+
this.skip(this.readUnsignedVarInt());
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
}
|
|
@@ -4,7 +4,7 @@ import { type MessageToProduce } from '../protocol/records.ts';
|
|
|
4
4
|
export interface SchemaRegistry<Id = unknown, Schema = unknown, Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> {
|
|
5
5
|
get(id: Id): Schema | undefined;
|
|
6
6
|
fetch(id: Id, callback?: (error?: Error) => void): void | Promise<void>;
|
|
7
|
-
getSchemaId(payload: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): Id;
|
|
7
|
+
getSchemaId(payload: Buffer | null | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): Id;
|
|
8
8
|
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
|
9
9
|
getDeserializers(): Deserializers<Key, Value, HeaderKey, HeaderValue>;
|
|
10
10
|
getBeforeSerializationHook(): BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue>;
|
|
@@ -14,7 +14,7 @@ export declare function runAsyncSeries<V>(operation: (item: V, cb: Callback<void
|
|
|
14
14
|
export declare class AbstractSchemaRegistry<Id = unknown, Schema = unknown, Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> implements SchemaRegistry<Id, Schema, Key, Value, HeaderKey, HeaderValue> {
|
|
15
15
|
get(_: Id): Schema | undefined;
|
|
16
16
|
fetch(_i: unknown, _c?: (error?: Error) => void): void | Promise<void>;
|
|
17
|
-
getSchemaId(_p: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, _t?: BeforeHookPayloadType): Id;
|
|
17
|
+
getSchemaId(_p: Buffer | null | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, _t?: BeforeHookPayloadType): Id;
|
|
18
18
|
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
|
19
19
|
getDeserializers(): Deserializers<Key, Value, HeaderKey, HeaderValue>;
|
|
20
20
|
getBeforeSerializationHook(): BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue>;
|
|
@@ -30,7 +30,7 @@ export declare function defaultProtobufTypeMapper(_: number, type: BeforeHookPay
|
|
|
30
30
|
export declare class ConfluentSchemaRegistry<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends AbstractSchemaRegistry<number | undefined, Schema, Key, Value, HeaderKey, HeaderValue> {
|
|
31
31
|
#private;
|
|
32
32
|
constructor(options: ConfluentSchemaRegistryOptions);
|
|
33
|
-
getSchemaId(message: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): number | undefined;
|
|
33
|
+
getSchemaId(message: Buffer | null | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): number | undefined;
|
|
34
34
|
get(id: number): Schema | undefined;
|
|
35
35
|
fetchSchema(id: number, callback: Callback<void>): Promise<void>;
|
|
36
36
|
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
|
@@ -27,6 +27,9 @@ export class ConfluentSchemaRegistry extends AbstractSchemaRegistry {
|
|
|
27
27
|
this.#auth = options.auth;
|
|
28
28
|
}
|
|
29
29
|
getSchemaId(message, type) {
|
|
30
|
+
if (message === null) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
30
33
|
if (Buffer.isBuffer(message)) {
|
|
31
34
|
if (type !== 'value') {
|
|
32
35
|
return undefined;
|
|
@@ -6,6 +6,4 @@ export type CallbackWithPromise<ReturnType> = Callback<ReturnType> & {
|
|
|
6
6
|
[kCallbackPromise]?: Promise<ReturnType>;
|
|
7
7
|
};
|
|
8
8
|
export declare function createPromisifiedCallback<ReturnType>(): CallbackWithPromise<ReturnType>;
|
|
9
|
-
export declare function runConcurrentCallbacks<
|
|
10
|
-
export declare function runConcurrentCallbacks<ReturnType, V>(errorMessage: string, collection: Set<V>, operation: (item: V, cb: Callback<ReturnType>) => void, callback: Callback<ReturnType[]>): void;
|
|
11
|
-
export declare function runConcurrentCallbacks<ReturnType, V>(errorMessage: string, collection: V[], operation: (item: V, cb: Callback<ReturnType>) => void, callback: Callback<ReturnType[]>): void;
|
|
9
|
+
export declare function runConcurrentCallbacks<R, V>(errorMessage: string, collection: Iterable<V>, operation: (item: V, cb: Callback<R>) => void, callback: Callback<R[]>): void;
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare function partitionKey(topic: string, partition: number): string;
|
|
1
|
+
export declare function partitionKey(topic: string, partition: number): string;
|
|
2
|
+
export declare function nextFetchEpoch(epoch: number): number;
|
|
@@ -17,7 +17,7 @@ export interface Deserializers<Key, Value, HeaderKey, HeaderValue> {
|
|
|
17
17
|
headerValue: Deserializer<HeaderValue>;
|
|
18
18
|
}
|
|
19
19
|
export type BeforeHookPayloadType = 'key' | 'value' | 'headerKey' | 'headerValue';
|
|
20
|
-
export type BeforeDeserializationHook = (payload: Buffer, type: BeforeHookPayloadType, message: MessageToConsume, callback: Callback<void>) => void | Promise<void>;
|
|
20
|
+
export type BeforeDeserializationHook = (payload: Buffer | null, type: BeforeHookPayloadType, message: MessageToConsume, callback: Callback<void>) => void | Promise<void>;
|
|
21
21
|
export type BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue> = (payload: unknown, type: BeforeHookPayloadType, message: MessageToProduce<Key, Value, HeaderKey, HeaderValue>, callback: Callback<void>) => void | Promise<void>;
|
|
22
22
|
export declare function stringSerializer(data?: string): Buffer | undefined;
|
|
23
23
|
export declare function stringDeserializer(data?: string | Buffer): string | undefined;
|
|
@@ -17,6 +17,7 @@ export declare class GenericError extends Error {
|
|
|
17
17
|
...rest
|
|
18
18
|
}?: ErrorProperties);
|
|
19
19
|
findBy<ErrorType extends GenericError = GenericError>(property: string, value: unknown): ErrorType | null;
|
|
20
|
+
toString(): string;
|
|
20
21
|
}
|
|
21
22
|
export declare class MultipleErrors extends AggregateError {
|
|
22
23
|
code: string;
|
|
@@ -31,6 +32,7 @@ export declare class MultipleErrors extends AggregateError {
|
|
|
31
32
|
...rest
|
|
32
33
|
}?: ErrorProperties);
|
|
33
34
|
findBy<ErrorType extends GenericError | MultipleErrors = MultipleErrors>(property: string, value: unknown): ErrorType | null;
|
|
35
|
+
toString(): string;
|
|
34
36
|
}
|
|
35
37
|
export * from "./protocol/errors";
|
|
36
38
|
export declare class AuthenticationError extends GenericError {
|
|
@@ -4,7 +4,7 @@ import { type MessageToProduce } from "../protocol/records";
|
|
|
4
4
|
export interface SchemaRegistry<Id = unknown, Schema = unknown, Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> {
|
|
5
5
|
get(id: Id): Schema | undefined;
|
|
6
6
|
fetch(id: Id, callback?: (error?: Error) => void): void | Promise<void>;
|
|
7
|
-
getSchemaId(payload: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): Id;
|
|
7
|
+
getSchemaId(payload: Buffer | null | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): Id;
|
|
8
8
|
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
|
9
9
|
getDeserializers(): Deserializers<Key, Value, HeaderKey, HeaderValue>;
|
|
10
10
|
getBeforeSerializationHook(): BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue>;
|
|
@@ -14,7 +14,7 @@ export declare function runAsyncSeries<V>(operation: (item: V, cb: Callback<void
|
|
|
14
14
|
export declare class AbstractSchemaRegistry<Id = unknown, Schema = unknown, Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> implements SchemaRegistry<Id, Schema, Key, Value, HeaderKey, HeaderValue> {
|
|
15
15
|
get(_: Id): Schema | undefined;
|
|
16
16
|
fetch(_i: unknown, _c?: (error?: Error) => void): void | Promise<void>;
|
|
17
|
-
getSchemaId(_p: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, _t?: BeforeHookPayloadType): Id;
|
|
17
|
+
getSchemaId(_p: Buffer | null | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, _t?: BeforeHookPayloadType): Id;
|
|
18
18
|
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
|
19
19
|
getDeserializers(): Deserializers<Key, Value, HeaderKey, HeaderValue>;
|
|
20
20
|
getBeforeSerializationHook(): BeforeSerializationHook<Key, Value, HeaderKey, HeaderValue>;
|
|
@@ -30,7 +30,7 @@ export declare function defaultProtobufTypeMapper(_: number, type: BeforeHookPay
|
|
|
30
30
|
export declare class ConfluentSchemaRegistry<Key = Buffer, Value = Buffer, HeaderKey = Buffer, HeaderValue = Buffer> extends AbstractSchemaRegistry<number | undefined, Schema, Key, Value, HeaderKey, HeaderValue> {
|
|
31
31
|
#private;
|
|
32
32
|
constructor(options: ConfluentSchemaRegistryOptions);
|
|
33
|
-
getSchemaId(message: Buffer | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): number | undefined;
|
|
33
|
+
getSchemaId(message: Buffer | null | MessageToProduce<Key, Value, HeaderKey, HeaderValue>, type?: BeforeHookPayloadType): number | undefined;
|
|
34
34
|
get(id: number): Schema | undefined;
|
|
35
35
|
fetchSchema(id: number, callback: Callback<void>): Promise<void>;
|
|
36
36
|
getSerializers(): Serializers<Key, Value, HeaderKey, HeaderValue>;
|
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.3.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "2.
|
|
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)",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"typecheck": "tsc -p . --noEmit",
|
|
92
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
|
-
"test:ci": "
|
|
94
|
+
"test:ci": "./scripts/node --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=90 --test-coverage-branches=90 --test-coverage-functions=90 --test-reporter=cleaner-spec-reporter 'test/*.test.ts' 'test/*/*.test.ts' 'test/*/*/*.test.ts'",
|
|
95
95
|
"test:integrity": "./scripts/node --test --test-reporter=cleaner-spec-reporter 'regression/integrity/*.test.ts'",
|
|
96
96
|
"test:performance": "./scripts/node --test --test-reporter=cleaner-spec-reporter 'regression/performance/*.test.ts'",
|
|
97
97
|
"test:regression": "npm run test:integrity",
|