@confluentinc/kafka-javascript
Version:
Node.js bindings for librdkafka
406 lines (359 loc) • 13.1 kB
JavaScript
/*
* confluent-kafka-javascript - Node.js wrapper for RdKafka C/C++ library
*
* Copyright (c) 2016-2023 Blizzard Entertainment
* (c) 2024 Confluent, Inc.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE.txt file for details.
*/
module.exports = Producer;
var Client = require('./client');
var util = require('util');
var Kafka = require('../librdkafka.js');
var ProducerStream = require('./producer-stream');
var LibrdKafkaError = require('./error');
var shallowCopy = require('./util').shallowCopy;
util.inherits(Producer, Client);
/**
* Producer class for sending messages to Kafka
*
* This is the main entry point for writing data to Kafka. You
* configure this like you do any other client, with a global
* configuration and default topic configuration.
*
* Once you instantiate this object, you need to connect to it first.
* This allows you to get the metadata and make sure the connection
* can be made before you depend on it. After that, problems with
* the connection will by brought down by using poll, which automatically
* runs when a transaction is made on the object.
*
* Methods on a Producer will throw a [LibrdKafkaError]{@link RdKafka.LibrdKafkaError}
* on failure.
*
* @param {object} conf - Key value pairs to configure the producer
* @param {object?} topicConf - Key value pairs to create a default
* topic configuration
* @extends RdKafka.Client
* @constructor
* @memberof RdKafka
* @see [Producer example]{@link https://github.com/confluentinc/confluent-kafka-javascript/blob/master/examples/node-rdkafka/producer.md}
*/
function Producer(conf, topicConf) {
if (!(this instanceof Producer)) {
return new Producer(conf, topicConf);
}
conf = shallowCopy(conf);
topicConf = shallowCopy(topicConf);
/**
* Producer message. This is sent to the wrapper, not received from it
*
* @typedef {object} Producer~Message
* @property {string|buffer} message - The buffer to send to Kafka.
* @property {Topic} topic - The Kafka topic to produce to.
* @property {number} partition - The partition to produce to. Defaults to
* the partitioner
* @property {string} key - The key string to use for the message.
* @see Consumer~Message
*/
var gTopic = conf.topic || false;
var gPart = conf.partition || null;
var dr_cb = conf.dr_cb || null;
var dr_msg_cb = conf.dr_msg_cb || null;
// delete keys we don't want to pass on
delete conf.topic;
delete conf.partition;
delete conf.dr_cb;
delete conf.dr_msg_cb;
// client is an initialized producer object
// @see NodeKafka::Producer::Init
Client.call(this, conf, Kafka.Producer, topicConf);
// Delete these keys after saving them in vars
this.globalConfig = conf;
this.topicConfig = topicConf;
this.defaultTopic = gTopic || null;
this.defaultPartition = gPart === null ? -1 : gPart;
this.sentMessages = 0;
this.pollInterval = undefined;
if (dr_msg_cb || dr_cb) {
this._cb_configs.event.delivery_cb = function(err, report) {
if (err) {
err = LibrdKafkaError.create(err);
}
this.emit('delivery-report', err, report);
}.bind(this);
this._cb_configs.event.delivery_cb.dr_msg_cb = !!dr_msg_cb;
if (typeof dr_cb === 'function') {
this.on('delivery-report', dr_cb);
}
}
}
/**
* Produce a message to Kafka asynchronously.
*
* This is the method mainly used in this class. Use it to produce
* a message to Kafka.
*
* When this is sent off, there is no guarantee it is delivered, as the method
* returns immediately after queuing the message in the library.
*
* If you need guaranteed delivery, change your `acks` settings and use delivery reports.
*
* @param {string} topic - The topic name to produce to.
* @param {number|null} partition - The partition number to produce to.
* @param {Buffer|null} message - The message to produce.
* @param {string} key - The key associated with the message.
* @param {number|null} timestamp - Timestamp to send with the message.
* @param {object} opaque - An object you want passed along with this message, if provided.
* This will be available in the delivery report
* @param {object} headers - A list of custom key value pairs that provide message metadata.
* @return {boolean} - throws an error if it failed, or returns true if not.
*/
Producer.prototype.produce = function(topic, partition, message, key, timestamp, opaque, headers) {
if (!this._isConnected) {
throw new Error('Producer not connected');
}
// I have removed support for using a topic object. It is going to be removed
// from librdkafka soon, and it causes issues with shutting down
if (!topic || typeof topic !== 'string') {
throw new TypeError('"topic" must be a string');
}
this.sentMessages++;
partition = partition === null ? this.defaultPartition : partition;
return this._errorWrap(
this._client.produce(topic, partition, message, key, timestamp, opaque, headers));
};
/**
* Create a write stream interface for a producer.
*
* This stream does not run in object mode. It only takes buffers of data.
*
* @param {object} conf - Key value pairs to configure the producer.
* @param {object} topicConf - Key value pairs to create a default topic configuration.
* @param {object} streamOptions - Stream options.
* @return {RdKafka.ProducerStream} - returns the write stream for writing to Kafka.
*/
Producer.createWriteStream = function(conf, topicConf, streamOptions) {
var producer = new Producer(conf, topicConf);
return new ProducerStream(producer, streamOptions);
};
/**
* Poll for events.
*
* We need to run poll in order to learn about new events that have occurred.
* This is not done automatically when we produce, so we need to run
* it manually, or set the producer to automatically poll using
* {@link RdKafka.Producer#setPollInterval} or {@link RdKafka.Producer#setPollInBackground}.
*
* @return {RdKafka.Producer} - returns itself.
*/
Producer.prototype.poll = function() {
if (!this._isConnected) {
throw new Error('Producer not connected');
}
this._client.poll();
return this;
};
/**
* Set automatic polling for events.
*
* We need to run poll in order to learn about new events that have occurred.
* If you would like this done on an interval with disconnects and reconnections
* managed, you can do it here.
*
* @param {number} interval - Interval, in milliseconds, to poll for.
*
* @return {RdKafka.Producer} - returns itself.
*/
Producer.prototype.setPollInterval = function(interval) {
// If we already have a poll interval we need to stop it
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = undefined;
}
if (interval === 0) {
// If the interval was set to 0, bail out. We don't want to process this.
// If there was an interval previously set, it has been removed.
return;
}
var self = this;
// Now we want to make sure we are connected.
if (!this._isConnected) {
// If we are not, execute this once the connection goes through.
this.once('ready', function() {
self.setPollInterval(interval);
});
return;
}
// We know we are connected at this point.
// Unref this interval
this.pollInterval = setInterval(function() {
try {
self.poll();
} catch {
// We can probably ignore errors here as far as broadcasting.
// Disconnection issues will get handled below
}
}, interval).unref();
// Handle disconnections
this.once('disconnected', function() {
// Just rerun this function with interval 0. If any
// poll interval is set, this will remove it
self.setPollInterval(0);
});
return this;
};
/**
* Set automatic polling for events on the librdkafka background thread.
*
* This provides several advantages over `setPollInterval`, as the polling
* does not happen on the event loop, but on the C thread spawned by librdkafka,
* and can be more efficient for high-throughput producers.
*
* If set = true, this will disable any polling interval set by `setPollInterval`.
*
* @param {boolean} set Whether to poll in the background or not.
* @returns {RdKafka.Producer} - returns itself.
*/
Producer.prototype.setPollInBackground = function(set) {
if (set) {
this.setPollInterval(0); // Clear poll interval from JS.
}
this._client.setPollInBackground(set);
return this;
};
/**
* Flush the producer,
*
* The producer accumulates messages upto `linger.ms` on an internal buffer.
* This method flushes on the buffer immediately. Do this before disconnects, usually.
*
* @param {number?} timeout - Number of milliseconds to try to flush before giving up.
* @param {function?} callback - Callback to fire when the flush is done.
*
* @return {RdKafka.Producer} - returns itself.
*/
Producer.prototype.flush = function(timeout, callback) {
if (!this._isConnected) {
throw new Error('Producer not connected');
}
if (timeout === undefined || timeout === null) {
timeout = 500;
}
this._client.flush(timeout, function(err) {
if (err) {
err = LibrdKafkaError.create(err);
}
if (callback) {
callback(err);
}
});
return this;
};
/**
* Save the base disconnect method here so we can overwrite it and add a flush.
* @private
*/
Producer.prototype._disconnect = Producer.prototype.disconnect;
/**
* Disconnect the producer.
*
* Flush everything on the internal librdkafka producer buffer. Then disconnect.
*
* @param {number?} timeout - Number of milliseconds to try to flush before giving up, defaults to 5 seconds.
* @param {function?} cb - The callback to fire when disconnected.
*/
Producer.prototype.disconnect = function(timeout, cb) {
var self = this;
var timeoutInterval = 5000;
if (typeof timeout === 'function') {
cb = timeout;
} else {
timeoutInterval = timeout;
}
this.flush(timeoutInterval, function() {
self._disconnect(cb);
});
};
/**
* Initialize transactions for this producer instance.
*
* Initialize transactions, this must only be performed once per transactional producer,
* before it can be used.
*
* @param {number} timeout - Number of milliseconds to try to initialize before giving up, defaults to 5 seconds.
* @param {function} cb - Callback to fire when operation is completed.
* @return {RdKafka.Producer} - returns itself.
*/
Producer.prototype.initTransactions = function(timeout, cb) {
if (typeof timeout === 'function') {
cb = timeout;
timeout = 5000;
}
this._client.initTransactions(timeout, function(err) {
cb(err ? LibrdKafkaError.create(err) : err);
});
};
/**
* Begin a transaction.
*
* `initTransaction` must have been called successfully (once) before this function is called.
*
* There can only be one ongoing transaction at a time.
* @return {RdKafka.Producer} - returns itself.
*/
Producer.prototype.beginTransaction = function(cb) {
this._client.beginTransaction(function(err) {
cb(err ? LibrdKafkaError.create(err) : err);
});
};
/**
* Commit the current transaction (as started with `beginTransaction`).
*
* @param {number} timeout - Number of milliseconds to try to commit before giving up, defaults to 5 seconds.
* @param {function} cb - Callback to fire when operation is completed.
* @return {RdKafka.Producer} - returns itself.
*/
Producer.prototype.commitTransaction = function(timeout, cb) {
if (typeof timeout === 'function') {
cb = timeout;
timeout = 5000;
}
this._client.commitTransaction(timeout, function(err) {
cb(err ? LibrdKafkaError.create(err) : err);
});
};
/**
* Aborts the ongoing transaction (as started with `beginTransaction`).
*
* @param {number} timeout - Number of milliseconds to try to abort, defaults to 5 seconds.
* @param {function} cb - Callback to fire when operation is completed.
* @return {RdKafka.Producer} - returns itself.
*/
Producer.prototype.abortTransaction = function(timeout, cb) {
if (typeof timeout === 'function') {
cb = timeout;
timeout = 5000;
}
this._client.abortTransaction(timeout, function(err) {
cb(err ? LibrdKafkaError.create(err) : err);
});
};
/**
* Send the current offsets of the consumer to the ongoing transaction.
*
* @param {RdKafka.TopicPartition[]} offsets - An array of topic-partitions with offsets filled in.
* @param {RdKafka.KafkaConsumer} consumer - An instance of the consumer to send offsets for.
* @param {number} timeout - Number of milliseconds to try to send offsets, defaults to 5 seconds.
* @param {function} cb - Callback to return when operation is completed.
* @return {RdKafka.Producer} - returns itself.
*/
Producer.prototype.sendOffsetsToTransaction = function(offsets, consumer, timeout, cb) {
if (typeof timeout === 'function') {
cb = timeout;
timeout = 5000;
}
this._client.sendOffsetsToTransaction(offsets, consumer.getClient(), timeout, function(err) {
cb(err ? LibrdKafkaError.create(err) : err);
});
};