UNPKG

kafka-node

Version:

Client for Apache Kafka v0.9.x, v0.10.x and v0.11.x

1,677 lines (1,461 loc) 57.5 kB
'use strict'; var Binary = require('binary'); var Buffermaker = require('buffermaker'); var _ = require('lodash'); var crc32 = require('buffer-crc32'); var protocol = require('./protocol_struct'); var getCodec = require('../codec'); var REQUEST_TYPE = protocol.REQUEST_TYPE; var ERROR_CODE = protocol.ERROR_CODE; var GROUP_ERROR = protocol.GROUP_ERROR; var PartitionMetadata = protocol.PartitionMetadata; const API_KEY_TO_NAME = _.invert(REQUEST_TYPE); const MessageSizeTooLarge = require('../errors/MessageSizeTooLargeError'); const SaslAuthenticationError = require('../errors/SaslAuthenticationError'); const InvalidRequestError = require('../errors/InvalidRequestError'); const async = require('async'); var API_VERSION = 0; var REPLICA_ID = -1; var GROUPS_PROTOCOL_TYPE = 'consumer'; function groupByTopic (payloads) { return payloads.reduce(function (out, p) { out[p.topic] = out[p.topic] || {}; out[p.topic][p.partition] = p; return out; }, {}); } function encodeRequestWithLength (request) { return new Buffermaker().Int32BE(request.length).string(request).make(); } function encodeRequestHeader (clientId, correlationId, apiKey, apiVersion) { return new Buffermaker() .Int16BE(apiKey) .Int16BE(apiVersion || API_VERSION) .Int32BE(correlationId) .Int16BE(clientId.length) .string(clientId); } function encodeSaslHandshakeRequest (clientId, correlationId, apiVersion, mechanism) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.saslHandshake, apiVersion); request.Int16BE(mechanism.length).string(mechanism.toUpperCase()); return encodeRequestWithLength(request.make()); } function decodeSaslHandshakeResponse (resp) { var mechanisms = []; var errorCode = null; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word16bs('errorCode') .tap(function (vars) { errorCode = vars.errorCode; }) .word32bs('numMechanisms') .loop(_decodeMechanisms); function _decodeMechanisms (end, vars) { if (vars.numMechanisms-- === 0) { return end(); } this .word16be('mechanismSize') .tap(function (vars) { this.buffer('mechanism', vars.mechanismSize); mechanisms.push(vars.mechanism); }); } if (errorCode == null || errorCode === 0) { return mechanisms; } return new SaslAuthenticationError(errorCode, 'Handshake failed.'); } function encodeSaslAuthenticateRequest (clientId, correlationId, apiVersion, saslOpts) { // // FIXME From the Kafka protocol docs: // If SaslHandshakeRequest version is v0, a series of SASL client and server tokens // corresponding to the mechanism are sent as opaque packets without wrapping the // messages with Kafka protocol headers. If SaslHandshakeRequest version is v1, the // SaslAuthenticate request/response are used, where the actual SASL tokens are // wrapped in the Kafka protocol. // var username = saslOpts.username || ''; var password = saslOpts.password || ''; var authBytes = null; if (saslOpts.mechanism.toUpperCase() === 'PLAIN') { authBytes = (new Buffermaker()) .string(username).Int8(0) .string(username).Int8(0) .string(password) .make(); } else { return new Error('unsupported SASL auth type: ' + saslOpts.mechanism.toUpperCase()); } if (apiVersion === 0) { return encodeRequestWithLength(authBytes); } var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.saslAuthenticate, 0); request.Int32BE(authBytes.length).string(authBytes); return encodeRequestWithLength(request.make()); } function decodeSaslAuthenticateResponse (resp) { var errorCode = null; var errorMessage = null; var authBytes = null; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word16bs('errorCode') .word16bs('errorMessageLength') .tap(function (vars) { errorCode = vars.errorCode; this.buffer('errorMessage', vars.errorMessageLength); errorMessage = vars.errorMessage.toString(); }) .word32bs('authBytesLength') .tap(function (vars) { this.buffer('authBytes', vars.authBytesLength); authBytes = vars.authBytes.toString(); }); if (errorCode == null || errorCode === 0) { return authBytes; } return new SaslAuthenticationError(errorCode, errorMessage); } function encodeFetchRequest (maxWaitMs, minBytes) { return function encodeFetchRequest (clientId, correlationId, payloads) { return _encodeFetchRequest(clientId, correlationId, payloads, maxWaitMs, minBytes); }; } function encodeFetchRequestV1 (maxWaitMs, minBytes) { return function encodeFetchRequest (clientId, correlationId, payloads) { return _encodeFetchRequest(clientId, correlationId, payloads, maxWaitMs, minBytes, 1); }; } function encodeFetchRequestV2 (maxWaitMs, minBytes) { return function encodeFetchRequest (clientId, correlationId, payloads) { return _encodeFetchRequest(clientId, correlationId, payloads, maxWaitMs, minBytes, 2); }; } function decodeTopics (decodePartitions) { return function (end, vars) { if (--vars.topicNum === 0) end(); this.word16bs('topic') .tap(function (vars) { this.buffer('topic', vars.topic); vars.topic = vars.topic.toString(); }) .word32bs('partitionNum') .loop(decodePartitions); }; } function _encodeFetchRequest (clientId, correlationId, payloads, maxWaitMs, minBytes, version) { payloads = groupByTopic(payloads); var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.fetch, version); var topics = Object.keys(payloads); request.Int32BE(REPLICA_ID).Int32BE(maxWaitMs).Int32BE(minBytes).Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.length).string(topic); var partitions = _.toPairs(payloads[topic]).map(function (pairs) { return pairs[1]; }); request.Int32BE(partitions.length); partitions.forEach(function (p) { request.Int32BE(p.partition).Int64BE(p.offset).Int32BE(p.maxBytes); }); }); return encodeRequestWithLength(request.make()); } function decodeFetchResponse (cb, maxTickMessages) { return function (resp) { return _decodeFetchResponse(resp, cb, maxTickMessages, 0); }; } function decodeFetchResponseV1 (cb, maxTickMessages) { return function (resp) { return _decodeFetchResponse(resp, cb, maxTickMessages, 1); }; } function createGroupError (errorCode) { if (errorCode == null || errorCode === 0) { return null; } var error = ERROR_CODE[errorCode]; if (error in GROUP_ERROR) { error = new GROUP_ERROR[error]('Kafka Error Code: ' + errorCode); } else { error = new Error(error); } error.errorCode = errorCode; return error; } function _decodeFetchResponse (resp, cb, maxTickMessages, version) { if (!cb) { throw new Error('Missing callback'); } var topics = {}; var events = []; let eventCount = 0; cb(null, 'processingfetch'); Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .tap(function () { if (version >= 1) { this.word32bs('throttleTime'); } }) .word32bs('topicNum') .loop(decodeTopics(decodePartitions)); // Parsing is all sync, but emitting events is async due to compression // At this point, the enqueue chain should be populated in order, and at // the end, the 'topics' map above should be fully populated. // topics is not ready for use until all events are processed, as an async // decompression may add more offsets to the topic map. async.series(events, (err) => { if (err) { cb(err); } cb(null, 'done', topics); }); function decodePartitions (end, vars) { if (--vars.partitionNum === 0) end(); topics[vars.topic] = topics[vars.topic] || {}; let hadError = false; this.word32bs('partition') .word16bs('errorCode') .word64bs('highWaterOffset') .word32bs('messageSetSize') .tap(function (vars) { this.buffer('messageSet', vars.messageSetSize); const errorCode = vars.errorCode; const topic = vars.topic; const partition = vars.partition; if (errorCode !== 0) { return events.push((next) => { const err = new Error(ERROR_CODE[errorCode]); err.topic = topic; err.partition = partition; cb(err); next(); }); } const highWaterOffset = vars.highWaterOffset; const { magicByte } = Binary.parse(vars.messageSet).skip(16).word8bs('magicByte').vars; if (magicByte != null && magicByte > 1) { return events.push((next) => { const err = new Error('Not a message set. Magic byte is ' + magicByte); err.topic = topic; err.partition = partition; cb(err); next(); }); } decodeMessageSet( topic, partition, vars.messageSet, (enqueuedTask) => { events.push(enqueuedTask); if (maxTickMessages && ++eventCount >= maxTickMessages) { eventCount = 0; events.push((next) => process.nextTick(next)); } }, (err, data) => { if (err) { hadError = true; } else if (hadError) { return; // Once we've had an error on this partition, don't emit any more messages } if (!err && data) { topics[topic][partition] = data.offset; cb(null, 'message', data); } else { cb(err); } }, highWaterOffset, topics ); }); } } function decodeMessageSet (topic, partition, messageSet, enqueue, emit, highWaterOffset, topics, lastOffset) { const messageSetSize = messageSet.length; // TODO: this is broken logic. It overwrites previous partitions HWO. // Need to refactor this on next major API bump topics[topic].highWaterOffset = highWaterOffset; let innerMessages = []; while (messageSet.length > 0) { var cur = 8 + 4 + 4 + 1 + 1 + 4 + 4; var partial = false; Binary.parse(messageSet) .word64bs('offset') .word32bs('messageSize') .tap(function (vars) { if (vars.messageSize > messageSet.length - 12) { partial = true; } }) .word32bs('crc') .word8bs('magicByte') .word8bs('attributes') .tap(function (vars) { if (vars.magicByte > 0) { this.word64bs('timestamp'); cur += 8; } }) .word32bs('key') .tap(function (vars) { if (vars.key === -1) { vars.key = null; return; } cur += vars.key; this.buffer('key', vars.key); }) .word32bs('value') .tap(function (vars) { if (vars.value !== -1) { cur += vars.value; this.buffer('value', vars.value); } else { vars.value = null; } if (vars.attributes === 0 && vars.messageSize > messageSetSize) { const offset = vars.offset; return enqueue(next => { emit( new MessageSizeTooLarge({ topic: topic, offset: offset, partition: partition }) ); next(null); }); } if (!partial && vars.offset !== null) { const offset = vars.offset; const value = vars.value; const key = vars.key; const magicByte = vars.magicByte; var codec = getCodec(vars.attributes); if (!codec) { const message = { topic: topic, value: value, offset: offset, partition: partition, highWaterOffset: highWaterOffset, key: key }; if (vars.timestamp) { message.timestamp = new Date(vars.timestamp); } if (lastOffset != null) { // need to fix offset skipping enqueue till later innerMessages.push(message); return; } enqueue((next) => { emit(null, message); next(null); }); return; } enqueue((next) => { codec.decode(value, function (error, inlineMessageSet) { if (error) { emit(error); next(null); return; } decodeMessageSet(topic, partition, inlineMessageSet, (cb) => { cb(_.noop); }, emit, highWaterOffset, topics, magicByte === 1 ? offset : null); // Delay 1 tick as this isn't counted to max tick messages, give a breather process.nextTick(() => next(null)); }); }); } }); // Skip decoding binary left in the message set if partial message detected if (partial) break; messageSet = messageSet.slice(cur); } if (lastOffset != null && innerMessages.length) { // contains inner messages, need to fix up offsets let len = innerMessages.length - 1; for (const message of innerMessages) { const offset = lastOffset - len--; message.offset = offset; enqueue((next) => { emit(null, message); next(null); }); } } } function encodeMetadataRequest (clientId, correlationId, topics) { return _encodeMetadataRequest(clientId, correlationId, topics, 0); } function decodeMetadataResponse (resp) { return _decodeMetadataResponse(resp, 0); } function encodeMetadataV1Request (clientId, correlationId, topics) { return _encodeMetadataRequest(clientId, correlationId, topics, 1); } function decodeMetadataV1Response (resp) { return _decodeMetadataResponse(resp, 1); } function _encodeMetadataRequest (clientId, correlationId, topics, version) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.metadata, version); // In version 0 an empty array will fetch all topics. // In version 1+ a null value (-1) will fetch all topics. An empty array returns no topics. // This adds support for maintaining version 0 behaviour in client regardless of kafka version ([] = fetch all topics). if (version > 0 && ((Array.isArray(topics) && topics.length === 0) || topics === null)) { request.Int32BE(-1); return encodeRequestWithLength(request.make()); } // Handle case where null is provided but version requested was 0 (not supported). // Can happen if the api versions requests fails and fallback api support is used. if (version === 0 && topics === null) { topics = []; } request.Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.length).string(topic); }); return encodeRequestWithLength(request.make()); } function _decodeMetadataResponse (resp, version) { var brokers = {}; var out = {}; var topics = {}; var controllerId = -1; var errors = []; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('brokerNum') .loop(decodeBrokers) .tap(function (vars) { if (version < 1) { return; } this.word32bs('controllerId'); controllerId = vars.controllerId; }) .word32bs('topicNum') .loop(_decodeTopics); function decodeBrokers (end, vars) { if (vars.brokerNum-- === 0) return end(); this.word32bs('nodeId') .word16bs('host') .tap(function (vars) { this.buffer('host', vars.host); vars.host = vars.host.toString(); }) .word32bs('port') .tap(function (vars) { if (version < 1) { return; } this.word16bs('rack'); if (vars.rack === -1) { vars.rack = ''; } else { this.buffer('rack', vars.rack); vars.rack = vars.rack.toString(); } }) .tap(function (vars) { brokers[vars.nodeId] = { nodeId: vars.nodeId, host: vars.host, port: vars.port }; }); } function _decodeTopics (end, vars) { if (vars.topicNum-- === 0) return end(); this.word16bs('topicError') .word16bs('topic') .tap(function (vars) { this.buffer('topic', vars.topic); vars.topic = vars.topic.toString(); if (version < 1) { return; } this.word8bs('isInternal'); }) .word32bs('partitionNum') .tap(function (vars) { if (vars.topicError !== 0) { return errors.push(ERROR_CODE[vars.topicError]); } this.loop(decodePartitions); }); } function decodePartitions (end, vars) { if (vars.partitionNum-- === 0) return end(); topics[vars.topic] = topics[vars.topic] || {}; this.word16bs('errorCode') .word32bs('partition') .word32bs('leader') .word32bs('replicasNum') .tap(function (vars) { var buffer = this.buffer('replicas', vars.replicasNum * 4).vars.replicas; this.vars.replicas = bufferToArray(vars.replicasNum, buffer); }) .word32bs('isrNum') .tap(function (vars) { var buffer = this.buffer('isr', vars.isrNum * 4).vars.isr; this.vars.isr = bufferToArray(vars.isrNum, buffer); if (vars.errorCode === 0 || vars.errorCode === 9) { topics[vars.topic][vars.partition] = new PartitionMetadata( vars.topic, vars.partition, vars.leader, vars.replicas, vars.isr ); } else { errors.push(ERROR_CODE[vars.errorCode]); } }); } if (!_.isEmpty(errors)) out.error = errors; out.metadata = topics; if (version > 0) { out.clusterMetadata = { controllerId }; } return [brokers, out]; } function encodeCreateTopicRequest (clientId, correlationId, topics, timeoutMs) { return _encodeCreateTopicRequest(clientId, correlationId, topics, timeoutMs, 0); } function encodeCreateTopicRequestV1 (clientId, correlationId, topics, timeoutMs) { return _encodeCreateTopicRequest(clientId, correlationId, topics, timeoutMs, 1); } function _encodeCreateTopicRequest (clientId, correlationId, topics, timeoutMs, version) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.createTopics, version); request.Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.topic.length).string(topic.topic); // When replica assignment is used, partitions and replication factor must be unset (-1) if (topic.replicaAssignment) { request.Int32BE(-1); request.Int16BE(-1); } else { request.Int32BE(topic.partitions); request.Int16BE(topic.replicationFactor); } if (topic.replicaAssignment) { request.Int32BE(topic.replicaAssignment.length); topic.replicaAssignment.forEach(function (replicaAssignment) { request.Int32BE(replicaAssignment.partition); request.Int32BE(replicaAssignment.replicas.length); replicaAssignment.replicas.forEach(function (replica) { request.Int32BE(replica); }); }); } else { request.Int32BE(0); } if (topic.configEntries) { request.Int32BE(topic.configEntries.length); topic.configEntries.forEach(function (config) { request.Int16BE(config.name.length).string(config.name); if (config.value == null) { request.Int16BE(-1); } else { request.Int16BE(config.value.length).string(config.value); } }); } else { request.Int32BE(0); } }); request.Int32BE(timeoutMs); // Validate only is 1+ if (version > 0) { request.Int8(0); } return encodeRequestWithLength(request.make()); } function decodeCreateTopicResponse (resp) { return _decodeCreateTopicResponse(resp, 0); } function decodeCreateTopicResponseV1 (resp) { return _decodeCreateTopicResponse(resp, 1); } function _decodeCreateTopicResponse (resp, version) { var topicErrorResponses = []; var error; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('topicNum') .loop(decodeTopics); function decodeTopics (end, vars) { if (vars.topicNum-- === 0) return end(); this.word16bs('topic') .tap(function (vars) { this.buffer('topic', vars.topic); vars.topic = vars.topic.toString(); }) .word16bs('errorCode') .tap(function (vars) { if (version > 0) { this.word16bs('errorMessage'); if (vars.errorMessage !== -1) { this.buffer('errorMessage', vars.errorMessage); vars.errorMessage = vars.errorMessage.toString(); } } if (vars.errorCode === 0) { return; } // Errors that are not related to the actual topic(s) but the entire request // (like timeout and sending the request to a non-controller) if (vars.errorCode === 7 || vars.errorCode === 41) { error = createGroupError(vars.errorCode); return; } if (version > 0) { topicErrorResponses.push({ topic: vars.topic, error: vars.errorMessage }); } else { topicErrorResponses.push({ topic: vars.topic, error: 'received error code ' + vars.errorCode + ' for topic' }); } }); } return error || topicErrorResponses; } function bufferToArray (num, buffer) { var ret = []; for (var i = 0; i < num; i++) { ret.push(Binary.parse(buffer).word32bs('r').vars.r); buffer = buffer.slice(4); } return ret; } function encodeOffsetCommitRequest (group) { return function (clientId, correlationId, payloads) { return _encodeOffsetCommitRequest(clientId, correlationId, group, payloads); }; } function encodeOffsetCommitV2Request (clientId, correlationId, group, generationId, memberId, payloads) { return encodeOffsetCommitGenericRequest(clientId, correlationId, group, generationId, memberId, payloads, 2); } function encodeOffsetCommitV1Request (clientId, correlationId, group, generationId, memberId, payloads) { return encodeOffsetCommitGenericRequest(clientId, correlationId, group, generationId, memberId, payloads, 1); } function encodeOffsetCommitGenericRequest ( clientId, correlationId, group, generationId, memberId, payloads, apiVersion ) { payloads = groupByTopic(payloads); var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.offsetCommit, apiVersion); var topics = Object.keys(payloads); request .Int16BE(group.length) .string(group) .Int32BE(generationId) .Int16BE(memberId.length) .string(memberId) .Int64BE(-1) .Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.length).string(topic); var partitions = _.toPairs(payloads[topic]).map(function (pairs) { return pairs[1]; }); request.Int32BE(partitions.length); partitions.forEach(function (p) { request.Int32BE(p.partition).Int64BE(p.offset).Int16BE(p.metadata.length).string(p.metadata); }); }); return encodeRequestWithLength(request.make()); } function _encodeOffsetCommitRequest (clientId, correlationId, group, payloads) { payloads = groupByTopic(payloads); var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.offsetCommit); var topics = Object.keys(payloads); request.Int16BE(group.length).string(group).Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.length).string(topic); var partitions = _.toPairs(payloads[topic]).map(function (pairs) { return pairs[1]; }); request.Int32BE(partitions.length); partitions.forEach(function (p) { request.Int32BE(p.partition).Int64BE(p.offset).Int16BE(p.metadata.length).string(p.metadata); }); }); return encodeRequestWithLength(request.make()); } function decodeOffsetCommitResponse (resp) { var topics = {}; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('topicNum') .loop(decodeTopics(decodePartitions)); function decodePartitions (end, vars) { if (--vars.partitionNum === 0) end(); topics[vars.topic] = topics[vars.topic] || {}; this.word32bs('partition').word16bs('errorcode').tap(function (vars) { topics[vars.topic]['partition'] = vars.partition; topics[vars.topic]['errorCode'] = vars.errorcode; }); } return topics; } function encodeProduceRequest (requireAcks, ackTimeoutMs) { return function (clientId, correlationId, payloads) { return _encodeProduceRequest(clientId, correlationId, payloads, requireAcks, ackTimeoutMs, 0); }; } function encodeProduceV1Request (requireAcks, ackTimeoutMs) { return function (clientId, correlationId, payloads) { return _encodeProduceRequest(clientId, correlationId, payloads, requireAcks, ackTimeoutMs, 1); }; } function encodeProduceV2Request (requireAcks, ackTimeoutMs) { return function (clientId, correlationId, payloads) { return _encodeProduceRequest(clientId, correlationId, payloads, requireAcks, ackTimeoutMs, 2); }; } function _encodeProduceRequest (clientId, correlationId, payloads, requireAcks, ackTimeoutMs, apiVersion) { payloads = groupByTopic(payloads); var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.produce, apiVersion); var topics = Object.keys(payloads); request.Int16BE(requireAcks).Int32BE(ackTimeoutMs).Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.length).string(topic); var reqs = _.toPairs(payloads[topic]).map(function (pairs) { return pairs[1]; }); request.Int32BE(reqs.length); reqs.forEach(function (p) { var messageSet = encodeMessageSet(p.messages, apiVersion === 2 ? 1 : 0); request.Int32BE(p.partition).Int32BE(messageSet.length).string(messageSet); }); }); return encodeRequestWithLength(request.make()); } function encodeMessageSet (messageSet, magic) { var buffer = new Buffermaker(); messageSet.forEach(function (message) { var msg = encodeMessage(message, magic); buffer.Int64BE(0).Int32BE(msg.length).string(msg); }); return buffer.make(); } function encodeMessage (message, magic) { if (magic == null) { magic = 0; } var m = new Buffermaker().Int8(magic).Int8(message.attributes); // Add timestamp support for new messages if (magic === 1) { m.Int64BE(message.timestamp); } var key = message.key; setValueOnBuffer(m, key); var value = message.value; setValueOnBuffer(m, value); m = m.make(); var crc = crc32.signed(m); return new Buffermaker().Int32BE(crc).string(m).make(); } function setValueOnBuffer (buffer, value) { if (value != null) { if (Buffer.isBuffer(value)) { buffer.Int32BE(value.length); } else { if (typeof value !== 'string') value = value.toString(); buffer.Int32BE(Buffer.byteLength(value)); } buffer.string(value); } else { buffer.Int32BE(-1); } } function decodeProduceV1Response (resp) { var topics = {}; var error; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('topicNum') .loop(decodeTopics(decodePartitions)) .word32bs('throttleTime'); function decodePartitions (end, vars) { if (--vars.partitionNum === 0) end(); topics[vars.topic] = topics[vars.topic] || {}; this.word32bs('partition').word16bs('errorCode').word64bs('offset').tap(function (vars) { if (vars.errorCode) { error = new Error(ERROR_CODE[vars.errorCode]); } else { topics[vars.topic][vars.partition] = vars.offset; } }); } return error || topics; } function decodeProduceV2Response (resp) { var topics = {}; var error; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('topicNum') .loop(decodeTopics(decodePartitions)) .word32bs('throttleTime'); function decodePartitions (end, vars) { if (--vars.partitionNum === 0) end(); topics[vars.topic] = topics[vars.topic] || {}; this.word32bs('partition').word16bs('errorCode').word64bs('offset').word64bs('timestamp').tap(function (vars) { if (vars.errorCode) { error = new Error(ERROR_CODE[vars.errorCode]); } else { topics[vars.topic][vars.partition] = vars.offset; } }); } return error || topics; } function decodeProduceResponse (resp) { var topics = {}; var error; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('topicNum') .loop(decodeTopics(decodePartitions)); function decodePartitions (end, vars) { if (--vars.partitionNum === 0) end(); topics[vars.topic] = topics[vars.topic] || {}; this.word32bs('partition').word16bs('errorCode').word64bs('offset').tap(function (vars) { if (vars.errorCode) { error = new Error(ERROR_CODE[vars.errorCode]); } else { topics[vars.topic][vars.partition] = vars.offset; } }); } return error || topics; } function encodeOffsetFetchRequest (group) { return function (clientId, correlationId, payloads) { return _encodeOffsetFetchRequest(clientId, correlationId, group, payloads); }; } function encodeOffsetFetchV1Request (clientId, correlationId, group, payloads) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.offsetFetch, 1); var topics = Object.keys(payloads); request.Int16BE(group.length).string(group).Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.length).string(topic).Int32BE(payloads[topic].length); payloads[topic].forEach(function (p) { request.Int32BE(p); }); }); return encodeRequestWithLength(request.make()); } function _encodeOffsetFetchRequest (clientId, correlationId, group, payloads) { payloads = groupByTopic(payloads); var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.offsetFetch); var topics = Object.keys(payloads); request.Int16BE(group.length).string(group).Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.length).string(topic); var partitions = _.toPairs(payloads[topic]).map(function (pairs) { return pairs[1]; }); request.Int32BE(partitions.length); partitions.forEach(function (p) { request.Int32BE(p.partition); }); }); return encodeRequestWithLength(request.make()); } function decodeOffsetFetchResponse (resp) { var topics = {}; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('topicNum') .loop(decodeTopics(decodePartitions)); function decodePartitions (end, vars) { if (--vars.partitionNum === 0) end(); topics[vars.topic] = topics[vars.topic] || {}; this.word32bs('partition') .word64bs('offset') .word16bs('metadata') .tap(function (vars) { if (vars.metadata === -1) { return; } this.buffer('metadata', vars.metadata); }) .word16bs('errorCode') .tap(function (vars) { topics[vars.topic][vars.partition] = vars.errorCode === 0 ? vars.offset : -1; }); } return topics; } function decodeOffsetFetchV1Response (resp) { var topics = {}; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('topicNum') .loop(decodeTopics(decodePartitions)); function decodePartitions (end, vars) { if (--vars.partitionNum === 0) end(); topics[vars.topic] = topics[vars.topic] || {}; this.word32bs('partition') .word64bs('offset') .word16bs('metadata') .tap(function (vars) { if (vars.metadata === -1) { return; } this.buffer('metadata', vars.metadata); }) .word16bs('errorCode') .tap(function (vars) { if (vars.metadata.length === 0 && vars.offset === 0) { topics[vars.topic][vars.partition] = -1; } else { topics[vars.topic][vars.partition] = vars.errorCode === 0 ? vars.offset : -1; } }); } return topics; } function encodeOffsetRequest (clientId, correlationId, payloads) { payloads = groupByTopic(payloads); var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.offset); var topics = Object.keys(payloads); request.Int32BE(REPLICA_ID).Int32BE(topics.length); topics.forEach(function (topic) { request.Int16BE(topic.length).string(topic); var partitions = _.toPairs(payloads[topic]).map(function (pairs) { return pairs[1]; }); request.Int32BE(partitions.length); partitions.forEach(function (p) { request.Int32BE(p.partition).Int64BE(p.time).Int32BE(p.maxNum); }); }); return encodeRequestWithLength(request.make()); } function decodeOffsetResponse (resp) { var topics = {}; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word32bs('topicNum') .loop(decodeTopics(decodePartitions)); function decodePartitions (end, vars) { if (--vars.partitionNum === 0) end(); topics[vars.topic] = topics[vars.topic] || {}; this.word32bs('partition').word16bs('errorCode').word32bs('offsetNum').loop(decodeOffsets); } function decodeOffsets (end, vars) { if (--vars.offsetNum <= 0) end(); topics[vars.topic][vars.partition] = topics[vars.topic][vars.partition] || []; this.word64bs('offset').tap(function (vars) { if (vars.offset != null) topics[vars.topic][vars.partition].push(vars.offset); }); } return topics; } function encodeGroupCoordinatorRequest (clientId, correlationId, groupId) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.groupCoordinator); request.Int16BE(groupId.length).string(groupId); return encodeRequestWithLength(request.make()); } function encodeGroupHeartbeatRequest (clientId, correlationId, groupId, generationId, memberId) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.heartbeat); request.Int16BE(groupId.length).string(groupId).Int32BE(generationId).Int16BE(memberId.length).string(memberId); return encodeRequestWithLength(request.make()); } function decodeGroupHeartbeatResponse (resp) { var result = null; Binary.parse(resp).word32bs('size').word32bs('correlationId').word16bs('errorCode').tap(function (vars) { result = createGroupError(vars.errorCode); }); return result; } function decodeGroupCoordinatorResponse (resp) { var result; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word16bs('errorCode') .word32bs('coordinatorId') .word16bs('coordinatorHost') .tap(function (vars) { this.buffer('coordinatorHost', vars.coordinatorHost); vars.coordinatorHost = vars.coordinatorHost.toString(); }) .word32bs('coordinatorPort') .tap(function (vars) { if (vars.errorCode !== 0) { result = createGroupError(vars.errorCode); return; } result = { coordinatorHost: vars.coordinatorHost, coordinatorPort: vars.coordinatorPort, coordinatorId: vars.coordinatorId }; }); return result; } /* ProtocolType => "consumer" ProtocolName => AssignmentStrategy AssignmentStrategy => string ProtocolMetadata => Version Subscription UserData Version => int16 Subscription => [Topic] Topic => string UserData => bytes */ function encodeGroupProtocol (protocol) { this.Int16BE(protocol.name.length).string(protocol.name).string(_encodeProtocolData(protocol)); } function _encodeProtocolData (protocol) { var protocolByte = new Buffermaker().Int16BE(protocol.version).Int32BE(protocol.subscription.length); protocol.subscription.forEach(function (topic) { protocolByte.Int16BE(topic.length).string(topic); }); if (protocol.userData) { var userDataStr = JSON.stringify(protocol.userData); var data = Buffer.from(userDataStr, 'utf8'); protocolByte.Int32BE(data.length).string(data); } else { protocolByte.Int32BE(-1); } return encodeRequestWithLength(protocolByte.make()); } function decodeSyncGroupResponse (resp) { var result; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word16bs('errorCode') .tap(function (vars) { result = createGroupError(vars.errorCode); }) .word32bs('memberAssignment') .tap(function (vars) { if (result) { return; } this.buffer('memberAssignment', vars.memberAssignment); result = decodeMemberAssignment(vars.memberAssignment); }); return result; } /* MemberAssignment => Version PartitionAssignment Version => int16 PartitionAssignment => [Topic [Partition]] Topic => string Partition => int32 UserData => bytes */ function decodeMemberAssignment (assignmentBytes) { var assignment = { partitions: {} }; Binary.parse(assignmentBytes) .word16bs('version') .tap(function (vars) { assignment.version = vars.version; }) .word32bs('partitionAssignment') .loop(function (end, vars) { if (vars.partitionAssignment-- === 0) return end(); var topic; var partitions = []; this.word16bs('topic') .tap(function (vars) { this.buffer('topic', vars.topic); topic = vars.topic.toString(); }) .word32bs('partitionsNum') .loop(function (end, vars) { if (vars.partitionsNum-- === 0) return end(); this.word32bs('partition').tap(function (vars) { partitions.push(vars.partition); }); }); assignment.partitions[topic] = partitions; }) .word32bs('userData') .tap(function (vars) { if (vars.userData == null || vars.userData === -1) { return; } this.buffer('userData', vars.userData); try { assignment.userData = JSON.parse(vars.userData.toString()); } catch (e) { assignment.userData = 'JSON Parse error'; } }); return assignment; } function encodeSyncGroupRequest (clientId, correlationId, groupId, generationId, memberId, groupAssignment) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.syncGroup); request.Int16BE(groupId.length).string(groupId).Int32BE(generationId).Int16BE(memberId.length).string(memberId); if (groupAssignment && groupAssignment.length) { request.Int32BE(groupAssignment.length); groupAssignment.forEach(function (assignment) { request .Int16BE(assignment.memberId.length) .string(assignment.memberId) .string(_encodeMemberAssignment(assignment)); }); } else { request.Int32BE(0); } return encodeRequestWithLength(request.make()); } function _encodeMemberAssignment (assignment) { var numberOfTopics = Object.keys(assignment.topicPartitions).length; var assignmentByte = new Buffermaker().Int16BE(assignment.version).Int32BE(numberOfTopics); for (var tp in assignment.topicPartitions) { if (!assignment.topicPartitions.hasOwnProperty(tp)) { continue; } var partitions = assignment.topicPartitions[tp]; assignmentByte.Int16BE(tp.length).string(tp).Int32BE(partitions.length); partitions.forEach(function (partition) { assignmentByte.Int32BE(partition); }); } if (assignment.userData) { var userDataStr = JSON.stringify(assignment.userData); var data = Buffer.from(userDataStr, 'utf8'); assignmentByte.Int32BE(data.length).string(data); } else { assignmentByte.Int32BE(-1); } return encodeRequestWithLength(assignmentByte.make()); } function encodeLeaveGroupRequest (clientId, correlationId, groupId, memberId) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.leaveGroup); request.Int16BE(groupId.length).string(groupId).Int16BE(memberId.length).string(memberId); return encodeRequestWithLength(request.make()); } function decodeLeaveGroupResponse (resp) { var error = null; Binary.parse(resp).word32bs('size').word32bs('correlationId').word16bs('errorCode').tap(function (vars) { error = createGroupError(vars.errorCode); }); return error; } // { // name: '', // string // subscription: [/* topics */], // version: 0, // integer // userData: {} //arbitary // } /* JoinGroupRequest => GroupId SessionTimeout MemberId ProtocolType GroupProtocols GroupId => string SessionTimeout => int32 MemberId => string ProtocolType => string GroupProtocols => [ProtocolName ProtocolMetadata] ProtocolName => string ProtocolMetadata => bytes */ function encodeJoinGroupRequest (clientId, correlationId, groupId, memberId, sessionTimeout, groupProtocols) { var request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.joinGroup); request .Int16BE(groupId.length) .string(groupId) .Int32BE(sessionTimeout) .Int16BE(memberId.length) .string(memberId) .Int16BE(GROUPS_PROTOCOL_TYPE.length) .string(GROUPS_PROTOCOL_TYPE) .Int32BE(groupProtocols.length); groupProtocols.forEach(encodeGroupProtocol.bind(request)); return encodeRequestWithLength(request.make()); } /* v0 and v1 supported in 0.9.0 and greater JoinGroupResponse => ErrorCode GenerationId GroupProtocol LeaderId MemberId Members ErrorCode => int16 GenerationId => int32 GroupProtocol => string LeaderId => string MemberId => string Members => [MemberId MemberMetadata] MemberId => string MemberMetadata => bytes */ function decodeJoinGroupResponse (resp) { var result = { members: [] }; var error; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word16bs('errorCode') .tap(function (vars) { error = createGroupError(vars.errorCode); }) .word32bs('generationId') .tap(function (vars) { result.generationId = vars.generationId; }) .word16bs('groupProtocol') .tap(function (vars) { this.buffer('groupProtocol', vars.groupProtocol); result.groupProtocol = vars.groupProtocol = vars.groupProtocol.toString(); }) .word16bs('leaderId') .tap(function (vars) { this.buffer('leaderId', vars.leaderId); result.leaderId = vars.leaderId = vars.leaderId.toString(); }) .word16bs('memberId') .tap(function (vars) { this.buffer('memberId', vars.memberId); result.memberId = vars.memberId = vars.memberId.toString(); }) .word32bs('memberNum') .loop(function (end, vars) { if (error) { return end(); } if (vars.memberNum-- === 0) return end(); var memberMetadata; this.word16bs('groupMemberId') .tap(function (vars) { this.buffer('groupMemberId', vars.groupMemberId); vars.memberId = vars.groupMemberId.toString(); }) .word32bs('memberMetadata') .tap(function (vars) { if (vars.memberMetadata > -1) { this.buffer('memberMetadata', vars.memberMetadata); memberMetadata = decodeGroupData(this.vars.memberMetadata); memberMetadata.id = vars.memberId; result.members.push(memberMetadata); } }); }); return error || result; } function decodeGroupData (resp) { var topics = []; var parsed = Binary.parse(resp) .word16bs('version') .word32bs('subscriptionNum') .loop(function decodeSubscription (end, vars) { if (vars.subscriptionNum-- === 0) return end(); this.word16bs('topic').tap(function () { this.buffer('topic', vars.topic); topics.push(vars.topic.toString()); }); }) .word32bs('userData') .tap(function (vars) { if (vars.userData === -1) { vars.userData = undefined; return; } this.buffer('userData', vars.userData); try { vars.userData = JSON.parse(vars.userData.toString()); } catch (error) { vars.userData = 'JSON parse error'; } }).vars; return { subscription: topics, version: parsed.version, userData: parsed.userData }; } function encodeDescribeGroups (clientId, correlationId, groups) { const request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.describeGroups); request.Int32BE(groups.length); groups.forEach(groupId => { request.Int16BE(groupId.length).string(groupId); }); return encodeRequestWithLength(request.make()); } function decodeDescribeGroups (resp) { let results = {}; let error; Binary.parse(resp).word32bs('size').word32bs('correlationId').word32bs('describeNum').loop(decodeDescriptions); function decodeDescriptions (end, vars) { if (vars.describeNum-- === 0) return end(); let described = { members: [] }; this.word16bs('errorCode') .tap(vars => { error = createGroupError(vars.errorCode); }) .word16bs('groupId') .tap(vars => { this.buffer('groupId', vars.groupId); described.groupId = vars.groupId.toString(); }) .word16bs('state') .tap(vars => { this.buffer('state', vars.state); described.state = vars.state.toString(); }) .word16bs('protocolType') .tap(vars => { this.buffer('protocolType', vars.protocolType); described.protocolType = vars.protocolType.toString(); }) .word16bs('protocol') .tap(vars => { this.buffer('protocol', vars.protocol); described.protocol = vars.protocol.toString(); // keep this for error cases results[described.groupId] = described; }) .word32bs('membersNum') .loop(function decodeGroupMembers (end, vars) { if (vars.membersNum-- === 0) return end(); let member = {}; this.word16bs('memberId') .tap(vars => { this.buffer('memberId', vars.memberId); member.memberId = vars.memberId.toString(); }) .word16bs('clientId') .tap(vars => { this.buffer('clientId', vars.clientId); member.clientId = vars.clientId.toString(); }) .word16bs('clientHost') .tap(vars => { this.buffer('clientHost', vars.clientHost); member.clientHost = vars.clientHost.toString(); }) .word32bs('memberMetadata') .tap(vars => { if (vars.memberMetadata > -1) { this.buffer('memberMetadata', vars.memberMetadata); let memberMetadata = decodeGroupData(vars.memberMetadata); memberMetadata.id = member.memberId; member.memberMetadata = memberMetadata; } }) .word32bs('memberAssignment') .tap(vars => { this.buffer('memberAssignment', vars.memberAssignment); member.memberAssignment = decodeMemberAssignment(vars.memberAssignment); described.members.push(member); results[described.groupId] = described; }); }); } return error || results; } function encodeListGroups (clientId, correlationId) { return encodeRequestWithLength(encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.listGroups).make()); } function decodeListGroups (resp) { let groups = {}; let error; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word16bs('errorCode') .tap(vars => { error = createGroupError(vars.errorCode); }) .word32bs('groupNum') .loop(function (end, vars) { if (vars.groupNum-- === 0) return end(); this.word16bs('groupId') .tap(function (vars) { this.buffer('groupId', vars.groupId); vars.groupId = vars.groupId.toString(); }) .word16bs('protocolType') .tap(function (vars) { this.buffer('protocolType', vars.protocolType); groups[vars.groupId] = vars.protocolType.toString(); }); }); return error || groups; } function encodeVersionsRequest (clientId, correlationId) { return encodeRequestWithLength(encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.apiVersions).make()); } function decodeVersionsResponse (resp) { const MAX_SUPPORT_VERSION = require('./protocolVersions').maxSupport; const versions = Object.create(null); let error = null; Binary.parse(resp) .word32bs('size') .word32bs('correlationId') .word16bs('errorCode') .tap(function (vars) { error = createGroupError(vars.errorCode); }) .word32bs('apiNum') .loop(function (end, vars) { if (vars.apiNum-- === 0 || error) return end(); let apiKey, minVersion, maxVersion; this.word16bs('apiKey') .tap(vars => { apiKey = vars.apiKey; }) .word16bs('minVersion') .tap(vars => { minVersion = vars.minVersion; }) .word16bs('maxVersion') .tap(vars => { maxVersion = vars.maxVersion; }); const apiName = apiKey in API_KEY_TO_NAME ? API_KEY_TO_NAME[apiKey] : apiKey; versions[apiName] = { min: minVersion, max: maxVersion, usable: MAX_SUPPORT_VERSION[apiName] != null ? Math.min(MAX_SUPPORT_VERSION[apiName], maxVersion) : false }; }); return error || versions; } function encodeDescribeConfigsRequest (clientId, correlationId, payload, apiVersion) { let request = encodeRequestHeader(clientId, correlationId, REQUEST_TYPE.describeConfigs, apiVersion); const resources = payload.resources; request.Int32BE(resources.length); resources.forEach(function (resource) { request.Int8(resource.resourceType); request.Int16BE(resource.resourceName.l