kafka-consumer-manager
Version:
Kafka consumer manager handles retries, offset issues and retries
156 lines (141 loc) • 7.44 kB
JavaScript
;
let kafka = require('kafka-node'),
_ = require('lodash');
module.exports = class ConsumerOffsetOutOfSyncChecker {
init(consumerGroup, kafkaOffsetDiffThreshold, logger) {
Object.assign(this, {
logger: logger,
consumer: consumerGroup,
kafkaOffsetDiffThreshold: kafkaOffsetDiffThreshold,
offset: new kafka.Offset(consumerGroup.client),
previousConsumerReadOffset: _.cloneDeep(consumerGroup.topicPayloads)
});
}
validateOffsetsAreSynced() {
return new Promise((resolve, reject) => {
if (!this.previousConsumerReadOffset) {
this.logger.info('Monitor Offset: Skipping check as the consumer is not ready');
return resolve();
}
let notIncrementedTopicPayloads = getNotIncrementedTopicPayloads(this.previousConsumerReadOffset, this.consumer);
if (notIncrementedTopicPayloads.length === 0) {
this.logger.trace('Monitor Offset: Skipping check as offsets change was detected in the consumer', {
previous: this.previousConsumerReadOffset,
current: this.consumer.topicPayloads
});
this.previousConsumerReadOffset = _.cloneDeep(this.consumer.topicPayloads);
return resolve();
} else {
let offsetsPayload = buildOffsetRequestPayloads(notIncrementedTopicPayloads);
this.logger.trace('Monitor Offset: No progress detected in offsets in all partitions since the last check. Checking that the consumer is in sync..');
if (this.consumer.topicPayloads.length > 0) {
this.offset.fetch(offsetsPayload, function (err, zookeeperOffsets) {
if (err) {
this.logger.error(err, 'Monitor Offset: Failed to fetch offsets');
return reject(new Error('Monitor Offset: Failed to fetch offsets:' + err.message));
}
let errorsToHealthCheck = isOffsetsInSync(notIncrementedTopicPayloads, zookeeperOffsets,
this.kafkaOffsetDiffThreshold, this.logger);
if (errorsToHealthCheck) {
return reject(errorsToHealthCheck);
} else {
this.logger.trace('Monitor Offset: Consumer found to be in sync', this.consumer.topicPayloads);
this.previousConsumerReadOffset = _.cloneDeep(this.consumer.topicPayloads);
return resolve();
}
}.bind(this));
} else {
return reject(new Error('Kafka consumer Payloads are empty.'));
}
}
});
}
async registerOffsetGauge(kafkaConsumerGroupOffset, checkerInterval) {
let offsetUpdate = async () => {
let currentOffsetArr = await getOffsetsArray(this.consumer, this.offset, this.logger);
if (currentOffsetArr) {
currentOffsetArr.forEach((offsetObj) => {
kafkaConsumerGroupOffset.set({
topic: offsetObj.topic,
partition: offsetObj.partition
}, offsetObj.offset);
});
}
};
setInterval(offsetUpdate, checkerInterval).unref();
}
};
function buildOffsetRequestPayloads(topicPayloads) {
let offsetRequestPayloads = topicPayloads.map(function (topicPayload) {
return {
topic: topicPayload.topic,
partition: topicPayload.partition,
time: -1
};
});
return offsetRequestPayloads;
}
// Compares the current consumer offsets vs the its previous state
// Returns topic payload that wasn't incremented from last check
function getNotIncrementedTopicPayloads(previousConsumerReadOffset, consumer) {
let notIncrementedTopicPayloads = consumer.topicPayloads.filter((topicPayload) => {
let {topic, partition, offset: currentOffset} = topicPayload;
let previousTopicPayloadForPartition = _.find(previousConsumerReadOffset, {topic, partition});
return previousTopicPayloadForPartition && currentOffset === previousTopicPayloadForPartition.offset;
});
return notIncrementedTopicPayloads;
}
// Compares the consumer offsets vs ZooKeeper's offset
// Will return false if founds a diff
function isOffsetsInSync(notIncrementedTopicPayloads, zookeeperOffsets, kafkaOffsetDiffThreshold, logger) {
logger.trace('Monitor Offset: Topics offsets', zookeeperOffsets);
let lastErrorToHealthCheck;
notIncrementedTopicPayloads.forEach(function (topicPayload) {
let {topic, partition, offset} = topicPayload;
if (zookeeperOffsets && zookeeperOffsets[topic] && zookeeperOffsets[topic][partition]) {
let zkLatestOffset = zookeeperOffsets[topic][partition][0];
let unhandledMessages = zkLatestOffset - offset;
if (unhandledMessages > kafkaOffsetDiffThreshold) {
let state = {
topic: topic,
partition: partition,
partitionLatestOffset: zkLatestOffset,
partitionReadOffset: offset,
unhandledMessages: unhandledMessages
};
logger.error('Monitor Offset: Kafka consumer offsets found to be out of sync', state);
lastErrorToHealthCheck = new Error('Monitor Offset: Kafka consumer offsets found to be out of sync:' + JSON.stringify(state));
}
} else {
logger.error('Monitor Offset: Kafka consumer topics/partitions found to be out of sync in topic: ' + topic + ' and in partition:' + partition);
lastErrorToHealthCheck = new Error('Monitor Offset: Kafka consumer topics/partitions found to be out of sync in topic: ' + topic + ' and in partition:' + partition);
}
});
return lastErrorToHealthCheck;
}
async function getOffsetsArray(consumer, offset, logger) {
if (consumer.topicPayloads && consumer.topicPayloads.length > 0) {
let consumerGroupOffsetDifference = new Array(consumer.topicPayloads.length);
let offsetsPayloads = buildOffsetRequestPayloads(consumer.topicPayloads);
return new Promise(function (resolve, reject) {
offset.fetch(offsetsPayloads, function (err, zookeeperOffsets) {
if (err) {
logger.error(err, 'Monitor Offset: Failed to fetch offsets');
return reject(err);
}
consumer.topicPayloads.forEach((topicPayload, i) => {
let {topic, partition, offset} = topicPayload;
if (zookeeperOffsets && zookeeperOffsets[topic] && zookeeperOffsets[topic][partition]) {
let zkLatestOffset = zookeeperOffsets[topic][partition][0];
consumerGroupOffsetDifference[i] = {
topic: topic,
offset: zkLatestOffset - offset,
partition: partition
};
}
});
return resolve(consumerGroupOffsetDifference);
});
});
}
}