kafka-ts
Version:
**KafkaTS** is a Apache Kafka client library for Node.js. It provides both a low-level API for communicating directly with the Apache Kafka cluster and high-level APIs for publishing and subscribing to Kafka topics.
254 lines (253 loc) • 11.9 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Consumer = void 0;
const events_1 = __importDefault(require("events"));
const api_1 = require("../api");
const messages_to_topic_partition_leaders_1 = require("../distributors/messages-to-topic-partition-leaders");
const delay_1 = require("../utils/delay");
const error_1 = require("../utils/error");
const logger_1 = require("../utils/logger");
const retrier_1 = require("../utils/retrier");
const tracer_1 = require("../utils/tracer");
const consumer_group_1 = require("./consumer-group");
const consumer_metadata_1 = require("./consumer-metadata");
const fetch_manager_1 = require("./fetch-manager");
const offset_manager_1 = require("./offset-manager");
const trace = (0, tracer_1.createTracer)('Consumer');
class Consumer extends events_1.default {
cluster;
options;
metadata;
consumerGroup;
offsetManager;
fetchManager;
stopHook;
constructor(cluster, options) {
super();
this.cluster = cluster;
this.options = {
...options,
groupId: options.groupId ?? null,
groupInstanceId: options.groupInstanceId ?? null,
rackId: options.rackId ?? '',
sessionTimeoutMs: options.sessionTimeoutMs ?? 30_000,
rebalanceTimeoutMs: options.rebalanceTimeoutMs ?? 60_000,
maxWaitMs: options.maxWaitMs ?? 5000,
minBytes: options.minBytes ?? 1,
maxBytes: options.maxBytes ?? 1_048_576,
partitionMaxBytes: options.partitionMaxBytes ?? 1_048_576,
isolationLevel: options.isolationLevel ?? 1 /* IsolationLevel.READ_COMMITTED */,
allowTopicAutoCreation: options.allowTopicAutoCreation ?? false,
fromBeginning: options.fromBeginning ?? false,
fromTimestamp: options.fromTimestamp ?? (options.fromBeginning ? -2n : -1n),
retrier: options.retrier ?? retrier_1.defaultRetrier,
};
this.metadata = new consumer_metadata_1.ConsumerMetadata({ cluster: this.cluster });
this.offsetManager = new offset_manager_1.OffsetManager({
cluster: this.cluster,
metadata: this.metadata,
isolationLevel: this.options.isolationLevel,
});
this.consumerGroup = this.options.groupId
? new consumer_group_1.ConsumerGroup({
cluster: this.cluster,
topics: this.options.topics,
groupId: this.options.groupId,
groupInstanceId: this.options.groupInstanceId,
sessionTimeoutMs: this.options.sessionTimeoutMs,
rebalanceTimeoutMs: this.options.rebalanceTimeoutMs,
metadata: this.metadata,
offsetManager: this.offsetManager,
consumer: this,
})
: undefined;
}
async start() {
const { topics, allowTopicAutoCreation, fromTimestamp } = this.options;
this.stopHook = undefined;
try {
await this.cluster.connect();
await this.metadata.fetchMetadata({ topics, allowTopicAutoCreation });
this.metadata.setAssignment(this.metadata.getTopicPartitions());
await this.offsetManager.fetchOffsets({ fromTimestamp });
await this.consumerGroup?.init();
}
catch (error) {
logger_1.log.error('Failed to start consumer', error);
logger_1.log.debug(`Restarting consumer in 1 second...`);
await (0, delay_1.delay)(1000);
if (this.stopHook)
return this.stopHook();
return this.close(true).then(() => this.start());
}
this.startFetchManager();
}
async close(force = false) {
if (!force) {
await new Promise(async (resolve) => {
this.stopHook = resolve;
await this.fetchManager?.stop();
});
}
await this.consumerGroup?.leaveGroup().catch((error) => logger_1.log.debug(`Failed to leave group: ${error.message}`));
await this.cluster.disconnect().catch((error) => logger_1.log.debug(`Failed to disconnect: ${error.message}`));
}
async startFetchManager() {
const { groupId } = this.options;
while (!this.stopHook) {
try {
await this.consumerGroup?.join();
// TODO: If leader is not available, find another read replica
const nodeAssignments = Object.entries((0, messages_to_topic_partition_leaders_1.distributeMessagesToTopicPartitionLeaders)(Object.entries(this.metadata.getAssignment()).flatMap(([topic, partitions]) => partitions.map((partition) => ({ topic, partition }))), this.metadata.getTopicPartitionLeaderIds())).map(([nodeId, assignment]) => ({
nodeId: parseInt(nodeId),
assignment: Object.fromEntries(Object.entries(assignment).map(([topic, partitions]) => [
topic,
Object.keys(partitions).map(Number),
])),
}));
this.fetchManager = new fetch_manager_1.FetchManager({
fetch: this.fetch.bind(this),
process: this.process.bind(this),
nodeAssignments,
});
await this.fetchManager.start();
if (!nodeAssignments.length) {
await this.waitForReassignment();
}
}
catch (error) {
await this.fetchManager?.stop();
if (error.errorCode === api_1.API_ERROR.REBALANCE_IN_PROGRESS) {
logger_1.log.debug('Rebalance in progress...', { apiName: error.apiName, groupId });
continue;
}
if (error.errorCode === api_1.API_ERROR.FENCED_INSTANCE_ID) {
logger_1.log.debug('New consumer with the same groupInstanceId joined. Exiting the consumer...');
this.close();
break;
}
if (error instanceof error_1.ConnectionError ||
(error instanceof error_1.KafkaTSApiError && error.errorCode === api_1.API_ERROR.NOT_COORDINATOR)) {
logger_1.log.debug(`${error.message}. Restarting consumer...`);
this.close().then(() => this.start());
break;
}
logger_1.log.error(error.message, error);
logger_1.log.debug(`Restarting consumer in 1 second...`);
await (0, delay_1.delay)(1000);
this.close().then(() => this.start());
break;
}
}
this.stopHook?.();
}
async waitForReassignment() {
const { groupId } = this.options;
logger_1.log.debug('No partitions assigned. Waiting for reassignment...', { groupId });
while (!this.stopHook) {
await (0, delay_1.delay)(1000);
this.consumerGroup?.handleLastHeartbeat();
}
}
async process(response) {
const { options } = this;
const { retrier } = options;
this.consumerGroup?.handleLastHeartbeat();
const topicPartitions = {};
const messages = response.responses.flatMap(({ topicId, partitions }) => {
const topic = this.metadata.getTopicNameById(topicId);
topicPartitions[topic] ??= new Set();
return partitions.flatMap(({ partitionIndex, records }) => {
topicPartitions[topic].add(partitionIndex);
return records.flatMap(({ baseTimestamp, baseOffset, records }) => records.flatMap((message) => ({
topic,
partition: partitionIndex,
key: message.key ?? null,
value: message.value ?? null,
headers: Object.fromEntries(message.headers.map(({ key, value }) => [key, value])),
timestamp: baseTimestamp + BigInt(message.timestampDelta),
offset: baseOffset + BigInt(message.offsetDelta),
})));
});
});
if (!messages.length) {
return;
}
const resolveOffset = (message) => this.offsetManager.resolve(message.topic, message.partition, message.offset + 1n);
try {
await retrier(() => options.onBatch(messages.filter((message) => !this.offsetManager.isResolved(message)), { resolveOffset }));
}
catch (error) {
await this.consumerGroup
?.offsetCommit(topicPartitions)
.then(() => this.offsetManager.flush(topicPartitions))
.catch();
throw error;
}
response.responses.forEach(({ topicId, partitions }) => {
partitions.forEach(({ partitionIndex, records }) => {
records.forEach(({ baseOffset, lastOffsetDelta }) => {
this.offsetManager.resolve(this.metadata.getTopicNameById(topicId), partitionIndex, baseOffset + BigInt(lastOffsetDelta) + 1n);
});
});
});
await this.consumerGroup?.offsetCommit(topicPartitions);
this.offsetManager.flush(topicPartitions);
}
fetch(nodeId, assignment) {
const { rackId, maxWaitMs, minBytes, maxBytes, partitionMaxBytes, isolationLevel } = this.options;
this.consumerGroup?.handleLastHeartbeat();
return this.cluster.sendRequestToNode(nodeId)(api_1.API.FETCH, {
maxWaitMs,
minBytes,
maxBytes,
isolationLevel,
sessionId: 0,
sessionEpoch: -1,
topics: Object.entries(assignment).map(([topic, partitions]) => ({
topicId: this.metadata.getTopicIdByName(topic),
partitions: partitions.map((partition) => ({
partition,
currentLeaderEpoch: -1,
fetchOffset: this.offsetManager.getCurrentOffset(topic, partition),
lastFetchedEpoch: -1,
logStartOffset: -1n,
partitionMaxBytes,
})),
})),
forgottenTopicsData: [],
rackId,
});
}
}
exports.Consumer = Consumer;
__decorate([
trace(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], Consumer.prototype, "start", null);
__decorate([
trace(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], Consumer.prototype, "close", null);
__decorate([
trace(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], Consumer.prototype, "process", null);