@voicenter-team/failover-amqp-pool
Version:
314 lines (283 loc) • 9.85 kB
JavaScript
let process = require('process');
const Metrics = require('./metrics')
const METRICS_NAMES = require('./metrics/names')
function build(pool, parsedConfig){
let url = _buildUrl(pool.connection);
parsedConfig[url] = (pool.hasOwnProperty(url)) ? parsedConfig[url] : {};
parsedConfig[url].channels = (parsedConfig[url].hasOwnProperty('channels')) ? parsedConfig[url].channels : [];
if(pool.hasOwnProperty('channels'))
parsedConfig[url].channels = parsedConfig[url].channels.concat(pool.channels)
else
parsedConfig[url].channels.push(Object.assign( {}, pool?.defaultChannelSettings, pool.channel) );
parsedConfig[url].config = pool.connection;
return parsedConfig;
}
function _parsConfig (rawConfig) {
let parsedConfig = {};
for (let item in rawConfig) {
build(rawConfig[item], parsedConfig)
}
return parsedConfig;
}
function _buildUrl(config) {
let url = (config.ssl ? 'amqps' : 'amqp') + '://';
if (config.username && config.password) {
url += config.username + ':' + config.password + '@';
}
url += config.host + ':' + config.port;
if (config.vhost) {
url += config.vhost;
}
if (config.hasOwnProperty('heartbeat')) {
url += '?heartbeat=' + config.heartbeat;
}
return url;
}
const Connection = require('./Connection');
const Channel = require('./Channel');
const EventEmitter = require('events').EventEmitter;
const hash = require('object-hash');
class AMQPPool extends EventEmitter {
constructor(rawConfig) {
super();
this.connections = [];
this.msgCache= [];
this.interval;
// round_robin
this.rr_i = 0;
this.addConnection(rawConfig);
process.on('SIGINT', () => {
this.stop((err) => {
this.emit('info', "Exit on SIGINT")
process.exit(err ? 1 : 0);
})
});
this.metrics = new Metrics()
// Metrics used to work with connections
}
start(msgCacheInterval = 2000) {
this.interval = setInterval(() => {
let aliveChannels = this.getAllChannels().length
this.metrics.metric({
type: 'gauge',
name: 'rabbit_message_cached_count',
value: () => this.msgCache.length,
})
this.metrics.metric({
type: 'gauge',
name: 'rabbit_all_channels_count',
value: () => (this.getAllChannels()).length,
})
this.metrics.metric({
type: 'gauge',
name: 'rabbit_alive_channels_count',
value: () => aliveChannels
})
this.metrics.metric({
type: 'gauge',
name: 'rabbit_connections_total_count',
value: () => this.connections.length,
})
if (this.msgCache.length > 0 && aliveChannels > 0) {
let m = this.msgCache.shift();
this.publish(m.msg, m.filter, m.topic, m.props);
}
}, msgCacheInterval);
}
async stop() {
let promises = []
clearInterval(this.interval);
this.removeAllListeners();
for (const [connectionIndex, _connection] of this.connections.entries()) {
promises.push(
new Promise( (resolve) => {
if(!_connection.alive){
this.connections.splice(connectionIndex, 1);
return resolve(connectionIndex)
}
_connection.once('close', () => {
this.connections.splice(connectionIndex, 1);
resolve(connectionIndex)
})
_connection.disconnect();
})
)
}
return Promise.all(promises)
}
addConnection(rawConfig) {
let channelIds = []
let config = _parsConfig(rawConfig);
for (let _url in config) {
let connectionIndex = this.getConnectionIndexByUrl(_url);
if(!connectionIndex) {
// Create a new connection
connectionIndex = this.createConnection(_url, config[_url].config);
}
for (let channelConfigIndex in config[_url].channels) {
if(!this.getChannelByHash(connectionIndex, hash(config[_url].channels[channelConfigIndex]))) {
// Create a new channel
let id = this.createChannel(connectionIndex, config[_url].channels[channelConfigIndex])
channelIds.push(id)
}
}
}
return channelIds
}
// ToDo: Needs some DRYing
publish(msg, filter, topic, props) {
let channels = this.getAliveChannels();
if (typeof filter == 'function') {
let filteredChannels = filter(channels);
if (!filteredChannels) this.msgCache.push({msg, filter, topic, props});
else {
if (filteredChannels instanceof Channel) filteredChannels = [filteredChannels];
for (let channelIndex in filteredChannels) {
try {
filteredChannels[channelIndex].publish(msg, topic, props);
} catch (e) {
this.emit('error', e)
this.msgCache.push({msg, filter, topic, props});
}
}
}
} else if (filter === 'rr') {
if (channels.length > 0 ) {
if(this.rr_i >= channels.length) {
this.rr_i = 0;
}
try {
channels[this.rr_i++].publish(msg, topic, props);
} catch (e) {
this.msgCache.push({msg, filter, topic, props});
}
} else {
this.msgCache.push({msg, filter, topic, props});
}
} else if (filter === 'all') {
if (channels.length > 0) {
for (let channelIndex in channels) {
try {
channels[channelIndex].publish(msg, topic, props);
} catch (e) {
this.msgCache.push({msg, filter, topic, props});
}
}
} else {
this.msgCache.push({msg, filter, topic, props});
}
} else { // first alive
if (channels.length > 0) {
try {
channels[0].publish(msg, topic, props);
} catch (e) {
this.emit('error', e)
this.msgCache.push({msg, filter, topic, props});
}
} else {
this.msgCache.push({msg, filter, topic, props});
}
}
}
ack(msg) {
this.getChannelById(msg.properties.channelId).map((channel) => {
channel.ack(msg);
});
}
nack(msg) {
this.getChannelById(msg.properties.channelId).map((channel) => {
channel.nack(msg);
});
}
getAliveChannels() {
let channels = [];
for (let connectionIndex in this.connections) {
if (this.connections[connectionIndex].alive) {
for (let channelIndex in this.connections[connectionIndex].channels) {
if (this.connections[connectionIndex].channels[channelIndex].alive) {
channels.push(this.connections[connectionIndex].channels[channelIndex]);
}
}
}
}
return channels;
}
getChannelByHash(connectionIndex, hash) {
return this.connections[connectionIndex].channels.find(channel => channel.hash === hash)
}
getAllChannels() {
let channels = [];
for (let connectionIndex in this.connections) {
channels = channels.concat(this.connections[connectionIndex].channels);
}
return channels;
}
getConnectionIndexByUrl(url){
for (let connectionIndex in this.connections){
if(this.connections[connectionIndex].url === url){
return connectionIndex
}
}
return false;
}
getChannelById(id) {
let channels = [];
for (let connectionIndex in this.connections) {
for (let channelIndex in this.connections[connectionIndex].channels) {
if (this.connections[connectionIndex].channels[channelIndex]._id === id) {
channels.push(this.connections[connectionIndex].channels[channelIndex]);
}
}
}
return channels;
}
async removeChannelById(id){
let promises = []
for (let connectionIndex in this.connections) {
for (let channelIndex in this.connections[connectionIndex].channels) {
if (this.connections[connectionIndex].channels[channelIndex]._id === id) {
promises.push(
new Promise( (resolve) => {
this.connections[connectionIndex].channels[channelIndex].once('close', () => {
this.connections[connectionIndex].channels.splice(channelIndex, 1);
resolve(channelIndex)
})
this.connections[connectionIndex].channels[channelIndex]?.close()
})
)
}
}
}
return Promise.all(promises)
}
createConnection(url, connectionConfig){
let connection = new Connection(url, connectionConfig);
connection.on('close', () => {
this.emit('close', url)
connection?.metrics?.metric(METRICS_NAMES.reconnectedConnectionsCount)?.inc()
});
connection.on('error', (error) => {
this.emit('error', error)
})
connection.on('info', (info) => this.emit('info', info))
connection.on('connection', () => this.emit('connection', url))
connection.start();
return this.connections.push(connection) - 1;
}
createChannel(connectionIndex, Channelconfig) {
const connection = this.connections[connectionIndex]
let channel = new Channel(connection, Channelconfig);
channel.on('ready', (channel) => this.emit('ready', channel));
channel.on('close', (close) => this.emit('close', close));
channel.on('error', (error) => this.emit('error', error));
channel.on('channelMessage', (msg) => {
this.emit('channelMessage', msg)
});
channel.on('info', (msg) => this.emit('info', msg));
connection.addChannel(channel);
if(connection.alive)
channel.create();
return channel._id
}
}
module.exports = AMQPPool;