@platformatic/kafka 2.1.0 → 2.2.3

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.
@@ -84,11 +84,11 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
84
84
  [kMetadata](options: MetadataOptions, callback: CallbackWithPromise<ClusterMetadata>): void;
85
85
  [kCheckNotClosed](callback: CallbackWithPromise<any>): boolean;
86
86
  clearMetadata(): void;
87
- [kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
87
+ [kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>, attempt: number) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
88
88
  [kPerformDeduplicated]<ReturnType>(operationId: string, operation: (callback: CallbackWithPromise<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>): void | Promise<ReturnType>;
89
89
  [kGetApi]<RequestArguments extends Array<unknown>, ResponseType>(name: string, callback: Callback<API<RequestArguments, ResponseType>>): void;
90
90
  [kGetConnection](broker: Broker, callback: Callback<Connection>): void;
91
- [kGetBootstrapConnection](callback: Callback<Connection>): void;
91
+ [kGetBootstrapConnection](callback: Callback<Connection>, attempt?: number): void;
92
92
  [kValidateOptions](target: unknown, validator: ValidateFunction<unknown>, targetName: string, throwOnErrors?: boolean): Error | null;
93
93
  [kInspect](...args: unknown[]): void;
94
94
  [kFormatValidationErrors](validator: ValidateFunction<unknown>, targetName: string): string;
@@ -206,7 +206,7 @@ export class Base extends TypedEventEmitter {
206
206
  }
207
207
  [kListApis](callback) {
208
208
  this[kPerformDeduplicated]('listApis', deduplicateCallback => {
209
- this[kPerformWithRetry]('listApis', retryCallback => {
209
+ this[kPerformWithRetry]('listApis', (retryCallback, attempt) => {
210
210
  this[kGetBootstrapConnection]((error, connection) => {
211
211
  if (error) {
212
212
  retryCallback(error);
@@ -214,7 +214,7 @@ export class Base extends TypedEventEmitter {
214
214
  }
215
215
  // We use V3 to be able to get APIS from Kafka 2.4.0+
216
216
  apiVersionsV3(connection, clientSoftwareName, clientSoftwareVersion, retryCallback);
217
- });
217
+ }, attempt);
218
218
  }, (error, metadata) => {
219
219
  if (error) {
220
220
  deduplicateCallback(error);
@@ -260,7 +260,13 @@ export class Base extends TypedEventEmitter {
260
260
  this.emitWithDebug('client', 'performWithRetry:retry', operationId, attempt, retries, delay);
261
261
  const timeout = setTimeout(() => {
262
262
  this.removeListener('client:close', onClose);
263
- this[kPerformWithRetry](operationId, operation, callback, attempt + 1, errors, shouldSkipRetry);
263
+ try {
264
+ this[kPerformWithRetry](operationId, operation, callback, attempt + 1, errors, shouldSkipRetry);
265
+ }
266
+ catch (error) {
267
+ errors.push(error);
268
+ callback(new MultipleErrors(`${operationId} failed ${attempt + 1} times.`, errors));
269
+ }
264
270
  }, delay);
265
271
  this.once('client:close', onClose);
266
272
  }
@@ -279,7 +285,7 @@ export class Base extends TypedEventEmitter {
279
285
  errors.splice(0, errors.length);
280
286
  }
281
287
  callback(null, result);
282
- });
288
+ }, attempt);
283
289
  return callback[kCallbackPromise];
284
290
  }
285
291
  [kPerformDeduplicated](operationId, operation, callback) {
@@ -334,13 +340,20 @@ export class Base extends TypedEventEmitter {
334
340
  [kGetConnection](broker, callback) {
335
341
  this[kConnections].get(broker, callback);
336
342
  }
337
- [kGetBootstrapConnection](callback) {
343
+ [kGetBootstrapConnection](callback, attempt = 0) {
344
+ let brokers;
338
345
  if (!this.#metadata) {
339
- this[kConnections].getFirstAvailable(this[kBootstrapBrokers], callback);
340
- return;
346
+ brokers = this[kBootstrapBrokers];
341
347
  }
342
- const discovered = Array.from(this.#metadata.brokers.values());
343
- this[kConnections].getFirstAvailable([...this[kBootstrapBrokers], ...discovered], callback);
348
+ else {
349
+ const discovered = Array.from(this.#metadata.brokers.values());
350
+ brokers = [...this[kBootstrapBrokers], ...discovered];
351
+ }
352
+ if (attempt > 0 && brokers.length > 1) {
353
+ const offset = attempt % brokers.length;
354
+ brokers = [...brokers.slice(offset), ...brokers.slice(0, offset)];
355
+ }
356
+ this[kConnections].getFirstAvailable(brokers, callback);
344
357
  }
345
358
  [kValidateOptions](target, validator, targetName, throwOnErrors = true) {
346
359
  if (!this[kOptions].strict) {
@@ -395,7 +408,7 @@ export class Base extends TypedEventEmitter {
395
408
  this[kPerformDeduplicated](
396
409
  // Unique key to avoid mixing callbacks
397
410
  `metadata-${topics.sort().join(',')}-${autocreateTopics}-${options.forceUpdate}`, deduplicateCallback => {
398
- this[kPerformWithRetry]('metadata', retryCallback => {
411
+ this[kPerformWithRetry]('metadata', (retryCallback, attempt) => {
399
412
  this[kGetBootstrapConnection]((error, connection) => {
400
413
  if (error) {
401
414
  retryCallback(error);
@@ -408,7 +421,7 @@ export class Base extends TypedEventEmitter {
408
421
  }
409
422
  api(connection, topicsToFetch, autocreateTopics, true, retryCallback);
410
423
  });
411
- });
424
+ }, attempt);
412
425
  }, (error, metadata) => {
413
426
  if (error) {
414
427
  const unknownTopicError = error.findBy('apiCode', 3);
@@ -302,7 +302,11 @@ export class MessagesStream extends Readable {
302
302
  return super[Symbol.asyncIterator]();
303
303
  }
304
304
  _construct(callback) {
305
- this.#refreshOffsets(callback);
305
+ this.#refreshOffsetsInflight = true;
306
+ this.#refreshOffsets(error => {
307
+ this.#refreshOffsetsInflight = false;
308
+ callback(error ?? undefined);
309
+ });
306
310
  }
307
311
  _destroy(error, callback) {
308
312
  if (this.#autocommitInterval) {
@@ -409,10 +413,22 @@ export class MessagesStream extends Readable {
409
413
  this.push(null);
410
414
  return;
411
415
  }
412
- if (this.#fallbackMode !== MessagesStreamFallbackModes.FAIL &&
413
- this.#handleOffsetOutOfRange(error, topicIds)) {
414
- process.nextTick(() => {
415
- this.#fetch();
416
+ if (this.#fallbackMode !== MessagesStreamFallbackModes.FAIL) {
417
+ this.#handleOffsetOutOfRange(error, topicIds, (recoveryError, recovered) => {
418
+ if (this.#closed || this.closed || this.destroyed) {
419
+ return;
420
+ }
421
+ if (recoveryError) {
422
+ this.destroy(recoveryError);
423
+ return;
424
+ }
425
+ if (recovered) {
426
+ process.nextTick(() => {
427
+ this.#fetch();
428
+ });
429
+ return;
430
+ }
431
+ this.destroy(error);
416
432
  });
417
433
  return;
418
434
  }
@@ -432,34 +448,81 @@ export class MessagesStream extends Readable {
432
448
  }
433
449
  });
434
450
  }
435
- #handleOffsetOutOfRange(error, topicIds) {
451
+ #handleOffsetOutOfRange(error, topicIds, callback) {
436
452
  if (!error.findBy?.('apiId', 'OFFSET_OUT_OF_RANGE')) {
437
- return false;
453
+ callback(null, false);
454
+ return;
438
455
  }
439
456
  const response = error.response;
440
457
  if (!response || response.errorCode !== 0) {
441
- return false;
458
+ callback(null, false);
459
+ return;
442
460
  }
443
461
  const recoveredOffsets = [];
462
+ const partitionsToRefresh = new Map();
444
463
  for (const topicResponse of response.responses) {
445
464
  const topic = topicIds.get(topicResponse.topicId);
446
465
  if (!topic) {
447
- return false;
466
+ callback(null, false);
467
+ return;
448
468
  }
449
469
  for (const partitionResponse of topicResponse.partitions) {
450
470
  if (partitionResponse.errorCode === 0) {
451
471
  continue;
452
472
  }
453
473
  if (partitionResponse.errorCode !== protocolErrors.OFFSET_OUT_OF_RANGE.code) {
454
- return false;
474
+ callback(null, false);
475
+ return;
455
476
  }
456
- const key = `${topic}:${partitionResponse.partitionIndex}`;
457
477
  const offset = this.#fallbackMode === MessagesStreamFallbackModes.EARLIEST
458
478
  ? partitionResponse.logStartOffset
459
479
  : partitionResponse.highWatermark;
460
- recoveredOffsets.push([key, offset]);
480
+ if (offset >= 0n) {
481
+ recoveredOffsets.push([partitionKey(topic, partitionResponse.partitionIndex), offset]);
482
+ continue;
483
+ }
484
+ let partitions = partitionsToRefresh.get(topic);
485
+ if (!partitions) {
486
+ partitions = [];
487
+ partitionsToRefresh.set(topic, partitions);
488
+ }
489
+ partitions.push(partitionResponse.partitionIndex);
461
490
  }
462
491
  }
492
+ if (partitionsToRefresh.size > 0) {
493
+ this.#refreshOutOfRangeOffsets(partitionsToRefresh, recoveredOffsets, callback);
494
+ return;
495
+ }
496
+ callback(null, this.#applyRecoveredOffsets(recoveredOffsets));
497
+ }
498
+ #refreshOutOfRangeOffsets(partitionsToRefresh, recoveredOffsets, callback) {
499
+ const partitions = Object.fromEntries(partitionsToRefresh);
500
+ this.#consumer.listOffsets({
501
+ topics: Array.from(partitionsToRefresh.keys()),
502
+ partitions,
503
+ timestamp: this.#fallbackMode === MessagesStreamFallbackModes.EARLIEST
504
+ ? ListOffsetTimestamps.EARLIEST
505
+ : ListOffsetTimestamps.LATEST
506
+ }, (error, offsets) => {
507
+ if (error) {
508
+ callback(error);
509
+ return;
510
+ }
511
+ for (const [topic, refreshedPartitions] of partitionsToRefresh) {
512
+ const topicOffsets = offsets.get(topic);
513
+ for (const partition of refreshedPartitions) {
514
+ const offset = topicOffsets?.[partition];
515
+ if (typeof offset !== 'bigint' || offset < 0n) {
516
+ callback(new UserError(`Cannot recover offset out of range for topic ${topic} partition ${partition}.`));
517
+ return;
518
+ }
519
+ recoveredOffsets.push([partitionKey(topic, partition), offset]);
520
+ }
521
+ }
522
+ callback(null, this.#applyRecoveredOffsets(recoveredOffsets));
523
+ });
524
+ }
525
+ #applyRecoveredOffsets(recoveredOffsets) {
463
526
  for (const [key, offset] of recoveredOffsets) {
464
527
  this.#offsetsToFetch.set(key, offset);
465
528
  this.#offsetsCommitted.set(key, offset);
@@ -129,7 +129,7 @@ export class Connection extends TypedEventEmitter {
129
129
  }
130
130
  /* c8 ignore next 13 - Hard to test */
131
131
  const connectingSocketTimeoutHandler = () => {
132
- const error = new TimeoutError(`Connection to ${host}:${port} timed out.`);
132
+ const error = new TimeoutError(`Connection to ${host}:${port} timed out.`, { canRetry: true });
133
133
  diagnosticContext.error = error;
134
134
  this.#socket.destroy();
135
135
  this.#status = ConnectionStatuses.ERROR;
@@ -219,7 +219,7 @@ export class Connection extends TypedEventEmitter {
219
219
  this.#socket?.destroy();
220
220
  callback(new TimeoutError(this.#host
221
221
  ? `Connection to ${this.#host}:${this.#port} timed out.`
222
- : `Connection ready timed out after ${this.#options.connectTimeout}ms.`));
222
+ : `Connection ready timed out after ${this.#options.connectTimeout}ms.`, { canRetry: true }));
223
223
  }, this.#options.connectTimeout);
224
224
  this.once('connect', onConnect);
225
225
  this.once('error', onError);
@@ -258,7 +258,9 @@ export class Connection extends TypedEventEmitter {
258
258
  return callback[kCallbackPromise];
259
259
  }
260
260
  send(apiKey, apiVersion, createPayload, responseParser, hasRequestHeaderTaggedFields, hasResponseHeaderTaggedFields, callback) {
261
- const correlationId = ++this.#correlationId;
261
+ // Correlation ID is a 32-bit integer in the protocol, so we need to wrap around after 2^31 - 1
262
+ const correlationId = (this.#correlationId + 1) & 0x7FFFFFFF;
263
+ this.#correlationId = correlationId;
262
264
  const diagnostic = createDiagnosticContext({
263
265
  connection: this,
264
266
  operation: 'send',
@@ -84,11 +84,11 @@ export declare class Base<OptionsType extends BaseOptions = BaseOptions, EventsT
84
84
  [kMetadata](options: MetadataOptions, callback: CallbackWithPromise<ClusterMetadata>): void;
85
85
  [kCheckNotClosed](callback: CallbackWithPromise<any>): boolean;
86
86
  clearMetadata(): void;
87
- [kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
87
+ [kPerformWithRetry]<ReturnType>(operationId: string, operation: (callback: Callback<ReturnType>, attempt: number) => void, callback: CallbackWithPromise<ReturnType>, attempt?: number, errors?: Error[], shouldSkipRetry?: (e: Error) => boolean): void | Promise<ReturnType>;
88
88
  [kPerformDeduplicated]<ReturnType>(operationId: string, operation: (callback: CallbackWithPromise<ReturnType>) => void, callback: CallbackWithPromise<ReturnType>): void | Promise<ReturnType>;
89
89
  [kGetApi]<RequestArguments extends Array<unknown>, ResponseType>(name: string, callback: Callback<API<RequestArguments, ResponseType>>): void;
90
90
  [kGetConnection](broker: Broker, callback: Callback<Connection>): void;
91
- [kGetBootstrapConnection](callback: Callback<Connection>): void;
91
+ [kGetBootstrapConnection](callback: Callback<Connection>, attempt?: number): void;
92
92
  [kValidateOptions](target: unknown, validator: ValidateFunction<unknown>, targetName: string, throwOnErrors?: boolean): Error | null;
93
93
  [kInspect](...args: unknown[]): void;
94
94
  [kFormatValidationErrors](validator: ValidateFunction<unknown>, targetName: string): string;
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const name = "@platformatic/kafka";
2
- export const version = "2.1.0";
2
+ export const version = "2.2.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/kafka",
3
- "version": "2.1.0",
3
+ "version": "2.2.3",
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)",