@platformatic/kafka 1.33.0 → 1.34.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.
@@ -39,6 +39,7 @@ export function parseResponse(_correlationId, apiKey, apiVersion, reader) {
39
39
  const throttleTimeMs = reader.readInt32();
40
40
  const errorCode = reader.readInt16();
41
41
  const errorMessage = reader.readNullableString();
42
+ /* c8 ignore next 3 - Hard to test */
42
43
  if (errorCode !== 0) {
43
44
  errors.push(['', [errorCode, errorMessage]]);
44
45
  }
@@ -5,7 +5,7 @@ import { type CallbackWithPromise } from '../../apis/callbacks.ts';
5
5
  import { type Callback } from '../../apis/definitions.ts';
6
6
  import { type Acl } from '../../apis/types.ts';
7
7
  import { Base } from '../base/base.ts';
8
- import { type AdminListOffsetsOptions, type AdminOptions, type AlterClientQuotasOptions, type AlterConfigsOptions, type AlterConsumerGroupOffsetsOptions, type BrokerLogDirDescription, type ConfigDescription, type CreateAclsOptions, type CreatedTopic, type CreatePartitionsOptions, type CreateTopicsOptions, type DeleteAclsOptions, type DeleteConsumerGroupOffsetsOptions, type DeleteRecordsOptions, type DeleteGroupsOptions, type DeleteTopicsOptions, type DeletedRecordsTopic, type DescribeAclsOptions, type DescribeClientQuotasOptions, type DescribeConfigsOptions, type DescribeGroupsOptions, type DescribeLogDirsOptions, type FindCoordinatorOptions, type FindCoordinatorResult, type Group, type GroupBase, type IncrementalAlterConfigsOptions, type ListConsumerGroupOffsetsGroup, type ListConsumerGroupOffsetsOptions, type ListedOffsetsTopic, type ListGroupsOptions, type ListTopicsOptions, type RemoveMembersFromConsumerGroupOptions } from './types.ts';
8
+ import { type AdminListOffsetsOptions, type AdminOptions, type AlterClientQuotasOptions, type AlterConfigsOptions, type AlterConsumerGroupOffsetsOptions, type BrokerLogDirDescription, type ConfigDescription, type CreateAclsOptions, type CreatedTopic, type CreatePartitionsOptions, type CreateTopicsOptions, type DeleteAclsOptions, type DeleteConsumerGroupOffsetsOptions, type DeletedRecordsTopic, type DeleteGroupsOptions, type DeleteRecordsOptions, type DeleteTopicsOptions, type DescribeAclsOptions, type DescribeClientQuotasOptions, type DescribeConfigsOptions, type DescribeGroupsOptions, type DescribeLogDirsOptions, type FindCoordinatorOptions, type FindCoordinatorResult, type Group, type GroupBase, type IncrementalAlterConfigsOptions, type ListConsumerGroupOffsetsGroup, type ListConsumerGroupOffsetsOptions, type ListedOffsetsTopic, type ListGroupsOptions, type ListTopicsOptions, type RemoveMembersFromConsumerGroupOptions } from './types.ts';
9
9
  export declare class Admin extends Base<AdminOptions> {
10
10
  #private;
11
11
  constructor(options: AdminOptions);
@@ -4,7 +4,7 @@ import { adminAclsChannel, adminClientQuotasChannel, adminConfigsChannel, adminC
4
4
  import { MultipleErrors, UserError } from "../../errors.js";
5
5
  import { Reader } from "../../protocol/reader.js";
6
6
  import { Base, kAfterCreate, kCheckNotClosed, kConnections, kGetApi, kGetBootstrapConnection, kGetConnection, kMetadata, kOptions, kPerformDeduplicated, kPerformWithRetry, kValidateOptions } from "../base/base.js";
7
- import { adminListOffsetsOptionsValidator, alterClientQuotasOptionsValidator, alterConfigsOptionsValidator, alterConsumerGroupOffsetsOptionsValidator, createAclsOptionsValidator, createPartitionsOptionsValidator, createTopicsOptionsValidator, deleteAclsOptionsValidator, deleteConsumerGroupOffsetsOptionsValidator, deleteRecordsOptionsValidator, deleteGroupsOptionsValidator, deleteTopicsOptionsValidator, describeAclsOptionsValidator, describeClientQuotasOptionsValidator, describeConfigsOptionsValidator, describeGroupsOptionsValidator, describeLogDirsOptionsValidator, findCoordinatorOptionsValidator, incrementalAlterConfigsOptionsValidator, listConsumerGroupOffsetsOptionsValidator, listGroupsOptionsValidator, listTopicsOptionsValidator, removeMembersFromConsumerGroupOptionsValidator } from "./options.js";
7
+ import { adminListOffsetsOptionsValidator, alterClientQuotasOptionsValidator, alterConfigsOptionsValidator, alterConsumerGroupOffsetsOptionsValidator, createAclsOptionsValidator, createPartitionsOptionsValidator, createTopicsOptionsValidator, deleteAclsOptionsValidator, deleteConsumerGroupOffsetsOptionsValidator, deleteGroupsOptionsValidator, deleteRecordsOptionsValidator, deleteTopicsOptionsValidator, describeAclsOptionsValidator, describeClientQuotasOptionsValidator, describeConfigsOptionsValidator, describeGroupsOptionsValidator, describeLogDirsOptionsValidator, findCoordinatorOptionsValidator, incrementalAlterConfigsOptionsValidator, listConsumerGroupOffsetsOptionsValidator, listGroupsOptionsValidator, listTopicsOptionsValidator, removeMembersFromConsumerGroupOptionsValidator } from "./options.js";
8
8
  export class Admin extends Base {
9
9
  #controller = null;
10
10
  constructor(options) {
@@ -1391,6 +1391,7 @@ export class Admin extends Base {
1391
1391
  // topics.get(topic.name) must be defined as the metadata request was successful
1392
1392
  const topicData = topics.get(topic.name);
1393
1393
  const targetPartitionData = topicData.partitions[partition.partitionIndex];
1394
+ /* c8 ignore next 4 - Hard to test */
1394
1395
  if (!targetPartitionData) {
1395
1396
  callback(new UserError(`Unknown partition ${partition.partitionIndex} for topic ${topic.name}.`));
1396
1397
  return;
@@ -66,6 +66,7 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
66
66
  get closed(): boolean;
67
67
  get type(): ClientType;
68
68
  get context(): unknown;
69
+ get connections(): ConnectionPool;
69
70
  emitWithDebug(section: string | null, name: string, ...args: any[]): boolean;
70
71
  close(callback: CallbackWithPromise<void>): void;
71
72
  close(): Promise<void>;
@@ -90,6 +90,9 @@ export class Base extends TypedEventEmitter {
90
90
  get context() {
91
91
  return this[kContext];
92
92
  }
93
+ get connections() {
94
+ return this[kConnections];
95
+ }
93
96
  emitWithDebug(section, name, ...args) {
94
97
  if (!section) {
95
98
  return this.emit(name, ...args);
@@ -254,14 +257,19 @@ export class Base extends TypedEventEmitter {
254
257
  this.once('client:close', onClose);
255
258
  }
256
259
  else {
257
- if (attempt === 0) {
258
- callback(error);
259
- return;
260
+ if (attempt > 0) {
261
+ error = new MultipleErrors(`${operationId} failed ${attempt + 1} times.`, errors);
260
262
  }
261
- callback(new MultipleErrors(`${operationId} failed ${attempt + 1} times.`, errors));
263
+ this.emitWithDebug('client', 'performWithRetry:failed', operationId, retries, errors);
264
+ callback(error);
265
+ return;
262
266
  }
263
267
  return;
264
268
  }
269
+ // Make sure all previous errors are deleted
270
+ if (errors.length > 0) {
271
+ errors.splice(0, errors.length);
272
+ }
265
273
  callback(null, result);
266
274
  });
267
275
  return callback[kCallbackPromise];
@@ -398,6 +406,7 @@ export class Base extends TypedEventEmitter {
398
406
  const unknownTopicError = error.findBy('apiCode', 3);
399
407
  if (unknownTopicError) {
400
408
  const topicIndexMatch = unknownTopicError.path?.match(/\/topics\/(\d+)/);
409
+ /* c8 ignore next 3 - Hard to test */
401
410
  const topicIndex = topicIndexMatch ? parseInt(topicIndexMatch[1]) : -1;
402
411
  const topicName = topicIndex >= 0 && topicIndex < topicsToFetch.length ? topicsToFetch[topicIndex] : 'unknown';
403
412
  deduplicateCallback(new UserError(`Unknown topic ${topicName}.`));
@@ -405,6 +414,7 @@ export class Base extends TypedEventEmitter {
405
414
  }
406
415
  const hasStaleMetadata = error.findBy('hasStaleMetadata', true);
407
416
  // Stale metadata, we need to fetch everything again
417
+ /* c8 ignore next 4 - Hard to test */
408
418
  if (hasStaleMetadata) {
409
419
  this.clearMetadata();
410
420
  topicsToFetch = topics;
@@ -483,7 +483,7 @@ export class Consumer extends Base {
483
483
  callback(error, response);
484
484
  }, 0);
485
485
  }
486
- #commit(options, callback) {
486
+ #commit(options, callback, rejoinAttempts = 0) {
487
487
  this.#performGroupOperation('commit', (connection, groupCallback) => {
488
488
  const topics = new Map();
489
489
  for (const { topic, partition, offset, leaderEpoch } of options.offsets) {
@@ -507,6 +507,16 @@ export class Consumer extends Base {
507
507
  api(connection, this.groupId, this.#useConsumerGroupProtocol ? this.#memberEpoch : this.generationId, this.memberId, null, Array.from(topics.values()), groupCallback);
508
508
  });
509
509
  }, error => {
510
+ if (error && rejoinAttempts < this[kOptions].retries && this.#getRejoinError(error)) {
511
+ this.joinGroup({}, joinError => {
512
+ if (joinError) {
513
+ callback(joinError);
514
+ return;
515
+ }
516
+ this.#commit(options, callback, rejoinAttempts + 1);
517
+ });
518
+ return;
519
+ }
510
520
  callback(error);
511
521
  });
512
522
  }
@@ -19,6 +19,7 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
19
19
  get offsetsToCommit(): Map<string, CommitOptionsPartition>;
20
20
  get offsetsCommitted(): Map<string, bigint>;
21
21
  get committedOffsets(): Map<string, bigint>;
22
+ get connections(): ConnectionPool;
22
23
  close(callback: CallbackWithPromise<void>): void;
23
24
  close(): Promise<void>;
24
25
  isActive(): boolean;
@@ -61,6 +61,8 @@ export class MessagesStream extends Readable {
61
61
  #metricsConsumedMessages;
62
62
  #corruptedMessageHandler;
63
63
  #context;
64
+ #onConsumerGroupJoin;
65
+ #onBrokerDisconnect;
64
66
  #pushRecordsOperation;
65
67
  [kInstance];
66
68
  /*
@@ -122,6 +124,14 @@ export class MessagesStream extends Readable {
122
124
  this.#closeCallbacks = [];
123
125
  this.#corruptedMessageHandler = onCorruptedMessage ?? defaultCorruptedMessageHandler;
124
126
  this.#context = context;
127
+ this.#onConsumerGroupJoin = () => {
128
+ this.#offsetsCommitted.clear();
129
+ this.#partitionsEpochs.clear();
130
+ this.#scheduleRefreshOffsetsAndFetch();
131
+ };
132
+ this.#onBrokerDisconnect = () => {
133
+ this.#partitionsEpochs.clear();
134
+ };
125
135
  if (registry) {
126
136
  this.#pushRecordsOperation = this.#beforeDeserialization.bind(this, registry.getBeforeDeserializationHook());
127
137
  }
@@ -150,18 +160,12 @@ export class MessagesStream extends Readable {
150
160
  // When the consumer joins a group, we need to fetch again as the assignments
151
161
  // will have changed so we may have gone from last with no assignments to
152
162
  // having some.
153
- this.#consumer.on('consumer:group:join', () => {
154
- this.#offsetsCommitted.clear();
155
- this.#partitionsEpochs.clear();
156
- this.#scheduleRefreshOffsetsAndFetch();
157
- });
163
+ this.#consumer.on('consumer:group:join', this.#onConsumerGroupJoin);
158
164
  if (consumer[kPrometheus]) {
159
165
  this.#metricsConsumedMessages = ensureMetric(consumer[kPrometheus], 'Counter', 'kafka_consumed_messages', 'Number of consumed Kafka messages');
160
166
  }
161
167
  // Whenever the consumer loses a connection, reset all the partitions epochs
162
- consumer.on('client:broker:disconnect', () => {
163
- this.#partitionsEpochs.clear();
164
- });
168
+ consumer.on('client:broker:disconnect', this.#onBrokerDisconnect);
165
169
  notifyCreation('messages-stream', this);
166
170
  }
167
171
  /* c8 ignore next 3 - Simple getter */
@@ -188,6 +192,10 @@ export class MessagesStream extends Readable {
188
192
  get committedOffsets() {
189
193
  return this.#offsetsCommitted;
190
194
  }
195
+ /* c8 ignore next 3 - Simple getter */
196
+ get connections() {
197
+ return this[kConnections];
198
+ }
191
199
  close(callback) {
192
200
  if (!callback) {
193
201
  callback = createPromisifiedCallback();
@@ -304,6 +312,8 @@ export class MessagesStream extends Readable {
304
312
  if (this.#autocommitInterval) {
305
313
  clearInterval(this.#autocommitInterval);
306
314
  }
315
+ this.#consumer.removeListener('consumer:group:join', this.#onConsumerGroupJoin);
316
+ this.#consumer.removeListener('client:broker:disconnect', this.#onBrokerDisconnect);
307
317
  this[kConnections].close(closeError => {
308
318
  callback(closeError ?? error);
309
319
  });
@@ -589,7 +599,12 @@ export class MessagesStream extends Readable {
589
599
  this.#autocommitInflight = false;
590
600
  if (error) {
591
601
  this.emit('autocommit', error);
592
- this.destroy(error);
602
+ // Only destroy the stream when the broker requires a group rejoin
603
+ // (ILLEGAL_GENERATION, UNKNOWN_MEMBER_ID, REBALANCE_IN_PROGRESS).
604
+ // Transient coordinator errors must not tear down the consumption loop.
605
+ if (error.findBy?.('needsRejoin', true)) {
606
+ this.destroy(error);
607
+ }
593
608
  return;
594
609
  }
595
610
  for (const { topic, partition, offset } of offsets) {
@@ -1,8 +1,10 @@
1
1
  import { murmur2 } from "../../protocol/murmur2.js";
2
2
  const compatibilityMurmur2Mask = 0x7fffffff;
3
3
  export function defaultPartitioner(_, key) {
4
+ /* c8 ignore next - Hard to test */
4
5
  return Buffer.isBuffer(key) ? murmur2(key) >>> 0 : 0;
5
6
  }
6
7
  export function compatibilityPartitioner(_, key) {
8
+ /* c8 ignore next - Hard to test */
7
9
  return Buffer.isBuffer(key) ? murmur2(key) & compatibilityMurmur2Mask : 0;
8
10
  }
@@ -1,4 +1,5 @@
1
1
  import { type CallbackWithPromise } from '../../apis/callbacks.ts';
2
+ import { type Broker, type Connection } from '../../network/connection.ts';
2
3
  import { type Message } from '../../protocol/records.ts';
3
4
  import { kTransaction, kTransactionAddOffsets, kTransactionAddPartitions, kTransactionCancel, kTransactionCommitOffset, kTransactionEnd, kTransactionFindCoordinator } from '../../symbols.ts';
4
5
  import { Base } from '../base/base.ts';
@@ -27,6 +28,11 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
27
28
  asStream(options?: ProducerStreamOptions<Key, Value, HeaderKey, HeaderValue>): ProducerStream<Key, Value, HeaderKey, HeaderValue>;
28
29
  beginTransaction(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<Transaction<Key, Value, HeaderKey, HeaderValue>>): void;
29
30
  beginTransaction(options?: ProduceOptions<Key, Value, HeaderKey, HeaderValue>): Promise<Transaction<Key, Value, HeaderKey, HeaderValue>>;
31
+ getSendTopicPartitions(options: SendOptions<Key, Value, HeaderKey, HeaderValue>): Map<string, Set<number>>;
32
+ getSendBrokers(options: SendOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<Record<string, Broker>>): void;
33
+ getSendBrokers(options: SendOptions<Key, Value, HeaderKey, HeaderValue>): Promise<Record<string, Broker>>;
34
+ getSendConnections(options: SendOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<Record<string, Connection>>): void;
35
+ getSendConnections(options: SendOptions<Key, Value, HeaderKey, HeaderValue>): Promise<Record<string, Connection>>;
30
36
  [kTransactionFindCoordinator](callback: CallbackWithPromise<void>): void;
31
37
  [kTransactionAddPartitions](transactionId: number, topicsPartitions: Map<string, Set<number>>, callback: CallbackWithPromise<void>): void;
32
38
  [kTransactionAddOffsets](transactionId: number, groupId: string, callback: CallbackWithPromise<void>): void;
@@ -243,6 +243,98 @@ export class Producer extends Base {
243
243
  producerTransactionsChannel.traceCallback(this.#transaction[kTransactionPrepare], 2, createDiagnosticContext({ client: this, operation: 'begin' }), this.#transaction, this.#producerInfo?.transactionalId, options, callback);
244
244
  return callback[kCallbackPromise];
245
245
  }
246
+ getSendTopicPartitions(options) {
247
+ const topicsPartitions = new Map();
248
+ const partitioner = options.partitioner ?? this[kOptions].partitioner;
249
+ for (const message of options.messages) {
250
+ const topic = message.topic;
251
+ let key;
252
+ let headers = new Map();
253
+ try {
254
+ if (message.headers) {
255
+ headers =
256
+ message.headers instanceof Map
257
+ ? message.headers
258
+ : new Map(Object.entries(message.headers));
259
+ }
260
+ key = this.#keySerializer(message.key, headers, message);
261
+ }
262
+ catch (error) {
263
+ throw new UserError('Failed to serialize a message.', { cause: error });
264
+ }
265
+ const partition = this.#assignPartition(message, partitioner, key, topic);
266
+ let partitions = topicsPartitions.get(topic);
267
+ if (!partitions) {
268
+ partitions = new Set();
269
+ topicsPartitions.set(topic, partitions);
270
+ }
271
+ partitions.add(partition);
272
+ }
273
+ return topicsPartitions;
274
+ }
275
+ getSendBrokers(options, callback) {
276
+ if (!callback) {
277
+ callback = createPromisifiedCallback();
278
+ }
279
+ if (this[kCheckNotClosed](callback)) {
280
+ return callback[kCallbackPromise];
281
+ }
282
+ let topicsPartitions;
283
+ try {
284
+ topicsPartitions = this.getSendTopicPartitions(options);
285
+ }
286
+ catch (error) {
287
+ callback(error);
288
+ return callback[kCallbackPromise];
289
+ }
290
+ this[kMetadata]({ topics: Array.from(topicsPartitions.keys()), autocreateTopics: options.autocreateTopics }, (error, metadata) => {
291
+ if (error) {
292
+ callback(error);
293
+ return;
294
+ }
295
+ const brokers = {};
296
+ for (const [topic, partitions] of topicsPartitions) {
297
+ for (const rawPartition of partitions) {
298
+ const partition = rawPartition & metadata.topics.get(topic).partitionsCount;
299
+ const leader = metadata.topics.get(topic).partitions[partition].leader;
300
+ const broker = metadata.brokers.get(leader);
301
+ brokers[`${topic}:${partition}`] = broker;
302
+ }
303
+ }
304
+ callback(null, brokers);
305
+ });
306
+ return callback[kCallbackPromise];
307
+ }
308
+ getSendConnections(options, callback) {
309
+ if (!callback) {
310
+ callback = createPromisifiedCallback();
311
+ }
312
+ if (this[kCheckNotClosed](callback)) {
313
+ return callback[kCallbackPromise];
314
+ }
315
+ this.getSendBrokers(options, (error, brokers) => {
316
+ if (error) {
317
+ callback(error);
318
+ return;
319
+ }
320
+ runConcurrentCallbacks('Preparing producer connections failed.', Object.entries(brokers), ([topicPartition, broker], concurrentCallback) => {
321
+ this[kGetConnection](broker, (error, connection) => {
322
+ if (error) {
323
+ concurrentCallback(error);
324
+ return;
325
+ }
326
+ concurrentCallback(null, [topicPartition, connection]);
327
+ });
328
+ }, (error, results) => {
329
+ if (error) {
330
+ callback(error);
331
+ return;
332
+ }
333
+ callback(null, Object.fromEntries(results));
334
+ });
335
+ });
336
+ return callback[kCallbackPromise];
337
+ }
246
338
  [kTransactionFindCoordinator](callback) {
247
339
  if (this.#coordinatorId !== undefined) {
248
340
  callback(null);
@@ -533,22 +625,7 @@ export class Producer extends Base {
533
625
  callback(new UserError('Failed to serialize a message.', { cause: error }));
534
626
  return;
535
627
  }
536
- let partition = 0;
537
- if (typeof message.partition !== 'number') {
538
- if (partitioner) {
539
- partition = partitioner(message, key);
540
- }
541
- else if (key) {
542
- partition = defaultPartitioner(message, key);
543
- }
544
- else {
545
- // Use the roundrobin
546
- partition = this.#partitionsRoundRobin.postIncrement(topic, 1, 0);
547
- }
548
- }
549
- else {
550
- partition = message.partition;
551
- }
628
+ const partition = this.#assignPartition(message, partitioner, key, topic);
552
629
  topics.add(topic);
553
630
  messages.push({
554
631
  key,
@@ -610,16 +687,12 @@ export class Producer extends Base {
610
687
  nodes.push(destination);
611
688
  this.#performSingleDestinationSend(topics, destinationMessages, this[kOptions].timeout, sendOptions.acks, sendOptions.autocreateTopics, sendOptions.repeatOnStaleMetadata, produceOptions, concurrentCallback);
612
689
  }, (error, apiResults) => {
613
- if (error) {
614
- callback(error);
615
- return;
616
- }
617
690
  this.#metricsProducedMessages?.inc(messages.length);
618
691
  const results = {};
619
692
  if (sendOptions.acks === ProduceAcks.NO_RESPONSE) {
620
693
  const unwritableNodes = [];
621
694
  for (let i = 0; i < apiResults.length; i++) {
622
- if (apiResults[i] === false) {
695
+ if (typeof apiResults[i] === 'undefined' || apiResults[i] === false) {
623
696
  unwritableNodes.push(nodes[i]);
624
697
  }
625
698
  }
@@ -628,7 +701,7 @@ export class Producer extends Base {
628
701
  else {
629
702
  const topics = [];
630
703
  for (const result of apiResults) {
631
- for (const { name, partitionResponses } of result.responses) {
704
+ for (const { name, partitionResponses } of result?.responses ?? []) {
632
705
  for (const partitionResponse of partitionResponses) {
633
706
  topics.push({
634
707
  topic: name,
@@ -642,7 +715,14 @@ export class Producer extends Base {
642
715
  results.offsets = topics;
643
716
  }
644
717
  }
645
- callback(null, results);
718
+ if (error) {
719
+ ;
720
+ error.produced = results;
721
+ callback(error);
722
+ }
723
+ else {
724
+ callback(null, results);
725
+ }
646
726
  });
647
727
  });
648
728
  }
@@ -673,6 +753,7 @@ export class Producer extends Base {
673
753
  if (error) {
674
754
  // If the last error was due to stale metadata, we retry the operation with this set of messages
675
755
  // since the partition is already set, it should attempt on the new destination
756
+ /* c8 ignore next 3 - Hard to test */
676
757
  const kafkaError = GenericError.isGenericError(error)
677
758
  ? error
678
759
  : null;
@@ -682,11 +763,12 @@ export class Producer extends Base {
682
763
  this.#performSingleDestinationSend(topics, messages, timeout, acks, autocreateTopics, false, produceOptions, callback);
683
764
  return;
684
765
  }
685
- callback(error);
766
+ callback(error, results);
686
767
  return;
687
768
  }
688
769
  callback(error, results);
689
770
  }, 0, [], error => {
771
+ /* c8 ignore next 3 - Hard to test */
690
772
  if (!repeatOnStaleMetadata || !GenericError.isGenericError(error)) {
691
773
  return false;
692
774
  }
@@ -752,4 +834,23 @@ export class Producer extends Base {
752
834
  this.#send(options, callback);
753
835
  });
754
836
  }
837
+ #assignPartition(message, partitioner, key, topic) {
838
+ let partition = 0;
839
+ if (typeof message.partition !== 'number') {
840
+ if (partitioner) {
841
+ partition = partitioner(message, key);
842
+ }
843
+ else if (key) {
844
+ partition = defaultPartitioner(message, key);
845
+ }
846
+ else {
847
+ // Use the roundrobin
848
+ partition = this.#partitionsRoundRobin.postIncrement(topic, 1, 0);
849
+ }
850
+ }
851
+ else {
852
+ partition = message.partition;
853
+ }
854
+ return partition;
855
+ }
755
856
  }
package/dist/errors.js CHANGED
@@ -120,7 +120,7 @@ export class ProtocolError extends GenericError {
120
120
  'NOT_LEADER_OR_FOLLOWER',
121
121
  'FENCED_LEADER_EPOCH'
122
122
  ].includes(id),
123
- needsRejoin: ['MEMBER_ID_REQUIRED', 'UNKNOWN_MEMBER_ID', 'REBALANCE_IN_PROGRESS'].includes(id),
123
+ needsRejoin: ['MEMBER_ID_REQUIRED', 'UNKNOWN_MEMBER_ID', 'REBALANCE_IN_PROGRESS', 'ILLEGAL_GENERATION'].includes(id),
124
124
  producerFenced: id === 'INVALID_PRODUCER_EPOCH',
125
125
  rebalanceInProgress: id === 'REBALANCE_IN_PROGRESS',
126
126
  unknownMemberId: id === 'UNKNOWN_MEMBER_ID',
@@ -27,12 +27,15 @@ export declare class ConnectionPool extends TypedEventEmitter<ConnectionPoolEven
27
27
  get instanceId(): number;
28
28
  get ownerId(): number | undefined;
29
29
  get context(): unknown;
30
+ [Symbol.iterator](): MapIterator<[string, Connection]>;
30
31
  get(broker: Broker, callback: CallbackWithPromise<Connection>): void;
31
32
  get(broker: Broker): Promise<Connection>;
32
33
  getFirstAvailable(brokers: Broker[], callback: CallbackWithPromise<Connection>): void;
33
34
  getFirstAvailable(brokers: Broker[]): Promise<Connection>;
35
+ getEstablishedConnection(broker: Broker): Connection | undefined;
34
36
  close(callback: CallbackWithPromise<void>): void;
35
37
  close(): Promise<void>;
36
38
  isActive(): boolean;
37
39
  isConnected(): boolean;
40
+ has(broker: Broker): boolean;
38
41
  }
@@ -34,6 +34,9 @@ export class ConnectionPool extends TypedEventEmitter {
34
34
  get context() {
35
35
  return this.#context;
36
36
  }
37
+ [Symbol.iterator]() {
38
+ return this.#connections[Symbol.iterator]();
39
+ }
37
40
  get(broker, callback) {
38
41
  if (!callback) {
39
42
  callback = createPromisifiedCallback();
@@ -48,6 +51,9 @@ export class ConnectionPool extends TypedEventEmitter {
48
51
  connectionsPoolGetsChannel.traceCallback(this.#getFirstAvailable, 3, createDiagnosticContext({ connectionPool: this, brokers, operation: 'getFirstAvailable' }), this, brokers, 0, [], callback);
49
52
  return callback[kCallbackPromise];
50
53
  }
54
+ getEstablishedConnection(broker) {
55
+ return this.#connections.get(`${broker.host}:${broker.port}`);
56
+ }
51
57
  close(callback) {
52
58
  if (!callback) {
53
59
  callback = createPromisifiedCallback();
@@ -81,6 +87,9 @@ export class ConnectionPool extends TypedEventEmitter {
81
87
  }
82
88
  return true;
83
89
  }
90
+ has(broker) {
91
+ return this.#connections.has(`${broker.host}:${broker.port}`);
92
+ }
84
93
  #get(broker, callback) {
85
94
  if (this.#closed) {
86
95
  callback(new Error('Connection pool is closed.'));
@@ -122,6 +122,7 @@ export class Connection extends TypedEventEmitter {
122
122
  connectionOptions.servername =
123
123
  typeof this.#options.tlsServerName === 'string' ? this.#options.tlsServerName : host;
124
124
  }
125
+ /* c8 ignore next 13 - Hard to test */
125
126
  const connectingSocketTimeoutHandler = () => {
126
127
  const error = new TimeoutError(`Connection to ${host}:${port} timed out.`);
127
128
  diagnosticContext.error = error;
@@ -5,7 +5,7 @@ import { type CallbackWithPromise } from "../../apis/callbacks";
5
5
  import { type Callback } from "../../apis/definitions";
6
6
  import { type Acl } from "../../apis/types";
7
7
  import { Base } from "../base/base";
8
- import { type AdminListOffsetsOptions, type AdminOptions, type AlterClientQuotasOptions, type AlterConfigsOptions, type AlterConsumerGroupOffsetsOptions, type BrokerLogDirDescription, type ConfigDescription, type CreateAclsOptions, type CreatedTopic, type CreatePartitionsOptions, type CreateTopicsOptions, type DeleteAclsOptions, type DeleteConsumerGroupOffsetsOptions, type DeleteRecordsOptions, type DeleteGroupsOptions, type DeleteTopicsOptions, type DeletedRecordsTopic, type DescribeAclsOptions, type DescribeClientQuotasOptions, type DescribeConfigsOptions, type DescribeGroupsOptions, type DescribeLogDirsOptions, type FindCoordinatorOptions, type FindCoordinatorResult, type Group, type GroupBase, type IncrementalAlterConfigsOptions, type ListConsumerGroupOffsetsGroup, type ListConsumerGroupOffsetsOptions, type ListedOffsetsTopic, type ListGroupsOptions, type ListTopicsOptions, type RemoveMembersFromConsumerGroupOptions } from "./types";
8
+ import { type AdminListOffsetsOptions, type AdminOptions, type AlterClientQuotasOptions, type AlterConfigsOptions, type AlterConsumerGroupOffsetsOptions, type BrokerLogDirDescription, type ConfigDescription, type CreateAclsOptions, type CreatedTopic, type CreatePartitionsOptions, type CreateTopicsOptions, type DeleteAclsOptions, type DeleteConsumerGroupOffsetsOptions, type DeletedRecordsTopic, type DeleteGroupsOptions, type DeleteRecordsOptions, type DeleteTopicsOptions, type DescribeAclsOptions, type DescribeClientQuotasOptions, type DescribeConfigsOptions, type DescribeGroupsOptions, type DescribeLogDirsOptions, type FindCoordinatorOptions, type FindCoordinatorResult, type Group, type GroupBase, type IncrementalAlterConfigsOptions, type ListConsumerGroupOffsetsGroup, type ListConsumerGroupOffsetsOptions, type ListedOffsetsTopic, type ListGroupsOptions, type ListTopicsOptions, type RemoveMembersFromConsumerGroupOptions } from "./types";
9
9
  export declare class Admin extends Base<AdminOptions> {
10
10
  #private;
11
11
  constructor(options: AdminOptions);
@@ -66,6 +66,7 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
66
66
  get closed(): boolean;
67
67
  get type(): ClientType;
68
68
  get context(): unknown;
69
+ get connections(): ConnectionPool;
69
70
  emitWithDebug(section: string | null, name: string, ...args: any[]): boolean;
70
71
  close(callback: CallbackWithPromise<void>): void;
71
72
  close(): Promise<void>;
@@ -19,6 +19,7 @@ export declare class MessagesStream<Key, Value, HeaderKey, HeaderValue> extends
19
19
  get offsetsToCommit(): Map<string, CommitOptionsPartition>;
20
20
  get offsetsCommitted(): Map<string, bigint>;
21
21
  get committedOffsets(): Map<string, bigint>;
22
+ get connections(): ConnectionPool;
22
23
  close(callback: CallbackWithPromise<void>): void;
23
24
  close(): Promise<void>;
24
25
  isActive(): boolean;
@@ -1,4 +1,5 @@
1
1
  import { type CallbackWithPromise } from "../../apis/callbacks";
2
+ import { type Broker, type Connection } from "../../network/connection";
2
3
  import { type Message } from "../../protocol/records";
3
4
  import { kTransaction, kTransactionAddOffsets, kTransactionAddPartitions, kTransactionCancel, kTransactionCommitOffset, kTransactionEnd, kTransactionFindCoordinator } from "../../symbols";
4
5
  import { Base } from "../base/base";
@@ -27,6 +28,11 @@ export declare class Producer<Key = Buffer, Value = Buffer, HeaderKey = Buffer,
27
28
  asStream(options?: ProducerStreamOptions<Key, Value, HeaderKey, HeaderValue>): ProducerStream<Key, Value, HeaderKey, HeaderValue>;
28
29
  beginTransaction(options: ProduceOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<Transaction<Key, Value, HeaderKey, HeaderValue>>): void;
29
30
  beginTransaction(options?: ProduceOptions<Key, Value, HeaderKey, HeaderValue>): Promise<Transaction<Key, Value, HeaderKey, HeaderValue>>;
31
+ getSendTopicPartitions(options: SendOptions<Key, Value, HeaderKey, HeaderValue>): Map<string, Set<number>>;
32
+ getSendBrokers(options: SendOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<Record<string, Broker>>): void;
33
+ getSendBrokers(options: SendOptions<Key, Value, HeaderKey, HeaderValue>): Promise<Record<string, Broker>>;
34
+ getSendConnections(options: SendOptions<Key, Value, HeaderKey, HeaderValue>, callback: CallbackWithPromise<Record<string, Connection>>): void;
35
+ getSendConnections(options: SendOptions<Key, Value, HeaderKey, HeaderValue>): Promise<Record<string, Connection>>;
30
36
  [kTransactionFindCoordinator](callback: CallbackWithPromise<void>): void;
31
37
  [kTransactionAddPartitions](transactionId: number, topicsPartitions: Map<string, Set<number>>, callback: CallbackWithPromise<void>): void;
32
38
  [kTransactionAddOffsets](transactionId: number, groupId: string, callback: CallbackWithPromise<void>): void;
@@ -27,12 +27,15 @@ export declare class ConnectionPool extends TypedEventEmitter<ConnectionPoolEven
27
27
  get instanceId(): number;
28
28
  get ownerId(): number | undefined;
29
29
  get context(): unknown;
30
+ [Symbol.iterator](): MapIterator<[string, Connection]>;
30
31
  get(broker: Broker, callback: CallbackWithPromise<Connection>): void;
31
32
  get(broker: Broker): Promise<Connection>;
32
33
  getFirstAvailable(brokers: Broker[], callback: CallbackWithPromise<Connection>): void;
33
34
  getFirstAvailable(brokers: Broker[]): Promise<Connection>;
35
+ getEstablishedConnection(broker: Broker): Connection | undefined;
34
36
  close(callback: CallbackWithPromise<void>): void;
35
37
  close(): Promise<void>;
36
38
  isActive(): boolean;
37
39
  isConnected(): boolean;
40
+ has(broker: Broker): boolean;
38
41
  }
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = "@platformatic/kafka";
2
- export const version = "1.33.0";
2
+ export const version = "1.34.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "description": "Modern and performant client for Apache Kafka",
5
5
  "homepage": "https://github.com/platformatic/kafka",
6
6
  "author": "Platformatic Inc. <oss@platformatic.dev> (https://platformatic.dev)",