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.

428 lines
13 KiB

'use strict';
var events = require('events');
var util = require('util');
var net = require('net');
var tls = require('tls');
var fs = require('fs');
var _ = require('lodash');
var methods = require('./definitions').methods;
var Channel = require('./channel');
var debug = require('./debug');
var Exchange = module.exports = function Exchange (connection, channel, name, options, openCallback) {
Channel.call(this, connection, channel);
this.name = name;
this.binds = 0; // keep track of queues bound
this.exchangeBinds = 0; // keep track of exchanges bound
this.sourceExchanges = {};
this.options = _.defaults(options || {}, {autoDelete: true});
this._openCallback = openCallback;
this._sequence = null;
this._unAcked = {};
this._addedExchangeErrorHandler = false;
};
util.inherits(Exchange, Channel);
// creates an error handler scoped to the given `exchange`
function createExchangeErrorHandlerFor (exchange) {
return function (err) {
if (!exchange.options.confirm) return;
// should requeue instead?
// https://www.rabbitmq.com/reliability.html#producer
debug && debug('Exchange error handler triggered, erroring and wiping all unacked publishes');
for (var id in exchange._unAcked) {
var task = exchange._unAcked[id];
task.emit('ack error', err);
delete exchange._unAcked[id];
}
};
}
Exchange.prototype._onMethod = function (channel, method, args) {
this.emit(method.name, args);
if (this._handleTaskReply.apply(this, arguments))
return true;
var cb;
switch (method) {
case methods.channelOpenOk:
this._sequence = null;
if (!this._addedExchangeErrorHandler) {
var errorHandler = createExchangeErrorHandlerFor(this);
this.connection.on('error', errorHandler);
this.on('error', errorHandler);
this._addedExchangeErrorHandler = true;
}
// Pre-baked exchanges don't need to be declared
if (/^$|(amq\.)/.test(this.name)) {
//If confirm mode is specified we have to set it no matter the exchange.
if (this.options.confirm) {
this._confirmSelect(channel);
return;
}
this.state = 'open';
// - issue #33 fix
if (this._openCallback) {
this._openCallback(this);
this._openCallback = null;
}
// --
this.emit('open');
// For if we want to delete a exchange,
// we dont care if all of the options match.
} else if (this.options.noDeclare) {
if (this.options.confirm) {
this._confirmSelect(channel);
return;
}
this.state = 'open';
if (this._openCallback) {
this._openCallback(this);
this._openCallback = null;
}
this.emit('open');
} else {
this.connection._sendMethod(channel, methods.exchangeDeclare,
{ reserved1: 0
, reserved2: false
, reserved3: false
, exchange: this.name
, type: this.options.type || 'topic'
, passive: !!this.options.passive
, durable: !!this.options.durable
, autoDelete: !!this.options.autoDelete
, internal: !!this.options.internal
, noWait: false
, "arguments":this.options.arguments || {}
});
this.state = 'declaring';
}
break;
case methods.exchangeDeclareOk:
if (this.options.confirm) {
this._confirmSelect(channel);
} else {
this.state = 'open';
this.emit('open');
if (this._openCallback) {
this._openCallback(this);
this._openCallback = null;
}
}
break;
case methods.confirmSelectOk:
this._sequence = 1;
this.state = 'open';
this.emit('open');
if (this._openCallback) {
this._openCallback(this);
this._openCallback = null;
}
break;
case methods.channelClose:
this.state = "closed";
this.closeOK();
this.connection.exchangeClosed(this.name);
var e = new Error(args.replyText);
e.code = args.replyCode;
this.emit('error', e);
this.emit('close');
break;
case methods.channelCloseOk:
this.connection.exchangeClosed(this.name);
this.emit('close');
break;
case methods.basicAck:
this.emit('basic-ack', args);
var sequenceNumber = args.deliveryTag.readUInt32BE(4), tag;
debug && debug("basic-ack, sequence: ", sequenceNumber);
if (sequenceNumber === 0 && args.multiple === true) {
// we must ack everything
for (tag in this._unAcked) {
this._unAcked[tag].emit('ack');
delete this._unAcked[tag];
}
} else if (sequenceNumber !== 0 && args.multiple === true) {
// we must ack everything before the delivery tag
for (tag in this._unAcked) {
if (tag <= sequenceNumber) {
this._unAcked[tag].emit('ack');
delete this._unAcked[tag];
}
}
} else if (this._unAcked[sequenceNumber] && args.multiple === false) {
// simple single ack
this._unAcked[sequenceNumber].emit('ack');
delete this._unAcked[sequenceNumber];
}
break;
case methods.basicReturn:
this.emit('basic-return', args);
break;
case methods.exchangeBindOk:
if (this._bindCallback) {
// setting this._bindCallback to null before calling the callback allows for a subsequent bind within the callback
cb = this._bindCallback;
this._bindCallback = null;
cb(this);
}
break;
case methods.exchangeUnbindOk:
if (this._unbindCallback) {
cb = this._unbindCallback;
this._unbindCallback = null;
cb(this);
}
break;
default:
throw new Error("Uncaught method '" + method.name + "' with args " +
JSON.stringify(args));
}
this._tasksFlush();
};
// exchange.publish('routing.key', 'body');
//
// the third argument can specify additional options
// - mandatory (boolean, default false)
// - immediate (boolean, default false)
// - contentType (default 'application/octet-stream')
// - contentEncoding
// - headers
// - deliveryMode
// - priority (0-9)
// - correlationId
// - replyTo
// - expiration
// - messageId
// - timestamp
// - userId
// - appId
// - clusterId
//
// the callback is optional and is only used when confirm is turned on for the exchange
Exchange.prototype.publish = function (routingKey, data, options, callback) {
var self = this;
callback = callback || function() {};
if (this.connection._blocked) {
return callback(true, new Error('Connection is blocked, server reason: ' + this.connection._blockedReason));
}
if (this.state !== 'open') {
this._sequence = null;
return callback(true, new Error('Can not publish: exchange is not open'));
}
if (this.options.confirm && !this._readyToPublishWithConfirms()) {
return callback(true, new Error('Not yet ready to publish with confirms'));
}
options = _.assignIn({}, options || {});
options.routingKey = routingKey;
options.exchange = self.name;
options.mandatory = options.mandatory ? true : false;
options.immediate = options.immediate ? true : false;
options.reserved1 = 0;
var task = this._taskPush(null, function () {
self.connection._sendMethod(self.channel, methods.basicPublish, options);
// This interface is probably not appropriate for streaming large files.
// (Of course it's arguable about whether AMQP is the appropriate
// transport for large files.) The content header wants to know the size
// of the data before sending it - so there's no point in trying to have a
// general streaming interface - streaming messages of unknown size simply
// isn't possible with AMQP. This is all to say, don't send big messages.
// If you need to stream something large, chunk it yourself.
self.connection._sendBody(self.channel, data, options);
});
if (self.options.confirm) self._awaitConfirm(task, callback);
return task;
};
// registers tasks for confirms
Exchange.prototype._awaitConfirm = function _awaitConfirm (task, callback) {
if (!this._addedExchangeErrorHandler) {
// if connection fails, we want to ack error all unacked publishes.
this.connection.on('error', createExchangeErrorHandlerFor(this));
this.on('error', createExchangeErrorHandlerFor(this));
this._addedExchangeErrorHandler = true;
}
debug && debug('awaiting confirmation for ' + this._sequence);
task.sequence = this._sequence;
this._unAcked[this._sequence] = task;
this._sequence++;
if ('function' != typeof callback) return;
task.once('ack error', function (err) {
task.removeAllListeners();
callback(true, err);
});
task.once('ack', function () {
task.removeAllListeners();
callback(false);
});
};
// do any necessary cleanups eg. after queue destruction
Exchange.prototype.cleanup = function() {
if (this.binds === 0) { // don't keep reference open if unused
this.connection.exchangeClosed(this.name);
}
};
Exchange.prototype.destroy = function (ifUnused) {
var self = this;
return this._taskPush(methods.exchangeDeleteOk, function () {
self.connection.exchangeClosed(self.name);
self.connection._sendMethod(self.channel, methods.exchangeDelete,
{ reserved1: 0
, exchange: self.name
, ifUnused: ifUnused ? true : false
, noWait: false
});
});
};
// E2E Unbind
// support RabbitMQ's exchange-to-exchange binding extension
// http://www.rabbitmq.com/e2e.html
Exchange.prototype.unbind = function (/* exchange, routingKey [, bindCallback] */) {
var self = this;
// Both arguments are required. The binding to the destination
// exchange/routingKey will be unbound.
var exchange = arguments[0]
, routingKey = arguments[1]
, callback = arguments[2]
;
if (callback) this._unbindCallback = callback;
return this._taskPush(methods.exchangeUnbindOk, function () {
var source = exchange instanceof Exchange ? exchange.name : exchange;
var destination = self.name;
if (source in self.connection.exchanges) {
delete self.sourceExchanges[source];
self.connection.exchanges[source].exchangeBinds--;
}
self.connection._sendMethod(self.channel, methods.exchangeUnbind,
{ reserved1: 0
, destination: destination
, source: source
, routingKey: routingKey
, noWait: false
, "arguments": {}
});
});
};
// E2E Bind
// support RabbitMQ's exchange-to-exchange binding extension
// http://www.rabbitmq.com/e2e.html
Exchange.prototype.bind = function (/* exchange, routingKey [, bindCallback] */) {
var self = this;
// Two arguments are required. The binding to the destination
// exchange/routingKey will be established.
var exchange = arguments[0]
, routingKey = arguments[1]
, callback = arguments[2]
;
if (callback) this._bindCallback = callback;
var source = exchange instanceof Exchange ? exchange.name : exchange;
var destination = self.name;
if(source in self.connection.exchanges) {
self.sourceExchanges[source] = self.connection.exchanges[source];
self.connection.exchanges[source].exchangeBinds++;
}
self.connection._sendMethod(self.channel, methods.exchangeBind,
{ reserved1: 0
, destination: destination
, source: source
, routingKey: routingKey
, noWait: false
, "arguments": {}
});
};
// E2E Bind
// support RabbitMQ's exchange-to-exchange binding extension
// http://www.rabbitmq.com/e2e.html
Exchange.prototype.bind_headers = function (/* exchange, routing [, bindCallback] */) {
var self = this;
// Two arguments are required. The binding to the destination
// exchange/routingKey will be established.
var exchange = arguments[0]
, routing = arguments[1]
, callback = arguments[2]
;
if (callback) this._bindCallback = callback;
var source = exchange instanceof Exchange ? exchange.name : exchange;
var destination = self.name;
if (source in self.connection.exchanges) {
self.sourceExchanges[source] = self.connection.exchanges[source];
self.connection.exchanges[source].exchangeBinds++;
}
self.connection._sendMethod(self.channel, methods.exchangeBind,
{ reserved1: 0
, destination: destination
, source: source
, routingKey: ''
, noWait: false
, "arguments": routing
});
};
Exchange.prototype._confirmSelect = function(channel) {
this.connection._sendMethod(channel, methods.confirmSelect, { noWait: false });
};
Exchange.prototype._readyToPublishWithConfirms = function() {
return this._sequence != null;
};