You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

475 lines
12 KiB

'use strict';
var zookeeper = require('node-zookeeper-client');
var util = require('util');
var async = require('async');
var EventEmiter = require('events').EventEmitter;
var debug = require('debug')('kafka-node:zookeeper');
/**
* Provides kafka specific helpers for talking with zookeeper
*
* @param {String} [connectionString='localhost:2181/kafka0.8'] A list of host:port for each zookeeper node and
* optionally a chroot path
*
* @constructor
*/
var Zookeeper = function (connectionString, options) {
this.client = zookeeper.createClient(connectionString, options);
var that = this;
this.client.on('connected', function () {
that.listBrokers();
});
this.client.on('disconnected', function () {
that.emit('disconnected');
});
this.client.connect();
};
util.inherits(Zookeeper, EventEmiter);
Zookeeper.prototype.unregisterConsumer = function (groupId, consumerId, cb) {
var path = '/consumers/' + groupId + '/ids/' + consumerId;
var self = this;
debug('unregister consumer node: %s', path);
async.waterfall([
function (callback) {
self.client.exists(path, callback);
},
function (stat, callback) {
if (stat) {
self.client.remove(path, function (error) {
if (error) {
debug(error);
return callback(error);
}
callback(null, true);
});
} else {
callback(null, false);
}
}
], cb);
};
Zookeeper.prototype.registerConsumer = function (groupId, consumerId, payloads, cb) {
var path = '/consumers/' + groupId;
var that = this;
async.series([
/* eslint-disable handle-callback-err */
function (callback) {
that.client.create(
path,
function (error, path) {
// simply carry on
callback();
});
},
function (callback) {
that.client.create(
path + '/ids',
function (error, path) {
// simply carry on
callback();
});
},
/* eslint-enable handle-callback-err */
function (callback) {
that.client.create(
path + '/ids/' + consumerId,
null,
null,
zookeeper.CreateMode.EPHEMERAL,
function (error, path) {
if (error) {
callback(error);
} else {
callback();
}
});
},
function (callback) {
var metadata = '{"version":1,"subscription":';
metadata += '{';
var sep = '';
payloads.map(function (p) {
metadata += sep + '"' + p.topic + '": 1';
sep = ', ';
});
metadata += '}';
var milliseconds = (new Date()).getTime();
metadata += ',"pattern":"white_list","timestamp":"' + milliseconds + '"}';
that.client.setData(path + '/ids/' + consumerId, new Buffer(metadata), function (error, stat) {
if (error) {
callback(error);
} else {
debug('Node: %s was created.', path + '/ids/' + consumerId);
cb();
}
});
}],
function (err) {
if (err) cb(err);
else cb();
});
};
Zookeeper.prototype.getConsumersPerTopic = function (groupId, cb) {
var consumersPath = '/consumers/' + groupId + '/ids';
var brokerTopicsPath = '/brokers/topics';
var that = this;
var consumerPerTopicMap = new ZookeeperConsumerMappings();
async.series([
function (callback) {
that.client.getChildren(consumersPath, function (error, children, stats) {
if (error) {
callback(error);
return;
} else {
debug('Children are: %j.', children);
async.each(children, function (consumer, cbb) {
var path = consumersPath + '/' + consumer;
that.client.getData(
path,
function (error, data) {
if (error) {
cbb(error);
} else {
try {
var obj = JSON.parse(data.toString());
// For each topic
for (var topic in obj.subscription) {
if (consumerPerTopicMap.topicConsumerMap[topic] == null) {
consumerPerTopicMap.topicConsumerMap[topic] = [];
}
consumerPerTopicMap.topicConsumerMap[topic].push(consumer);
if (consumerPerTopicMap.consumerTopicMap[consumer] == null) {
consumerPerTopicMap.consumerTopicMap[consumer] = [];
}
consumerPerTopicMap.consumerTopicMap[consumer].push(topic);
}
cbb();
} catch (e) {
debug(e);
cbb(new Error('Unable to assemble data'));
}
}
}
);
}, function (err) {
if (err) {
callback(err);
} else {
callback();
}
});
}
});
},
function (callback) {
Object.keys(consumerPerTopicMap.topicConsumerMap).forEach(function (key) {
consumerPerTopicMap.topicConsumerMap[key] = consumerPerTopicMap.topicConsumerMap[key].sort();
});
callback();
},
function (callback) {
async.each(Object.keys(consumerPerTopicMap.topicConsumerMap), function (topic, cbb) {
var path = brokerTopicsPath + '/' + topic;
that.client.getData(
path,
function (error, data) {
if (error) {
cbb(error);
} else {
var obj = JSON.parse(data.toString());
// Get the topic partitions
var partitions = Object.keys(obj.partitions).map(function (partition) {
return partition;
});
consumerPerTopicMap.topicPartitionMap[topic] = partitions.sort(compareNumbers);
cbb();
}
}
);
}, function (err) {
if (err) {
callback(err);
} else {
callback();
}
});
}],
function (err) {
if (err) {
debug(err);
cb(err);
} else cb(null, consumerPerTopicMap);
});
};
function compareNumbers (a, b) {
return a - b;
}
Zookeeper.prototype.listPartitions = function (topic) {
var self = this;
var path = '/brokers/topics/' + topic + '/partitions';
this.client.getChildren(
path,
function () {
if (!self.closed) {
self.listPartitions(topic);
}
},
function (error, children) {
if (error) {
debug(error);
// Ignore NO_NODE error here #157
if (error.name !== 'NO_NODE') {
self.emit('error', error);
}
} else {
if (self.initPartitions) {
return self.emit('partitionsChanged');
}
self.initPartitions = true;
}
}
);
};
Zookeeper.prototype.listBrokers = function (cb) {
var that = this;
var path = '/brokers/ids';
this.client.getChildren(
path,
function () {
that.listBrokers();
},
function (error, children) {
if (error) {
debug('Failed to list children of node: %s due to: %s.', path, error);
that.emit('error', error);
return;
}
if (children.length) {
var brokers = {};
async.each(children, getBrokerDetail, function (err) {
if (err) {
that.emit('error', err);
return;
}
if (!that.inited) {
that.emit('init', brokers);
that.inited = true;
} else {
that.emit('brokersChanged', brokers);
}
cb && cb(brokers); // For test
});
} else {
if (that.inited) {
return that.emit('brokersChanged', {});
}
that.inited = true;
that.emit('init', {});
}
function getBrokerDetail (id, cb) {
var path = '/brokers/ids/' + id;
that.client.getData(path, function (err, data) {
if (err) return cb(err);
brokers[id] = JSON.parse(data.toString());
cb();
});
}
}
);
};
Zookeeper.prototype.isConsumerRegistered = function (groupId, consumerId, callback) {
var path = '/consumers/' + groupId + '/ids/' + consumerId;
this.client.exists(path, function (error, stat) {
if (error) {
return callback(error);
}
callback(null, !!stat);
});
};
Zookeeper.prototype.listConsumers = function (groupId) {
var that = this;
var path = '/consumers/' + groupId + '/ids';
this.client.getChildren(
path,
function () {
if (!that.closed) {
that.listConsumers(groupId);
}
},
function (error, children) {
if (error) {
debug(error);
// Ignore NO_NODE error here #157
if (error.name !== 'NO_NODE') {
that.emit('error', error);
}
} else {
if (that.listConsumersInitialized) {
that.emit('consumersChanged');
}
}
that.listConsumersInitialized = true;
}
);
};
Zookeeper.prototype.topicExists = function (topic, cb, watch) {
var path = '/brokers/topics/' + topic;
var self = this;
this.client.exists(
path,
function (event) {
debug('Got event: %s.', event);
if (watch) {
self.topicExists(topic, cb);
}
},
function (error, stat) {
if (error) return cb(error);
cb(null, !!stat, topic);
}
);
};
Zookeeper.prototype.deletePartitionOwnership = function (groupId, topic, partition, cb) {
var path = '/consumers/' + groupId + '/owners/' + topic + '/' + partition;
this.client.remove(
path,
function (error) {
if (error) {
cb(error);
} else {
debug('Removed partition ownership %s', path);
cb();
}
}
);
};
Zookeeper.prototype.addPartitionOwnership = function (consumerId, groupId, topic, partition, cb) {
var path = '/consumers/' + groupId + '/owners/' + topic + '/' + partition;
var self = this;
async.series([
/* eslint-disable handle-callback-err */
function (callback) {
self.client.create(
'/consumers/' + groupId,
function (error, path) {
// simply carry on
callback();
});
},
function (callback) {
self.client.create(
'/consumers/' + groupId + '/owners',
function (error, path) {
// simply carry on
callback();
});
},
function (callback) {
self.client.create(
'/consumers/' + groupId + '/owners/' + topic,
function (error, path) {
// simply carry on
callback();
});
},
/* eslint-enable handle-callback-err */
function (callback) {
self.client.create(
path,
new Buffer(consumerId),
null,
zookeeper.CreateMode.EPHEMERAL,
function (error, path) {
if (error) {
callback(error);
} else callback();
});
},
function (callback) {
self.client.exists(path, null, function (error, stat) {
if (error) {
callback(error);
} else if (stat) {
debug('Gained ownership of %s by %s.', path, consumerId);
callback();
} else {
callback("Path wasn't created");
}
});
}],
function (err) {
if (err) cb(err);
else cb();
});
};
Zookeeper.prototype.checkPartitionOwnership = function (consumerId, groupId, topic, partition, cb) {
var path = '/consumers/' + groupId + '/owners/' + topic + '/' + partition;
var self = this;
async.series([
function (callback) {
self.client.exists(path, null, function (error, stat) {
if (error) {
callback(error);
} else if (stat) {
callback();
} else {
callback("Path wasn't created");
}
});
},
function (callback) {
self.client.getData(
path,
function (error, data) {
if (error) {
callback(error);
} else {
if (consumerId !== data.toString()) {
callback('Consumer not registered ' + consumerId);
} else callback();
}
}
);
}],
function (err) {
if (err) cb(err);
else cb();
});
};
Zookeeper.prototype.close = function () {
this.closed = true;
this.client.close();
};
var ZookeeperConsumerMappings = function () {
this.consumerTopicMap = {};
this.topicConsumerMap = {};
this.topicPartitionMap = {};
};
exports.Zookeeper = Zookeeper;
exports.ZookeeperConsumerMappings = ZookeeperConsumerMappings;