UNPKG

replay-rabbitmq

Version:

RabbitMQ wrapper for Replay project

145 lines (127 loc) 5.36 kB
var amqp = require('amqplib'); var util = require('util'); var connection, channel; var MAX_RESEND_ATTEMPTS = process.env.RABBITMQ_MAX_RESEND_ATTEMPTS || 3; var FAILED_JOBS_QUEUE = process.env.FAILED_JOBS_QUEUE_NAME || 'FailedJobsQueue'; // USERNAME:PASSWORD@HOST:PORT var RABBIT_URI_FORMAT = 'amqp://%s:%s@%s:%s'; module.exports.connect = function (host, port, username, password) { var rabbitUri = util.format(RABBIT_URI_FORMAT, username, password, host, port); // connect rabbitmq, then connect/create channel. return amqp.connect(rabbitUri) .then(createChannel); }; // connecting to channel, attaching to appropriate queue and perform // user callback upon incoming messages. // maxUnackedMessagesAmount is the maximum number of messages sent over the consumer that can be awaiting ack // callback should accept 3 parameters: params (the message itself), error, done. // error should be called whenever we want to signal that we've failed and we want to re-try process the message later // done should be called whenever we want to signal that we've finished and the message should be erased from queue module.exports.consume = function (queueName, maxUnackedMessagesAmount, callback) { // create queue if not exists return assertQueue(queueName) .then(function (ok) { console.log('Attached to queue:', queueName); // define maximum num of messages to be processed without receiving ack // false flag means to apply this to every new consumer channel.prefetch(maxUnackedMessagesAmount, false); return channel.consume(queueName, function (msg) { // null msg is sent by RabbitMQ when consumer is cancelled (e.g. queue deleted) if (msg === null) { return; } // convert to object var messageContent = JSON.parse(msg.content.toString()); console.log('Received message from queue %s: %s', queueName, JSON.stringify(messageContent)); // check how many times the message has failed var currentTransmissionNum = 0; if (msg.properties.headers['x-death']) { currentTransmissionNum = msg.properties.headers['x-death'][0].count; } // if message passed maximum of re-send attempts, pass it to failed jobs queue if (currentTransmissionNum > MAX_RESEND_ATTEMPTS) { console.log('Message exceeded resend attempts amount, passing to failed jobs queue...'); // produce to failed jobs queue asynchrously then ack message from old queue produce(FAILED_JOBS_QUEUE, messageContent) .then(function() { channel.ack(msg); return Promise.resolve(); }); } else { // else, just send the message. // exponential backoff: calculate total sleep time in millis var totalSleepMillis = Math.pow(2, currentTransmissionNum) * 1000; setTimeout(function () { // invoke callback and pass an err & done method which acks the message callback(messageContent, function err() { // negative-acks the message channel.nack(msg, false, false); }, function done() { // acks the message channel.ack(msg); }); }, totalSleepMillis); } }, { noAck: false }); }) .catch(function (err) { console.log('Error in consuming from %s: %s', queueName, err); throw err; }); }; // produce a message to specific queue. // this method does not return anything no purpose; // the client has nothing to do with such failures, the message jus't won't be sent // and a log will be emitted. function produce(queueName, message) { // create queue if not exists return assertQueue(queueName) .then(function (ok) { console.log('Sending message to queue %s: %s', queueName, JSON.stringify(message)); return channel.sendToQueue(queueName, new Buffer(JSON.stringify(message)), { persistent: true }); }) .catch(function (err) { console.log('Error in producing to %s: %s', queueName, err); throw err; }); } module.exports.produce = produce; module.exports.deleteQueue = function (queueName) { return channel.deleteQueue(queueName) .catch(function (err) { console.log('Error in deleting queue %s', queueName); throw err; }); }; function createChannel(conn) { connection = conn; return connection.createChannel() .then(function (ch) { channel = ch; channel.on('close', onChannelClose); channel.on('error', onChannelError); console.log('Connected to RabbitMQ.'); }); } // create queue if not exists function assertQueue(queueName) { // durable: persist queue messages // deadLetterExchange: use default exchange // deadLetterRoutingKey: router dead letter messages back to original queue return channel.assertQueue(queueName, { durable: true, deadLetterExchange: '', deadLetterRoutingKey: queueName }); } // A channel will emit 'close' once the closing handshake (possibly initiated by calling close()) has completed; // or, if its connection closes. function onChannelClose() { console.log('Channel has closed.'); } // A channel will emit 'error' if the server closes the channel for any reason. // Such reasons include: // * an operation failed due to a failed precondition (usually something named in an argument not existing) // * an human closed the channel with an admin tool // A channel will not emit 'error' if its connection closes with an error. function onChannelError() { console.log('Channel has errored.'); }