kafka-node
Version:
Client for Apache Kafka v0.9.x, v0.10.x and v0.11.x
657 lines (556 loc) • 19.3 kB
JavaScript
'use strict';
var util = require('util');
var _ = require('lodash');
var async = require('async');
var retry = require('retry');
var EventEmitter = require('events');
var errors = require('./errors');
var getCodec = require('./codec');
var protocol = require('./protocol');
var encodeMessageSet = protocol.encodeMessageSet;
var Message = protocol.Message;
var logger = require('./logging')('kafka-node:BaseClient');
var validateKafkaTopics = require('./utils').validateTopicNames;
const MAX_INT32 = 2147483647;
/**
*
* @constructor
*/
function Client () {
throw new TypeError('BaseClient cannot be instantiated directly');
}
util.inherits(Client, EventEmitter);
Client.prototype.closeBrokers = function (brokers) {
_.each(brokers, function (broker) {
broker.socket.closing = true;
broker.socket.end();
setImmediate(function () {
broker.socket.destroy();
broker.socket.unref();
});
});
};
function decodeValue (encoding, value) {
if (encoding !== 'buffer' && value != null) {
return value.toString(encoding);
}
return value;
}
Client.prototype._createMessageHandler = function (consumer, stateValidator) {
return (err, type, message) => {
if (stateValidator && !stateValidator(err, type, message)) {
return;
}
if (err) {
if (err.message === 'OffsetOutOfRange') {
return consumer.emit('offsetOutOfRange', err);
} else if (err.message === 'NotLeaderForPartition' || err.message === 'UnknownTopicOrPartition') {
return this.emit('brokersChanged');
}
return consumer.emit('error', err);
}
var encoding = consumer.options.encoding;
const keyEncoding = consumer.options.keyEncoding;
if (type === 'message') {
message.value = decodeValue(encoding, message.value);
message.key = decodeValue(keyEncoding || encoding, message.key);
consumer.emit('message', message);
} else {
consumer.emit(type, message);
}
};
};
Client.prototype.sendFetchRequest = function (consumer, payloads, fetchMaxWaitMs, fetchMinBytes, maxTickMessages) {
var encoder = protocol.encodeFetchRequest(fetchMaxWaitMs, fetchMinBytes);
// TODO: state validator for HLC for ignoring stale fetch requests
var decoder = protocol.decodeFetchResponse(this._createMessageHandler(consumer), maxTickMessages);
this.send(payloads, encoder, decoder, function (err) {
if (err) {
Array.prototype.unshift.call(arguments, 'error');
consumer.emit.apply(consumer, arguments);
}
});
};
Client.prototype.sendProduceRequest = function (payloads, requireAcks, ackTimeoutMs, cb) {
var encoder = protocol.encodeProduceRequest(requireAcks, ackTimeoutMs);
var decoder = protocol.decodeProduceResponse;
var self = this;
decoder.requireAcks = requireAcks;
async.each(payloads, buildRequest, function (err) {
if (err) return cb(err);
self.send(payloads, encoder, decoder, function (err, result) {
if (err) {
if (err.message === 'NotLeaderForPartition' || err.message === 'UnknownTopicOrPartition') {
self.emit('brokersChanged');
}
cb(err);
} else {
cb(null, result);
}
});
});
function buildRequest (payload, cb) {
var attributes = payload.attributes;
var codec = getCodec(attributes);
if (!codec) return cb();
var innerSet = encodeMessageSet(payload.messages);
codec.encode(innerSet, function (err, message) {
if (err) return cb(err);
payload.messages = [new Message(0, attributes, '', message)];
cb();
});
}
};
Client.prototype.sendOffsetCommitRequest = function (group, payloads, cb) {
var encoder = protocol.encodeOffsetCommitRequest(group);
var decoder = protocol.decodeOffsetCommitResponse;
this.send(payloads, encoder, decoder, cb);
};
Client.prototype.sendOffsetCommitV2Request = function (group, generationId, memberId, payloads, cb) {
var encoder = protocol.encodeOffsetCommitV2Request;
var decoder = protocol.decodeOffsetCommitResponse;
this.sendGroupRequest(encoder, decoder, arguments);
};
Client.prototype.sendOffsetFetchV1Request = function (group, payloads, cb) {
var encoder = protocol.encodeOffsetFetchV1Request;
var decoder = protocol.decodeOffsetFetchV1Response;
this.sendGroupRequest(encoder, decoder, arguments);
};
Client.prototype.setCoordinatorIdAndSendOffsetFetchV1Request = function (group, payloads, cb) {
this.sendGroupCoordinatorRequest(group, (err, coordinatorInfo) => {
if (err) return cb(new errors.BrokerNotAvailableError('Broker not available'));
this.coordinatorId = String(coordinatorInfo.coordinatorId);
this.sendOffsetFetchV1Request(group, payloads, cb);
});
};
Client.prototype.sendOffsetFetchRequest = function (group, payloads, cb) {
var encoder = protocol.encodeOffsetFetchRequest(group);
var decoder = protocol.decodeOffsetFetchResponse;
this.send(payloads, encoder, decoder, cb);
};
Client.prototype.sendOffsetRequest = function (payloads, cb) {
var encoder = protocol.encodeOffsetRequest;
var decoder = protocol.decodeOffsetResponse;
this.send(payloads, encoder, decoder, cb);
};
Client.prototype.refreshBrokerMetadata = function () {};
Client.prototype.sendWhenReady = function (broker, correlationId, request, decode, cb) {
this.queueCallback(broker.socket, correlationId, [decode, cb]);
broker.write(request);
};
Client.prototype.sendGroupRequest = function (encode, decode, requestArgs) {
requestArgs = _.values(requestArgs);
var cb = requestArgs.pop();
var correlationId = this.nextId();
requestArgs.unshift(this.clientId, correlationId);
var request = encode.apply(null, requestArgs);
var broker = this.brokerForLeader(this.coordinatorId);
var brokerError = null;
if (!broker) {
brokerError = 'Could not find broker';
} else if (!broker.isConnected()) {
brokerError = 'Broker socket is closed' + (broker.socket.error ? ' - ' + broker.socket.error.message : '');
}
if (brokerError) {
this.refreshBrokerMetadata();
return cb(new errors.BrokerNotAvailableError('Broker not available: ' + brokerError));
}
this.sendWhenReady(broker, correlationId, request, decode, cb);
};
Client.prototype.sendGroupCoordinatorRequest = function (groupId, cb) {
this.sendGroupRequest(protocol.encodeGroupCoordinatorRequest, protocol.decodeGroupCoordinatorResponse, arguments);
};
Client.prototype.sendJoinGroupRequest = function (groupId, memberId, sessionTimeout, groupProtocol, cb) {
this.sendGroupRequest(protocol.encodeJoinGroupRequest, protocol.decodeJoinGroupResponse, arguments);
};
Client.prototype.sendSyncGroupRequest = function (groupId, generationId, memberId, groupAssignment, cb) {
this.sendGroupRequest(protocol.encodeSyncGroupRequest, protocol.decodeSyncGroupResponse, arguments);
};
Client.prototype.sendHeartbeatRequest = function (groupId, generationId, memberId, cb) {
this.sendGroupRequest(protocol.encodeGroupHeartbeatRequest, protocol.decodeGroupHeartbeatResponse, arguments);
};
Client.prototype.sendLeaveGroupRequest = function (groupId, memberId, cb) {
this.sendGroupRequest(protocol.encodeLeaveGroupRequest, protocol.decodeLeaveGroupResponse, arguments);
};
/*
* Helper method
* topic in payloads may send to different broker, so we cache data util all request came back
*/
function wrap (payloads, cb) {
var out = {};
var count = Object.keys(payloads).length;
return function (err, data) {
// data: { topicName1: {}, topicName2: {} }
if (err) return cb && cb(err);
_.merge(out, data);
count -= 1;
// Waiting for all request return
if (count !== 0) return;
cb && cb(null, out);
};
}
/**
* Fetches metadata information for a topic
* This includes an array containing a each zookeeper node, their nodeId, host name, and port. As well as an object
* containing the topic name, partition, leader number, replica count, and in sync replicas per partition.
*
* @param {Array} topics An array of topics to load the metadata for
* @param {Client~loadMetadataForTopicsCallback} cb Function to call once all metadata is loaded
*/
Client.prototype.loadMetadataForTopics = function (topics, cb) {
var correlationId = this.nextId();
var request = protocol.encodeMetadataRequest(this.clientId, correlationId, topics);
var broker = this.brokerForLeader();
if (!broker || !broker.isConnected()) {
return cb(new errors.BrokerNotAvailableError('Broker not available'));
}
this.sendWhenReady(broker, correlationId, request, protocol.decodeMetadataResponse, cb);
};
Client.prototype.createTopics = function (topics, isAsync, cb) {
topics = typeof topics === 'string' ? [topics] : topics;
if (typeof isAsync === 'function' && typeof cb === 'undefined') {
cb = isAsync;
isAsync = true;
}
try {
validateKafkaTopics(topics);
} catch (e) {
if (isAsync) return cb(e);
throw e;
}
cb = _.once(cb);
const getTopicsFromKafka = (topics, callback) => {
this.loadMetadataForTopics(topics, function (error, resp) {
if (error) {
return callback(error);
}
callback(null, Object.keys(resp[1].metadata));
});
};
const operation = retry.operation({ minTimeout: 200, maxTimeout: 2000 });
operation.attempt(currentAttempt => {
logger.debug('create topics currentAttempt', currentAttempt);
getTopicsFromKafka(topics, function (error, kafkaTopics) {
if (error) {
if (operation.retry(error)) {
return;
}
}
logger.debug('kafka reported topics', kafkaTopics);
const left = _.difference(topics, kafkaTopics);
if (left.length === 0) {
logger.debug(`Topics created ${kafkaTopics}`);
return cb(null, kafkaTopics);
}
logger.debug(`Topics left ${left.join(', ')}`);
if (!operation.retry(new Error(`Topics not created ${left}`))) {
cb(operation.mainError());
}
});
});
if (!isAsync) {
cb(null);
}
};
Client.prototype.addTopics = function (topics, cb) {
var self = this;
this.topicExists(topics, function (err) {
if (err) return cb(err);
self.loadMetadataForTopics(topics, function (err, resp) {
if (err) return cb(err);
self.updateMetadatas(resp);
cb(null, topics);
});
});
};
Client.prototype.nextId = function () {
if (this.correlationId >= MAX_INT32) {
this.correlationId = 0;
}
return this.correlationId++;
};
Client.prototype.nextSocketId = function () {
return this._socketId++;
};
Client.prototype.refreshBrokers = function () {
var self = this;
var validBrokers = Object.keys(this.brokerProfiles);
function closeDeadBrokers (brokers) {
var deadBrokerKeys = _.difference(Object.keys(brokers), validBrokers);
if (deadBrokerKeys.length) {
self.closeBrokers(
deadBrokerKeys.map(function (key) {
var broker = brokers[key];
delete brokers[key];
return broker;
})
);
}
}
closeDeadBrokers(this.brokers);
closeDeadBrokers(this.longpollingBrokers);
};
Client.prototype.refreshMetadata = function (topicNames, cb) {
var self = this;
if (!topicNames.length) return cb();
attemptRequestMetadata(topicNames, cb);
function attemptRequestMetadata (topics, cb) {
var operation = retry.operation({ minTimeout: 200, maxTimeout: 1000 });
operation.attempt(function (currentAttempt) {
logger.debug('refresh metadata currentAttempt', currentAttempt);
self.loadMetadataForTopics(topics, function (err, resp) {
err = err || resp[1].error;
if (Array.isArray(err)) {
err = new Error(String(err));
}
if (operation.retry(err)) {
return;
}
if (err) {
logger.debug('refresh metadata error', err.message);
return cb(err);
}
self.updateMetadatas(resp);
cb();
});
});
}
};
Client.prototype.send = function (payloads, encoder, decoder, cb) {
var self = this;
var _payloads = payloads;
// payloads: [ [metadata exists], [metadata not exists] ]
payloads = this.checkMetadatas(payloads);
if (payloads[0].length && !payloads[1].length) {
this.sendToBroker(_.flatten(payloads), encoder, decoder, cb);
return;
}
if (payloads[1].length) {
var topicNames = payloads[1].map(function (p) {
return p.topic;
});
this.loadMetadataForTopics(topicNames, function (err, resp) {
if (err) {
return cb(err);
}
var error = resp[1].error;
if (error) {
return cb(error);
}
self.updateMetadatas(resp);
// check payloads again
payloads = self.checkMetadatas(_payloads);
if (payloads[1].length) {
self.refreshBrokerMetadata();
return cb(new errors.BrokerNotAvailableError('Could not find the leader'));
}
self.sendToBroker(payloads[1].concat(payloads[0]), encoder, decoder, cb);
});
}
};
Client.prototype.sendToBroker = function (payloads, encoder, decoder, cb) {
var longpolling = encoder.name === 'encodeFetchRequest';
payloads = this.payloadsByLeader(payloads);
if (!longpolling) {
cb = wrap(payloads, cb);
}
for (var leader in payloads) {
if (!payloads.hasOwnProperty(leader)) {
continue;
}
var correlationId = this.nextId();
var broker = this.brokerForLeader(leader, longpolling);
var brokerError = null;
if (!broker) {
brokerError = 'Could not find broker';
} else if (!broker.isConnected()) {
brokerError = 'Broker socket is closed' + (broker.socket.error ? ' - ' + broker.socket.error.message : '');
}
if (brokerError) {
this.refreshBrokerMetadata();
return cb(new errors.BrokerNotAvailableError('Broker not available: ' + brokerError), payloads[leader]);
}
if (longpolling) {
if (broker.socket.waiting) {
continue;
}
broker.socket.waiting = true;
}
var request = encoder(this.clientId, correlationId, payloads[leader]);
if (decoder.requireAcks === 0) {
broker.writeAsync(request);
cb(null, { result: 'no ack' });
} else {
this.sendWhenReady(broker, correlationId, request, decoder, cb);
}
}
};
Client.prototype.checkMetadatas = function (payloads) {
if (_.isEmpty(this.topicMetadata)) return [[], payloads];
// out: [ [metadata exists], [metadata not exists] ]
var out = [[], []];
payloads.forEach(
function (p) {
if (this.hasMetadata(p.topic, p.partition)) out[0].push(p);
else out[1].push(p);
}.bind(this)
);
return out;
};
Client.prototype.hasMetadata = function (topic, partition) {
var brokerMetadata = this.brokerMetadata;
var leader = this.leaderByPartition(topic, partition);
return leader !== undefined && brokerMetadata[leader];
};
Client.prototype.updateMetadatas = function (metadatas) {
// _.extend(this.brokerMetadata, metadatas[0])
_.extend(this.topicMetadata, metadatas[1].metadata);
for (var topic in this.topicMetadata) {
if (!this.topicMetadata.hasOwnProperty(topic)) {
continue;
}
this.topicPartitions[topic] = Object.keys(this.topicMetadata[topic]).map(function (val) {
return parseInt(val, 10);
});
}
};
Client.prototype.removeTopicMetadata = function (topics, cb) {
topics.forEach(
function (t) {
if (this.topicMetadata[t]) delete this.topicMetadata[t];
}.bind(this)
);
cb(null, topics.length);
};
Client.prototype.payloadsByLeader = function (payloads) {
return payloads.reduce(
function (out, p) {
var leader = this.leaderByPartition(p.topic, p.partition);
out[leader] = out[leader] || [];
out[leader].push(p);
return out;
}.bind(this),
{}
);
};
Client.prototype.leaderByPartition = function (topic, partition) {
var topicMetadata = this.topicMetadata;
return topicMetadata[topic] && topicMetadata[topic][partition] && topicMetadata[topic][partition].leader;
};
Client.prototype.brokerForLeader = function (leader, longpolling) {
var addr;
var brokers = this.getBrokers(longpolling);
// If leader is not give, choose the first broker as leader
if (typeof leader === 'undefined') {
if (!_.isEmpty(brokers)) {
addr = Object.keys(brokers)[0];
return brokers[addr];
} else if (!_.isEmpty(this.brokerMetadata)) {
leader = Object.keys(this.brokerMetadata)[0];
} else {
return;
}
}
var broker = _.find(this.brokerProfiles, { id: leader });
if (!broker) {
return;
}
addr = broker.host + ':' + broker.port;
return brokers[addr] || this.setupBroker(broker.host, broker.port, longpolling, brokers);
};
Client.prototype.getBrokers = function (longpolling) {
return longpolling ? this.longpollingBrokers : this.brokers;
};
Client.prototype.reconnectBroker = function (oldSocket) {
oldSocket.retrying = false;
if (oldSocket.error) {
oldSocket.destroy();
}
var brokers = this.getBrokers(oldSocket.longpolling);
var newBroker = this.setupBroker(oldSocket.host, oldSocket.port, oldSocket.longpolling, brokers);
newBroker.socket.error = oldSocket.error;
};
Client.prototype.handleReceivedData = function (socket) {
var buffer = socket.buffer;
if (!buffer.length || buffer.length < 4) {
return;
}
var size = buffer.readUInt32BE(0) + 4;
if (buffer.length >= size) {
if (socket.longpolling) {
socket.waiting = false;
}
var resp = buffer.shallowSlice(0, size);
var correlationId = resp.readUInt32BE(4);
this.invokeResponseCallback(socket, correlationId, resp);
buffer.consume(size);
} else {
return;
}
if (socket.buffer.length) {
setImmediate(
function () {
this.handleReceivedData(socket);
}.bind(this)
);
}
};
Client.prototype.invokeResponseCallback = function (socket, correlationId, resp) {
var handlers = this.unqueueCallback(socket, correlationId);
if (handlers) {
var [decoder, cb] = handlers;
var result = decoder(resp);
if (result instanceof Error) {
cb.call(this, result);
} else {
cb.call(this, null, result);
}
} else {
logger.error(`missing handlers for Correlation ID: ${correlationId}`);
}
};
Client.prototype.queueCallback = function (socket, id, data) {
var socketId = socket.socketId;
var queue = this.cbqueue.get(socketId);
if (!queue) {
queue = new Map();
this.cbqueue.set(socketId, queue);
}
queue.set(id, data);
};
Client.prototype.unqueueCallback = function (socket, id) {
var socketId = socket.socketId;
var queue = this.cbqueue.get(socketId);
try {
if (!queue) {
return null;
}
if (!queue.has(id)) {
return null;
}
var result = queue.get(id);
// cleanup socket queue
queue.delete(id);
return result;
} finally {
if (queue && !queue.size) {
this.cbqueue.delete(socketId);
}
}
};
Client.prototype.clearCallbackQueue = function (socket, error) {
var socketId = socket.socketId;
var longpolling = socket.longpolling;
var queue = this.cbqueue.get(socketId);
if (!queue) {
return;
}
if (!longpolling) {
queue.forEach(function (handlers) {
var cb = handlers[1];
cb(error);
});
}
this.cbqueue.delete(socketId);
};
module.exports = Client;