ufilesync
Version:
520 lines (423 loc) • 11.9 kB
JavaScript
;
const fs = require('fs-extra');
const async = require('async');
const FormData = require('form-data');
const _ = require('lodash');
const exceptions = require('../../exceptions');
const http = require('http');
const keepAliveAgent = new http.Agent({ keepAlive: true });
const amqplib = require('amqplib/callback_api');
const configDefault = require('./config');
class Transmitter {
constructor(config) {
this.config = _.extend(configDefault, config);
this.processedLetters = this.config.processedLetters.split('');
// массивы с коллбэками по действиям
this.onAction = {
connected: [],
consume: [],
error: [],
taskComplete: [],
};
this.isConnected = false;
// Изначальное время задержки при повторной отправки сообщения
this.timeoutDeferSend = this.config.timeReconnect;
this.stackTasks = {};
this.channels = {};
this.getChannelsCb = [];
this.connectToRabbitMq();
}
debug(message) {
console.log(message);
}
/**
* Обработчик событий
* @param cbName
* @param cb
*/
on(cbName, cb) {
const me = this;
if (me.onAction[cbName]) {
if (cbName === 'connected' && me.isConnected) {
cb();
} else {
me.onAction[cbName].push(cb);
}
} else {
cb(new exceptions.ErrorTransmitter(cbName + ' - not exist this action'));
}
}
/**
* Запуск события, когда задача выполненна
*/
fireEventTaskComplete(task) {
const me = this;
if (task.message.dates) task.message.dates.end = new Date();
me.onAction.taskComplete.forEach(cb => {
cb(task);
});
}
/**
* Запуск событий, когда готов коннект к RabbitMq
*/
fireEventConnected() {
const me = this;
me.isConnected = true;
me.onAction.connected.forEach(cb => {
cb();
});
}
/**
* Запуск событий, когда приходят ошибки
*/
fireEventError(err) {
const me = this;
if (me.onAction.error.length === 0) {
console.error(err);
} else {
me.onAction.error.forEach(cb => {
cb(err);
});
}
}
/**
* Запуск события, когда пришло сообщение от RabbitMQ
*/
fireEventTaskConsume(task) {
const me = this;
me.onAction.consume.forEach(cb => {
cb(task);
});
}
/**
* Подсоедениться к rbmq
*/
connectToRabbitMq() {
const me = this;
amqplib.connect(me.config.rabbitmq.connectionConfig, function(err, connection) {
if (err) {
return me.fireEventError(err);
}
me.rbmqConnection = connection;
me.fireEventConnected();
me.rbmqQueuesSubscribe(() => {});
});
}
rbmqCreateChannel(queueName, cb) {
const me = this;
me.rbmqConnection.createConfirmChannel((err, channel) => {
if (err) {
return cb(err);
}
me.debug(`Create channel ${queueName}`);
me.channels[queueName] = channel;
me.channels[queueName].on('error', err => {
if (err.code === 406) {
me.rbmqCreateChannel(queueName, () => {});
} else {
me.fireEventError(err);
}
});
me.channels[queueName].on('blocked', err => {
me.fireEventError(err);
});
me.channels[queueName].on('unblocked', err => {
me.fireEventError(err);
});
me.channels[queueName].on('close', () => {
me.debug(`Close channel ${queueName}`);
// me.fireEventError(new exceptions.ErrorTransmitter('Channel close'));
});
me.channels[queueName].prefetch(me.config.prefetchCount);
me.debug(`Prefetch count on channel ${queueName}: ${me.config.prefetchCount}`);
cb(null, me.channels[queueName]);
});
}
rbmqQueuesSubscribe(cb) {
const me = this;
let countQueues = 0;
async.each(
me.processedLetters,
(name, cb) => {
countQueues++;
me.rbmqQueueSubscribe(me.config.queuePrefix + '_' + name);
cb();
},
() => {
me.debug(`Connected to ${countQueues} queues`);
cb();
},
);
}
rbmqGetChannel(queueName, cb) {
if (this.channels[queueName]) {
cb(null, this.channels[queueName]);
} else {
this.getChannelsCb.push(cb);
this.rbmqCreateChannel(queueName, (err, channel) => {
if (err) return cb(err);
cb(null, channel);
});
}
}
rbmqChannelAck(channel, task, cb) {
channel.ack(task);
cb();
}
/**
* Подписка на очередь
* @param queueName
*/
rbmqQueueSubscribe(queueName) {
const me = this;
me.rbmqGetChannel(queueName, (err, channel) => {
if (err) return me.fireEventError(err);
// Создаем очередь или подключаемся к уже существующей
channel.assertQueue(queueName, me.config.rabbitmq.queueConfig);
me.debug(`Queue ${queueName} assert`);
// Внутренняя организация стека задач
let queue = async.cargo(function(stackTasks, cb) {
me.sendStack(stackTasks, me.processResponse(stackTasks, cb));
}, me.config.prefetchCount);
// Подписуемся на очередь
channel.consume(queueName, function(task) {
if (task !== null) {
me.fireEventTaskConsume(task);
task.message = JSON.parse(task.content.toString());
task.queueName = queueName;
if (task.message.dates) task.message.dates.process = new Date();
queue.push(task);
} else {
me.fireEventError(new Error('Task is empty'));
}
});
});
}
deferSend(stackTasks, cb) {
let me = this;
setTimeout(() => {
// Пытаемся отправить еще раз и сразу увеличиваем время задержки в 2 раза
me.timeoutDeferSend = me.timeoutDeferSend * 2;
me.sendStack(stackTasks, me.processResponse(stackTasks, cb));
}, me.timeoutDeferSend);
}
/**
* Обработать ответ от вторго сервера
* @param {Object} task
*/
processResponse(stackTasks, cb) {
const me = this;
return (err, response, body) => {
if (err) {
// Так как произошел какой-то пиздец при попытке отправить задачу, кидаем в лог и фейлим ошибку
me.fireEventError(err);
// Если нет соеденения, еще раз пробуем отправить через время
if (['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED'].indexOf(err.code) != -1) {
me.deferSend(stackTasks, cb);
} else {
me.stackFail(err, stackTasks);
cb();
}
} else {
// Сбрасываем время задержки при повторной отправки сообщения
me.timeoutDeferSend = me.config.timeReconnect;
switch (response.statusCode) {
case 200:
// Обрабатываем результат выполнения задач
me.processResponseTasks(stackTasks);
cb();
break;
case 503:
me.stackFail(body, stackTasks);
cb();
break;
default:
// Пробуем еще раз переотправить задачу
me.deferSend(stackTasks, cb);
}
}
};
}
/**
* Ошибка выполнения задачи
* @param err Ошибка пришедшая от второго сервера
* @param task
*/
taskFail(err, task, cb) {
const me = this;
const obj = {
error: err,
message: task.message,
queue: task.queueName,
};
me.debug(obj);
me.fireEventError(obj);
me.rbmqGetChannel(task.queueName, (err, channel) => {
if (err) return cb(err);
// Озмечаем задачу в очереди "выполненной"
me.rbmqChannelAck(channel, task, cb);
});
}
/**
* Озмечаем задачу в очереди выполненной
* @param {Object} task
*/
taskComplete(task, cb) {
const me = this;
if (task.message.dates) task.message.dates.complete = new Date();
me.removeFromStorage(task, err => {
if (err) {
me.debug({
error: err,
message: task.message,
queueName: task.queueName,
});
}
me.fireEventTaskComplete(task);
me.rbmqGetChannel(task.queueName, (err, channel) => {
if (err) return cb(err);
// Озмечаем задачу в очереди "выполненной"
me.rbmqChannelAck(channel, task, cb);
});
});
}
/**
* Обработка ошибки в стеке задач
* @param err
* @param stackTasks
*/
stackFail(err, stackTasks) {
const me = this;
const obj = {
error: err,
stack: [],
};
// Озмечаем задачу в очереди "выполненной"
async.eachSeries(
stackTasks,
(task, cb) => {
obj.stack.push(task.message);
me.rbmqGetChannel(task.queueName, (err, channel) => {
if (err) return me.fireEventError(err);
// Озмечаем задачу в очереди "выполненной"
channel.ack(task);
cb();
});
},
err => {
if (err) {
me.fireEventError(err);
}
me.debug(obj);
me.fireEventError(obj);
},
);
}
/**
* Обработка ответа с стеком задач
* @param stackTasks
* @param body
*/
processResponseTasks(stackTasks) {
const me = this;
async.each(
stackTasks,
(task, cb) => {
me.taskComplete(task, cb);
},
err => {
if (err) {
me.fireEventError(err);
}
},
);
}
/**
* Удалить файл из хранилища
* @param task
* @param cb
*/
removeFromStorage(task, cb) {
fs.access(task.message.path.store, fs.R_OK, err => {
if (err) return cb();
fs.unlink(task.message.path.store, cb);
});
}
/**
* Отправить стек задач на другой сервер
* @param {Object} task Стек задач из очереди
* @param {Function} cb
*/
sendStack(stackTasks, cb) {
const me = this;
const form = new FormData();
const options = {
hostname: me.config.domainName,
port: me.config.port,
path: '/upload',
method: 'POST',
agent: keepAliveAgent,
timeout: 500,
};
const queueNameCnt = {};
const prepareData = [];
async.eachSeries(
stackTasks,
(task, cb) => {
if (task.message.dates) task.message.dates.send = new Date();
if (!queueNameCnt[task.queueName]) queueNameCnt[task.queueName] = 0;
queueNameCnt[task.queueName]++;
// Если команда для записи файла, то нужно передать и сам файл
if (-1 !== me.config.fileSendMethods.indexOf(task.message.command)) {
fs.stat(task.message.path.store, (err, stat) => {
// Если какой-то пиздец уже тут, то нахер эту задачу
if (err) {
me.taskFail(err, task, cb);
} else if (!stat.isDirectory()) {
let stream = fs.createReadStream(task.message.path.store);
stream.on('error', err => {
me.fireEventError(err);
});
prepareData.push(task.message);
form.append(task.message.id, stream);
cb();
} else {
cb();
}
});
} else {
// me.debug({message: task.message, queue: task.queueName});
prepareData.push(task.message);
cb();
}
},
err => {
if (err) {
me.fireEventError(err);
return cb(err);
}
me.debug(`Stack length to be send: ${stackTasks.length} | ${JSON.stringify(queueNameCnt)}`);
form.append('tasks', JSON.stringify(prepareData));
options.headers = form.getHeaders();
const request = http.request(options, response => {
let body = '';
response.setEncoding('utf8');
response.on('data', chunk => {
body += chunk;
});
response.on('end', () => {
cb(null, response, body);
});
});
request.on('error', err => {
cb(err);
});
form.pipe(request);
form.on('end', () => {
request.end();
});
},
);
}
}
module.exports = Transmitter;