@platformatic/kafka 2.1.0 → 2.2.2
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);
|
|
@@ -409,10 +409,22 @@ export class MessagesStream extends Readable {
|
|
|
409
409
|
this.push(null);
|
|
410
410
|
return;
|
|
411
411
|
}
|
|
412
|
-
if (this.#fallbackMode !== MessagesStreamFallbackModes.FAIL
|
|
413
|
-
this.#handleOffsetOutOfRange(error, topicIds)
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
if (this.#fallbackMode !== MessagesStreamFallbackModes.FAIL) {
|
|
413
|
+
this.#handleOffsetOutOfRange(error, topicIds, (recoveryError, recovered) => {
|
|
414
|
+
if (this.#closed || this.closed || this.destroyed) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (recoveryError) {
|
|
418
|
+
this.destroy(recoveryError);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (recovered) {
|
|
422
|
+
process.nextTick(() => {
|
|
423
|
+
this.#fetch();
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
this.destroy(error);
|
|
416
428
|
});
|
|
417
429
|
return;
|
|
418
430
|
}
|
|
@@ -432,34 +444,81 @@ export class MessagesStream extends Readable {
|
|
|
432
444
|
}
|
|
433
445
|
});
|
|
434
446
|
}
|
|
435
|
-
#handleOffsetOutOfRange(error, topicIds) {
|
|
447
|
+
#handleOffsetOutOfRange(error, topicIds, callback) {
|
|
436
448
|
if (!error.findBy?.('apiId', 'OFFSET_OUT_OF_RANGE')) {
|
|
437
|
-
|
|
449
|
+
callback(null, false);
|
|
450
|
+
return;
|
|
438
451
|
}
|
|
439
452
|
const response = error.response;
|
|
440
453
|
if (!response || response.errorCode !== 0) {
|
|
441
|
-
|
|
454
|
+
callback(null, false);
|
|
455
|
+
return;
|
|
442
456
|
}
|
|
443
457
|
const recoveredOffsets = [];
|
|
458
|
+
const partitionsToRefresh = new Map();
|
|
444
459
|
for (const topicResponse of response.responses) {
|
|
445
460
|
const topic = topicIds.get(topicResponse.topicId);
|
|
446
461
|
if (!topic) {
|
|
447
|
-
|
|
462
|
+
callback(null, false);
|
|
463
|
+
return;
|
|
448
464
|
}
|
|
449
465
|
for (const partitionResponse of topicResponse.partitions) {
|
|
450
466
|
if (partitionResponse.errorCode === 0) {
|
|
451
467
|
continue;
|
|
452
468
|
}
|
|
453
469
|
if (partitionResponse.errorCode !== protocolErrors.OFFSET_OUT_OF_RANGE.code) {
|
|
454
|
-
|
|
470
|
+
callback(null, false);
|
|
471
|
+
return;
|
|
455
472
|
}
|
|
456
|
-
const key = `${topic}:${partitionResponse.partitionIndex}`;
|
|
457
473
|
const offset = this.#fallbackMode === MessagesStreamFallbackModes.EARLIEST
|
|
458
474
|
? partitionResponse.logStartOffset
|
|
459
475
|
: partitionResponse.highWatermark;
|
|
460
|
-
|
|
476
|
+
if (offset >= 0n) {
|
|
477
|
+
recoveredOffsets.push([partitionKey(topic, partitionResponse.partitionIndex), offset]);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
let partitions = partitionsToRefresh.get(topic);
|
|
481
|
+
if (!partitions) {
|
|
482
|
+
partitions = [];
|
|
483
|
+
partitionsToRefresh.set(topic, partitions);
|
|
484
|
+
}
|
|
485
|
+
partitions.push(partitionResponse.partitionIndex);
|
|
461
486
|
}
|
|
462
487
|
}
|
|
488
|
+
if (partitionsToRefresh.size > 0) {
|
|
489
|
+
this.#refreshOutOfRangeOffsets(partitionsToRefresh, recoveredOffsets, callback);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
callback(null, this.#applyRecoveredOffsets(recoveredOffsets));
|
|
493
|
+
}
|
|
494
|
+
#refreshOutOfRangeOffsets(partitionsToRefresh, recoveredOffsets, callback) {
|
|
495
|
+
const partitions = Object.fromEntries(partitionsToRefresh);
|
|
496
|
+
this.#consumer.listOffsets({
|
|
497
|
+
topics: Array.from(partitionsToRefresh.keys()),
|
|
498
|
+
partitions,
|
|
499
|
+
timestamp: this.#fallbackMode === MessagesStreamFallbackModes.EARLIEST
|
|
500
|
+
? ListOffsetTimestamps.EARLIEST
|
|
501
|
+
: ListOffsetTimestamps.LATEST
|
|
502
|
+
}, (error, offsets) => {
|
|
503
|
+
if (error) {
|
|
504
|
+
callback(error);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
for (const [topic, refreshedPartitions] of partitionsToRefresh) {
|
|
508
|
+
const topicOffsets = offsets.get(topic);
|
|
509
|
+
for (const partition of refreshedPartitions) {
|
|
510
|
+
const offset = topicOffsets?.[partition];
|
|
511
|
+
if (typeof offset !== 'bigint' || offset < 0n) {
|
|
512
|
+
callback(new UserError(`Cannot recover offset out of range for topic ${topic} partition ${partition}.`));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
recoveredOffsets.push([partitionKey(topic, partition), offset]);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
callback(null, this.#applyRecoveredOffsets(recoveredOffsets));
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
#applyRecoveredOffsets(recoveredOffsets) {
|
|
463
522
|
for (const [key, offset] of recoveredOffsets) {
|
|
464
523
|
this.#offsetsToFetch.set(key, offset);
|
|
465
524
|
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.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/kafka",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
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)",
|