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.

323 lines
10 KiB

  1. /*
  2. * node-rdkafka - Node.js wrapper for RdKafka C/C++ library
  3. *
  4. * Copyright (c) 2016 Blizzard Entertainment
  5. *
  6. * This software may be modified and distributed under the terms
  7. * of the MIT license. See the LICENSE.txt file for details.
  8. */
  9. module.exports = HighLevelProducer;
  10. var util = require('util');
  11. var Producer = require('../producer');
  12. var LibrdKafkaError = require('../error');
  13. var EventEmitter = require('events').EventEmitter;
  14. var RefCounter = require('../tools/ref-counter');
  15. var shallowCopy = require('../util').shallowCopy;
  16. var isObject = require('../util').isObject;
  17. util.inherits(HighLevelProducer, Producer);
  18. var noopSerializer = createSerializer(function (v) { return v; });
  19. /**
  20. * Create a serializer
  21. *
  22. * Method simply wraps a serializer provided by a user
  23. * so it adds context to the error
  24. *
  25. * @returns {function} Serialization function
  26. */
  27. function createSerializer(serializer) {
  28. var applyFn = function serializationWrapper(v, cb) {
  29. try {
  30. return cb ? serializer(v, cb) : serializer(v);
  31. } catch (e) {
  32. var modifiedError = new Error('Could not serialize value: ' + e.message);
  33. modifiedError.value = v;
  34. modifiedError.serializer = serializer;
  35. throw modifiedError;
  36. }
  37. };
  38. // We can check how many parameters the function has and activate the asynchronous
  39. // operation if the number of parameters the function accepts is > 1
  40. return {
  41. apply: applyFn,
  42. async: serializer.length > 1
  43. };
  44. }
  45. /**
  46. * Producer class for sending messages to Kafka in a higher level fashion
  47. *
  48. * This is the main entry point for writing data to Kafka if you want more
  49. * functionality than librdkafka supports out of the box. You
  50. * configure this like you do any other client, with a global
  51. * configuration and default topic configuration.
  52. *
  53. * Once you instantiate this object, you need to connect to it first.
  54. * This allows you to get the metadata and make sure the connection
  55. * can be made before you depend on it. After that, problems with
  56. * the connection will by brought down by using poll, which automatically
  57. * runs when a transaction is made on the object.
  58. *
  59. * This has a few restrictions, so it is not for free!
  60. *
  61. * 1. You may not define opaque tokens
  62. * The higher level producer is powered by opaque tokens.
  63. * 2. Every message ack will dispatch an event on the node thread.
  64. * 3. Will use a ref counter to determine if there are outgoing produces.
  65. *
  66. * This will return the new object you should use instead when doing your
  67. * produce calls
  68. *
  69. * @param {object} conf - Key value pairs to configure the producer
  70. * @param {object} topicConf - Key value pairs to create a default
  71. * topic configuration
  72. * @extends Producer
  73. * @constructor
  74. */
  75. function HighLevelProducer(conf, topicConf) {
  76. if (!(this instanceof HighLevelProducer)) {
  77. return new HighLevelProducer(conf, topicConf);
  78. }
  79. // Force this to be true for the high level producer
  80. conf = shallowCopy(conf);
  81. conf.dr_cb = true;
  82. // producer is an initialized consumer object
  83. // @see NodeKafka::Producer::Init
  84. Producer.call(this, conf, topicConf);
  85. var self = this;
  86. // Add a delivery emitter to the producer
  87. this._hl = {
  88. deliveryEmitter: new EventEmitter(),
  89. messageId: 0,
  90. // Special logic for polling. We use a reference counter to know when we need
  91. // to be doing it and when we can stop. This means when we go into fast polling
  92. // mode we don't need to do multiple calls to poll since they all will yield
  93. // the same result
  94. pollingRefTimeout: null,
  95. };
  96. // Add the polling ref counter to the class which ensures we poll when we go active
  97. this._hl.pollingRef = new RefCounter(function() {
  98. self._hl.pollingRefTimeout = setInterval(function() {
  99. try {
  100. self.poll();
  101. } catch (e) {
  102. if (!self._isConnected) {
  103. // If we got disconnected for some reason there is no point
  104. // in polling anymore
  105. clearInterval(self._hl.pollingRefTimeout);
  106. }
  107. }
  108. }, 1);
  109. }, function() {
  110. clearInterval(self._hl.pollingRefTimeout);
  111. });
  112. // Default poll interval. More sophisticated polling is also done in create rule method
  113. this.setPollInterval(1000);
  114. // Listen to all delivery reports to propagate elements with a _message_id to the emitter
  115. this.on('delivery-report', function(err, report) {
  116. if (report.opaque && report.opaque.__message_id !== undefined) {
  117. self._hl.deliveryEmitter.emit(report.opaque.__message_id, err, report.offset);
  118. }
  119. });
  120. // Save old produce here since we are making some modifications for it
  121. this._oldProduce = this.produce;
  122. this.produce = this._modifiedProduce;
  123. // Serializer information
  124. this.keySerializer = noopSerializer;
  125. this.valueSerializer = noopSerializer;
  126. }
  127. /**
  128. * Produce a message to Kafka asynchronously.
  129. *
  130. * This is the method mainly used in this class. Use it to produce
  131. * a message to Kafka.
  132. *
  133. * When this is sent off, and you recieve your callback, the assurances afforded
  134. * to you will be equal to those provided by your ack level.
  135. *
  136. * @param {string} topic - The topic name to produce to.
  137. * @param {number|null} partition - The partition number to produce to.
  138. * @param {Buffer|null} message - The message to produce.
  139. * @param {string} key - The key associated with the message.
  140. * @param {number|null} timestamp - Timestamp to send with the message.
  141. * @param {object} headers - A list of custom key value pairs that provide message metadata.
  142. * @param {function} callback - Callback to call when the delivery report is recieved.
  143. * @throws {LibrdKafkaError} - Throws a librdkafka error if it failed.
  144. * @return {boolean} - returns an error if it failed, or true if not
  145. * @see Producer#produce
  146. */
  147. HighLevelProducer.prototype._modifiedProduce = function(topic, partition, message, key, timestamp, headers, callback) {
  148. // headers are optional
  149. if (arguments.length === 6) {
  150. callback = headers;
  151. headers = undefined;
  152. }
  153. // Add the message id
  154. var opaque = {
  155. __message_id: this._hl.messageId++,
  156. };
  157. this._hl.pollingRef.increment();
  158. var self = this;
  159. var resolvedSerializedValue;
  160. var resolvedSerializedKey;
  161. var calledBack = false;
  162. // Actually do the produce with new key and value based on deserialized
  163. // results
  164. function doProduce(v, k) {
  165. try {
  166. var r = self._oldProduce(topic, partition,
  167. v, k,
  168. timestamp, opaque, headers);
  169. self._hl.deliveryEmitter.once(opaque.__message_id, function(err, offset) {
  170. self._hl.pollingRef.decrement();
  171. setImmediate(function() {
  172. // Offset must be greater than or equal to 0 otherwise it is a null offset
  173. // Possibly because we have acks off
  174. callback(err, offset >= 0 ? offset : null);
  175. });
  176. });
  177. return r;
  178. } catch (e) {
  179. callback(e);
  180. }
  181. }
  182. function produceIfComplete() {
  183. if (resolvedSerializedKey !== undefined && resolvedSerializedValue !== undefined) {
  184. doProduce(resolvedSerializedValue, resolvedSerializedKey);
  185. }
  186. }
  187. // To run on a promise if returned by the serializer
  188. function finishSerializedValue(v) {
  189. if (!calledBack) {
  190. resolvedSerializedValue = v;
  191. produceIfComplete();
  192. }
  193. }
  194. // To run on a promise of returned by the serializer
  195. function finishSerializedKey(k) {
  196. resolvedSerializedKey = k;
  197. if (!calledBack) {
  198. produceIfComplete();
  199. }
  200. }
  201. function failSerializedValue(err) {
  202. if (!calledBack) {
  203. calledBack = true;
  204. callback(err);
  205. }
  206. }
  207. function failSerializedKey(err) {
  208. if (!calledBack) {
  209. calledBack = true;
  210. callback(err);
  211. }
  212. }
  213. function valueSerializerCallback(err, v) {
  214. if (err) {
  215. failSerializedValue(err);
  216. } else {
  217. finishSerializedValue(v);
  218. }
  219. }
  220. function keySerializerCallback(err, v) {
  221. if (err) {
  222. failSerializedKey(err);
  223. } else {
  224. finishSerializedKey(v);
  225. }
  226. }
  227. try {
  228. if (this.valueSerializer.async) {
  229. // If this is async we need to give it a callback
  230. this.valueSerializer.apply(message, valueSerializerCallback);
  231. } else {
  232. var serializedValue = this.valueSerializer.apply(message);
  233. // Check if we were returned a promise in order to support promise behavior
  234. if (serializedValue &&
  235. typeof serializedValue.then === 'function' &&
  236. typeof serializedValue.catch === 'function') {
  237. // This is a promise. We need to hook into its then and catch
  238. serializedValue.then(finishSerializedValue).catch(failSerializedValue);
  239. } else {
  240. resolvedSerializedValue = serializedValue;
  241. }
  242. }
  243. if (this.keySerializer.async) {
  244. // If this is async we need to give it a callback
  245. this.keySerializer.apply(key, keySerializerCallback);
  246. } else {
  247. var serializedKey = this.keySerializer.apply(key);
  248. // Check if we were returned a promise in order to support promise behavior
  249. if (serializedKey &&
  250. typeof serializedKey.then === 'function' &&
  251. typeof serializedKey.catch === 'function') {
  252. // This is a promise. We need to hook into its then and catch
  253. serializedKey.then(finishSerializedKey).catch(failSerializedKey);
  254. } else {
  255. resolvedSerializedKey = serializedKey;
  256. }
  257. }
  258. // Only do the produce here if we are complete. That is, if the key
  259. // and value have been serialized.
  260. produceIfComplete();
  261. } catch (e) {
  262. setImmediate(function() {
  263. calledBack = true;
  264. callback(e);
  265. });
  266. }
  267. };
  268. /**
  269. * Set the key serializer
  270. *
  271. * This allows the value inside the produce call to differ from the value of the
  272. * value actually produced to kafka. Good if, for example, you want to serialize
  273. * it to a particular format.
  274. */
  275. HighLevelProducer.prototype.setKeySerializer = function(serializer) {
  276. this.keySerializer = createSerializer(serializer);
  277. };
  278. /**
  279. * Set the value serializer
  280. *
  281. * This allows the value inside the produce call to differ from the value of the
  282. * value actually produced to kafka. Good if, for example, you want to serialize
  283. * it to a particular format.
  284. */
  285. HighLevelProducer.prototype.setValueSerializer = function(serializer) {
  286. this.valueSerializer = createSerializer(serializer);
  287. };