/*
|
|
* node-rdkafka - Node.js wrapper for RdKafka C/C++ library
|
|
*
|
|
* Copyright (c) 2016 Blizzard Entertainment
|
|
*
|
|
* This software may be modified and distributed under the terms
|
|
* of the MIT license. See the LICENSE.txt file for details.
|
|
*/
|
|
|
|
var crypto = require('crypto');
|
|
var t = require('assert');
|
|
|
|
var Kafka = require('../');
|
|
var kafkaBrokerList = process.env.KAFKA_HOST || 'localhost:9092';
|
|
var eventListener = require('./listener');
|
|
var topic = 'test';
|
|
var topic2 = 'test2';
|
|
|
|
describe('Consumer/Producer', function() {
|
|
|
|
var producer;
|
|
var consumer;
|
|
|
|
beforeEach(function(done) {
|
|
var finished = 0;
|
|
var called = false;
|
|
|
|
function maybeDone(err) {
|
|
if (called) {
|
|
return;
|
|
}
|
|
|
|
finished++;
|
|
if (err) {
|
|
called = true;
|
|
return done(err);
|
|
}
|
|
|
|
if (finished === 2) {
|
|
done();
|
|
}
|
|
}
|
|
|
|
var grp = 'kafka-mocha-grp-' + crypto.randomBytes(20).toString('hex');
|
|
|
|
consumer = new Kafka.KafkaConsumer({
|
|
'metadata.broker.list': kafkaBrokerList,
|
|
'group.id': grp,
|
|
'fetch.wait.max.ms': 1000,
|
|
'session.timeout.ms': 10000,
|
|
'enable.auto.commit': true,
|
|
'enable.partition.eof': true,
|
|
'debug': 'all'
|
|
// paused: true,
|
|
}, {
|
|
'auto.offset.reset': 'largest'
|
|
});
|
|
|
|
consumer.connect({}, function(err, d) {
|
|
t.ifError(err);
|
|
t.equal(typeof d, 'object', 'metadata should be returned');
|
|
maybeDone(err);
|
|
});
|
|
|
|
eventListener(consumer);
|
|
|
|
producer = new Kafka.Producer({
|
|
'client.id': 'kafka-mocha',
|
|
'metadata.broker.list': kafkaBrokerList,
|
|
'fetch.wait.max.ms': 1,
|
|
'debug': 'all',
|
|
'dr_cb': true
|
|
}, {
|
|
'produce.offset.report': true
|
|
});
|
|
|
|
producer.connect({}, function(err, d) {
|
|
t.ifError(err);
|
|
t.equal(typeof d, 'object', 'metadata should be returned');
|
|
maybeDone(err);
|
|
});
|
|
|
|
eventListener(producer);
|
|
|
|
});
|
|
|
|
afterEach(function(done) {
|
|
this.timeout(6000);
|
|
var finished = 0;
|
|
var called = false;
|
|
|
|
function maybeDone(err) {
|
|
if (called) {
|
|
return;
|
|
}
|
|
|
|
finished++;
|
|
if (err) {
|
|
called = true;
|
|
return done(err);
|
|
}
|
|
|
|
if (finished === 2) {
|
|
done();
|
|
}
|
|
}
|
|
|
|
consumer.disconnect(function(err) {
|
|
maybeDone(err);
|
|
});
|
|
|
|
producer.disconnect(function(err) {
|
|
maybeDone(err);
|
|
});
|
|
});
|
|
|
|
it('should be able to produce, consume messages, read position: subscribe/consumeOnce', function(done) {
|
|
this.timeout(8000);
|
|
crypto.randomBytes(4096, function(ex, buffer) {
|
|
producer.setPollInterval(10);
|
|
|
|
var offset;
|
|
|
|
producer.once('delivery-report', function(err, report) {
|
|
t.ifError(err);
|
|
offset = report.offset;
|
|
});
|
|
|
|
consumer.setDefaultConsumeTimeout(10);
|
|
consumer.subscribe([topic]);
|
|
|
|
var ct;
|
|
|
|
var consumeOne = function() {
|
|
consumer.consume(1, function(err, messages) {
|
|
if (err && err.code === -185) {
|
|
ct = setTimeout(consumeOne, 100);
|
|
return;
|
|
} else if (messages.length === 0 || (err && err.code === -191)) {
|
|
producer.produce(topic, null, buffer, null);
|
|
ct = setTimeout(consumeOne, 100);
|
|
return;
|
|
} else if (err) {
|
|
return;
|
|
}
|
|
|
|
var message = messages[0];
|
|
|
|
t.equal(Array.isArray(consumer.assignments()), true, 'Assignments should be an array');
|
|
t.equal(consumer.assignments().length > 0, true, 'Should have at least one assignment');
|
|
t.equal(buffer.toString(), message.value.toString(),
|
|
'message is not equal to buffer');
|
|
|
|
// test consumer.position as we have consumed
|
|
var position = consumer.position();
|
|
t.equal(position.length, 1);
|
|
t.deepStrictEqual(position[0].partition, 0);
|
|
t.ok(position[0].offset >= 0);
|
|
done();
|
|
});
|
|
};
|
|
|
|
// Consume until we get it or time out
|
|
consumeOne();
|
|
|
|
});
|
|
});
|
|
|
|
it('should return ready messages on partition EOF', function(done) {
|
|
this.timeout(8000);
|
|
crypto.randomBytes(4096, function(ex, buffer) {
|
|
producer.setPollInterval(10);
|
|
|
|
producer.once('delivery-report', function(err, report) {
|
|
t.ifError(err);
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
|
|
var consumeAll = function() {
|
|
// Make sure we get the message fast when consuming with large timeout
|
|
consumer.setDefaultConsumeTimeout(1000000);
|
|
consumer.consume(100000, function(err, messages) {
|
|
t.ifError(err);
|
|
t.equal(messages.length, 1);
|
|
done();
|
|
});
|
|
};
|
|
|
|
var consumeNone = function() {
|
|
// With no new messages, the consume should wait whole timeout
|
|
var start = Date.now();
|
|
// Set the timeout to 2000ms to see that it actually waits the whole time
|
|
// (Needs to be higher than fetch.max.wait.ms which is 1000 here
|
|
// to ensure we don't only wait that long)
|
|
consumer.setDefaultConsumeTimeout(2000);
|
|
consumer.consume(100000, function(err, messages) {
|
|
t.ifError(err);
|
|
t.ok(Date.now() - start >= 1998);
|
|
t.equal(messages.length, 0);
|
|
|
|
// Produce one message to cause EOF with waiting message when consuming all
|
|
producer.produce(topic, null, buffer, null);
|
|
consumeAll();
|
|
});
|
|
};
|
|
|
|
consumeNone();
|
|
});
|
|
});
|
|
|
|
it('should emit partition.eof event when reaching end of partition', function(done) {
|
|
this.timeout(8000);
|
|
crypto.randomBytes(4096, function(ex, buffer) {
|
|
producer.setPollInterval(10);
|
|
|
|
producer.once('delivery-report', function(err, report) {
|
|
t.ifError(err);
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
|
|
var events = [];
|
|
|
|
consumer.once('data', function(msg) {
|
|
events.push("data");
|
|
});
|
|
|
|
consumer.once('partition.eof', function(eof) {
|
|
events.push("partition.eof");
|
|
});
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, buffer, null);
|
|
}, 500)
|
|
consumer.setDefaultConsumeTimeout(2000);
|
|
consumer.consume(1000, function(err, messages) {
|
|
t.ifError(err);
|
|
t.equal(messages.length, 1);
|
|
t.deepStrictEqual(events, ["data", "partition.eof"]);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should emit partition.eof when already at end of partition', function(done) {
|
|
this.timeout(8000);
|
|
crypto.randomBytes(4096, function(ex, buffer) {
|
|
producer.setPollInterval(10);
|
|
|
|
producer.once('delivery-report', function(err, report) {
|
|
t.ifError(err);
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
|
|
var events = [];
|
|
|
|
consumer.once('data', function(msg) {
|
|
events.push("data");
|
|
});
|
|
|
|
consumer.on('partition.eof', function(eof) {
|
|
events.push("partition.eof");
|
|
});
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, buffer, null);
|
|
}, 2000)
|
|
consumer.setDefaultConsumeTimeout(3000);
|
|
consumer.consume(1000, function(err, messages) {
|
|
t.ifError(err);
|
|
t.equal(messages.length, 1);
|
|
t.deepStrictEqual(events, ["partition.eof", "data", "partition.eof"]);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should be able to produce and consume messages: consumeLoop', function(done) {
|
|
var key = 'key';
|
|
|
|
this.timeout(5000);
|
|
|
|
crypto.randomBytes(4096, function(ex, buffer) {
|
|
|
|
producer.setPollInterval(10);
|
|
|
|
producer.once('delivery-report', function(err, report) {
|
|
if (!err) {
|
|
t.equal(topic, report.topic, 'invalid delivery-report topic');
|
|
t.equal(key, report.key, 'invalid delivery-report key');
|
|
t.ok(report.offset >= 0, 'invalid delivery-report offset');
|
|
}
|
|
});
|
|
|
|
consumer.on('data', function(message) {
|
|
t.equal(buffer.toString(), message.value.toString(), 'invalid message value');
|
|
t.equal(key, message.key, 'invalid message key');
|
|
t.equal(topic, message.topic, 'invalid message topic');
|
|
t.ok(message.offset >= 0, 'invalid message offset');
|
|
done();
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, buffer, key);
|
|
}, 2000);
|
|
|
|
});
|
|
});
|
|
|
|
it('should emit \'partition.eof\' events in consumeLoop', function(done) {
|
|
this.timeout(7000);
|
|
|
|
crypto.randomBytes(4096, function(ex, buffer) {
|
|
producer.setPollInterval(10);
|
|
|
|
producer.once('delivery-report', function(err, report) {
|
|
t.ifError(err);
|
|
});
|
|
|
|
|
|
var events = [];
|
|
var offsets = [];
|
|
|
|
consumer.on('data', function(message) {
|
|
t.equal(message.topic, topic);
|
|
t.equal(message.partition, 0);
|
|
offsets.push(message.offset);
|
|
events.push('data');
|
|
});
|
|
|
|
consumer.on('partition.eof', function(eofEvent) {
|
|
t.equal(eofEvent.topic, topic);
|
|
t.equal(eofEvent.partition, 0);
|
|
offsets.push(eofEvent.offset);
|
|
events.push('partition.eof');
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, buffer);
|
|
}, 2000);
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, buffer);
|
|
}, 4000);
|
|
|
|
setTimeout(function() {
|
|
t.deepStrictEqual(events, ['partition.eof', 'data', 'partition.eof', 'data', 'partition.eof']);
|
|
var startOffset = offsets[0];
|
|
t.deepStrictEqual(offsets,
|
|
[ startOffset,
|
|
startOffset,
|
|
startOffset + 1,
|
|
startOffset + 1,
|
|
startOffset + 2 ]);
|
|
done();
|
|
}, 6000);
|
|
});
|
|
});
|
|
|
|
it('should emit [warning] event on UNKNOWN_TOPIC_OR_PART error: consumeLoop', function(done) {
|
|
consumer.on('warning', function (err) {
|
|
if (err.code === Kafka.CODES.ERRORS.ERR_UNKNOWN_TOPIC_OR_PART) {
|
|
consumer.disconnect(function() {
|
|
done();
|
|
});
|
|
} else {
|
|
t.ifError(err);
|
|
}
|
|
});
|
|
|
|
consumer.subscribe(['non_existing_topic']);
|
|
consumer.consume();
|
|
});
|
|
|
|
it('should be able to produce and consume messages with one header value as string: consumeLoop', function(done) {
|
|
var headers = [
|
|
{ key: "value" }
|
|
];
|
|
this.timeout(5000);
|
|
run_headers_test(done, headers);
|
|
});
|
|
|
|
it('should be able to produce and consume messages with one header value as buffer: consumeLoop', function(done) {
|
|
var headers = [
|
|
{ key: Buffer.from('value') }
|
|
];
|
|
this.timeout(5000);
|
|
run_headers_test(done, headers);
|
|
});
|
|
|
|
it('should be able to produce and consume messages with one header value as int: consumeLoop', function(done) {
|
|
var headers = [
|
|
{ key: 10 }
|
|
];
|
|
this.timeout(5000);
|
|
run_headers_test(done, headers);
|
|
});
|
|
|
|
it('should be able to produce and consume messages with one header value as float: consumeLoop', function(done) {
|
|
var headers = [
|
|
{ key: 1.11 }
|
|
];
|
|
this.timeout(5000);
|
|
run_headers_test(done, headers);
|
|
});
|
|
|
|
it('should be able to produce and consume messages with multiple headers value as buffer: consumeLoop', function(done) {
|
|
var headers = [
|
|
{ key1: Buffer.from('value1') },
|
|
{ key2: Buffer.from('value2') },
|
|
{ key3: Buffer.from('value3') },
|
|
{ key4: Buffer.from('value4') },
|
|
];
|
|
this.timeout(5000);
|
|
run_headers_test(done, headers);
|
|
});
|
|
|
|
it('should be able to produce and consume messages with multiple headers value as string: consumeLoop', function(done) {
|
|
var headers = [
|
|
{ key1: 'value1' },
|
|
{ key2: 'value2' },
|
|
{ key3: 'value3' },
|
|
{ key4: 'value4' },
|
|
];
|
|
this.timeout(5000);
|
|
run_headers_test(done, headers);
|
|
});
|
|
|
|
it('should be able to produce and consume messages with multiple headers with mixed values: consumeLoop', function(done) {
|
|
var headers = [
|
|
{ key1: 'value1' },
|
|
{ key2: Buffer.from('value2') },
|
|
{ key3: 100 },
|
|
{ key4: 10.1 },
|
|
];
|
|
this.timeout(5000);
|
|
run_headers_test(done, headers);
|
|
});
|
|
|
|
it('should be able to produce and consume messages: empty buffer key and empty value', function(done) {
|
|
this.timeout(20000);
|
|
var emptyString = '';
|
|
var key = Buffer.from(emptyString);
|
|
var value = Buffer.from('');
|
|
|
|
producer.setPollInterval(10);
|
|
|
|
consumer.once('data', function(message) {
|
|
t.notEqual(message.value, null, 'message should not be null');
|
|
t.equal(value.toString(), message.value.toString(), 'invalid message value');
|
|
t.equal(emptyString, message.key, 'invalid message key');
|
|
done();
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, value, key);
|
|
}, 2000);
|
|
});
|
|
|
|
it('should be able to produce and consume messages: empty key and empty value', function(done) {
|
|
this.timeout(20000);
|
|
var key = '';
|
|
var value = Buffer.from('');
|
|
|
|
producer.setPollInterval(10);
|
|
|
|
consumer.once('data', function(message) {
|
|
t.notEqual(message.value, null, 'message should not be null');
|
|
t.equal(value.toString(), message.value.toString(), 'invalid message value');
|
|
t.equal(key, message.key, 'invalid message key');
|
|
done();
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, value, key);
|
|
}, 2000);
|
|
});
|
|
|
|
it('should be able to produce and consume messages: null key and null value', function(done) {
|
|
this.timeout(20000);
|
|
var key = null;
|
|
var value = null;
|
|
|
|
producer.setPollInterval(10);
|
|
|
|
consumer.once('data', function(message) {
|
|
t.equal(value, message.value, 'invalid message value');
|
|
t.equal(key, message.key, 'invalid message key');
|
|
done();
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, value, key);
|
|
}, 2000);
|
|
});
|
|
|
|
describe('Exceptional case - offset_commit_cb true', function() {
|
|
var grp = 'kafka-mocha-grp-' + crypto.randomBytes(20).toString('hex');
|
|
var consumerOpts = {
|
|
'metadata.broker.list': kafkaBrokerList,
|
|
'group.id': grp,
|
|
'fetch.wait.max.ms': 1000,
|
|
'session.timeout.ms': 10000,
|
|
'enable.auto.commit': false,
|
|
'debug': 'all',
|
|
'offset_commit_cb': true
|
|
};
|
|
|
|
beforeEach(function(done) {
|
|
consumer = new Kafka.KafkaConsumer(consumerOpts, {
|
|
'auto.offset.reset': 'largest',
|
|
});
|
|
|
|
consumer.connect({}, function(err, d) {
|
|
t.ifError(err);
|
|
t.equal(typeof d, 'object', 'metadata should be returned');
|
|
done();
|
|
});
|
|
|
|
eventListener(consumer);
|
|
});
|
|
|
|
afterEach(function(done) {
|
|
this.timeout(10000);
|
|
consumer.disconnect(function() {
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should async commit after consuming', function(done) {
|
|
this.timeout(25000);
|
|
var key = '';
|
|
var value = Buffer.from('');
|
|
|
|
var lastOffset = null;
|
|
|
|
consumer.once('data', function(message) {
|
|
lastOffset = message.offset;
|
|
|
|
// disconnect in offset commit callback
|
|
consumer.on('offset.commit', function(err, offsets) {
|
|
t.ifError(err);
|
|
t.equal(typeof offsets, 'object', 'offsets should be returned');
|
|
|
|
consumer.disconnect(function() {
|
|
// reconnect in disconnect callback
|
|
consumer.connect({}, function(err, d) {
|
|
t.ifError(err);
|
|
t.equal(typeof d, 'object', 'metadata should be returned');
|
|
|
|
// check that no new messages arrive, as the offset was committed
|
|
consumer.once('data', function(message) {
|
|
done(new Error('Should never be here'));
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
|
|
setTimeout(function() {
|
|
done();
|
|
}, 5000);
|
|
});
|
|
});
|
|
});
|
|
|
|
consumer.commitMessage(message);
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, value, key);
|
|
}, 2000);
|
|
});
|
|
});
|
|
|
|
describe('Exceptional case - offset_commit_cb function', function() {
|
|
var grp = 'kafka-mocha-grp-' + crypto.randomBytes(20).toString('hex');
|
|
|
|
afterEach(function(done) {
|
|
this.timeout(10000);
|
|
consumer.disconnect(function() {
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should callback offset_commit_cb after commit', function(done) {
|
|
this.timeout(20000);
|
|
|
|
var consumerOpts = {
|
|
'metadata.broker.list': kafkaBrokerList,
|
|
'group.id': grp,
|
|
'fetch.wait.max.ms': 1000,
|
|
'session.timeout.ms': 10000,
|
|
'enable.auto.commit': false,
|
|
'debug': 'all',
|
|
'offset_commit_cb': function(offset) {
|
|
done();
|
|
}
|
|
};
|
|
consumer = new Kafka.KafkaConsumer(consumerOpts, {
|
|
'auto.offset.reset': 'largest',
|
|
});
|
|
eventListener(consumer);
|
|
|
|
consumer.connect({}, function(err, d) {
|
|
t.ifError(err);
|
|
t.equal(typeof d, 'object', 'metadata should be returned');
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
setTimeout(function() {
|
|
producer.produce(topic, null, Buffer.from(''), '');
|
|
}, 2000);
|
|
});
|
|
|
|
consumer.once('data', function(message) {
|
|
consumer.commitMessage(message);
|
|
});
|
|
});
|
|
});
|
|
|
|
function assert_headers_match(expectedHeaders, messageHeaders) {
|
|
t.equal(expectedHeaders.length, messageHeaders.length, 'Headers length does not match expected length');
|
|
for (var i = 0; i < expectedHeaders.length; i++) {
|
|
var expectedKey = Object.keys(expectedHeaders[i])[0];
|
|
var messageKey = Object.keys(messageHeaders[i]);
|
|
t.equal(messageKey.length, 1, 'Expected only one Header key');
|
|
t.equal(expectedKey, messageKey[0], 'Expected key does not match message key');
|
|
var expectedValue = Buffer.isBuffer(expectedHeaders[i][expectedKey]) ?
|
|
expectedHeaders[i][expectedKey].toString() :
|
|
expectedHeaders[i][expectedKey];
|
|
var actualValue = messageHeaders[i][expectedKey].toString();
|
|
t.equal(expectedValue, actualValue, 'invalid message header');
|
|
}
|
|
}
|
|
|
|
function run_headers_test(done, headers) {
|
|
var key = 'key';
|
|
|
|
crypto.randomBytes(4096, function(ex, buffer) {
|
|
|
|
producer.setPollInterval(10);
|
|
|
|
producer.once('delivery-report', function(err, report) {
|
|
if (!err) {
|
|
t.equal(topic, report.topic, 'invalid delivery-report topic');
|
|
t.equal(key, report.key, 'invalid delivery-report key');
|
|
t.ok(report.offset >= 0, 'invalid delivery-report offset');
|
|
}
|
|
});
|
|
|
|
consumer.on('data', function(message) {
|
|
t.equal(buffer.toString(), message.value.toString(), 'invalid message value');
|
|
t.equal(key, message.key, 'invalid message key');
|
|
t.equal(topic, message.topic, 'invalid message topic');
|
|
t.ok(message.offset >= 0, 'invalid message offset');
|
|
assert_headers_match(headers, message.headers);
|
|
done();
|
|
});
|
|
|
|
consumer.subscribe([topic]);
|
|
consumer.consume();
|
|
|
|
setTimeout(function() {
|
|
var timestamp = new Date().getTime();
|
|
producer.produce(topic, null, buffer, key, timestamp, "", headers);
|
|
}, 2000);
|
|
|
|
});
|
|
}
|
|
|
|
});
|