@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
|
|
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
|
|
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
|
-
|
|
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[
|
|
340
|
-
return;
|
|
346
|
+
brokers = this[kBootstrapBrokers];
|
|
341
347
|
}
|
|
342
|
-
|
|
343
|
-
|
|
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.#
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
453
|
+
callback(null, false);
|
|
454
|
+
return;
|
|
438
455
|
}
|
|
439
456
|
const response = error.response;
|
|
440
457
|
if (!response || response.errorCode !== 0) {
|
|
441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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)",
|