288 lines
16 KiB
JavaScript
288 lines
16 KiB
JavaScript
/*
|
||
Сервис интеграции ПП Парус 8 с WEB API
|
||
Модуль ядра: контроль доступности сервисов
|
||
*/
|
||
|
||
//------------------------------
|
||
// Подключение внешних библиотек
|
||
//------------------------------
|
||
|
||
const _ = require("lodash"); //Работа с массивами и коллекциями
|
||
const rqp = require("request-promise"); //Работа с HTTP/HTTPS запросами
|
||
const EventEmitter = require("events"); //Обработчик пользовательских событий
|
||
const nodemailer = require("nodemailer"); //Отправка E-Mail сообщений
|
||
const { ServerError } = require("./server_errors"); //Типовая ошибка
|
||
const { SERR_SERVICE_UNAVAILABLE, SERR_MAIL_FAILED, SERR_OBJECT_BAD_INTERFACE } = require("./constants"); //Общесистемные константы
|
||
const { makeErrorText, validateObject } = require("./utils"); //Вспомогательные функции
|
||
const prmsServiceAvailableControllerSchema = require("../models/prms_service_available_controller"); //Схемы валидации параметров функций класса
|
||
const objServiceSchema = require("../models/obj_service"); //Схемы валидации сервисов
|
||
|
||
//--------------------------
|
||
// Глобальные идентификаторы
|
||
//--------------------------
|
||
|
||
//Типовые события
|
||
const SEVT_SERVICE_AVAILABLE_CONTROLLER_STARTED = "SERVICE_AVAILABLE_CONTROLLER_STARTED"; //Контроллер доступности сервисов запущен
|
||
const SEVT_SERVICE_AVAILABLE_CONTROLLER_STOPPED = "SERVICE_AVAILABLE_CONTROLLER_STOPPED"; //Контроллер доступности сервисов остановлен
|
||
|
||
//Время отложенного старта цикла контроля (мс)
|
||
const NDETECTING_LOOP_DELAY = 3000;
|
||
|
||
//Интервал проверки доступности сервисов (мс)
|
||
const NDETECTING_LOOP_INTERVAL = 60000;
|
||
|
||
//------------
|
||
// Тело модуля
|
||
//------------
|
||
|
||
//Класс контроллера доступности сервисов
|
||
class ServiceAvailableController extends EventEmitter {
|
||
//Конструктор класса
|
||
constructor(prms) {
|
||
//Создадим экземпляр родительского класса
|
||
super();
|
||
//Проверяем структуру переданного объекта для подключения
|
||
let sCheckResult = validateObject(
|
||
prms,
|
||
prmsServiceAvailableControllerSchema.ServiceAvailableController,
|
||
"Параметры конструктора класса ServiceAvailableController"
|
||
);
|
||
//Если структура объекта в норме
|
||
if (!sCheckResult) {
|
||
//Список обслуживаемых сервисов
|
||
this.services = null;
|
||
//Признак функционирования контроллера
|
||
this.bWorking = false;
|
||
//Признак необходимости оповещения об останове
|
||
this.bNotifyStopped = false;
|
||
//Флаг работы цикла проверки
|
||
this.bInDetectingLoop = false;
|
||
//Идентификатор таймера проверки доступности сервисов
|
||
this.nDetectingLoopTimeOut = null;
|
||
//Запомним параметры отправки E-Mail
|
||
this.mail = prms.mail;
|
||
//Запомним логгер
|
||
this.logger = prms.logger;
|
||
//Привяжем методы к указателю на себя для использования в обработчиках событий
|
||
this.serviceDetectingLoop = this.serviceDetectingLoop.bind(this);
|
||
} else {
|
||
throw new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult);
|
||
}
|
||
}
|
||
//Уведомление об запуске контроллера
|
||
notifyStarted() {
|
||
//Оповестим подписчиков о запуске
|
||
this.emit(SEVT_SERVICE_AVAILABLE_CONTROLLER_STARTED);
|
||
}
|
||
//Уведомление об остановке контроллера
|
||
notifyStopped() {
|
||
//Оповестим подписчиков об останове
|
||
this.emit(SEVT_SERVICE_AVAILABLE_CONTROLLER_STOPPED);
|
||
}
|
||
//Отправка E-Mail уведомления о недоступности сервиса
|
||
sendUnAvailableMail(prms) {
|
||
return new Promise((resolve, reject) => {
|
||
//Проверяем структуру переданного объекта для старта
|
||
let sCheckResult = validateObject(
|
||
prms,
|
||
prmsServiceAvailableControllerSchema.sendUnAvailableMail,
|
||
"Параметры функции отправки E-Mail уведомления о недоступности удалённого сервиса"
|
||
);
|
||
//Если структура объекта в норме
|
||
if (!sCheckResult) {
|
||
//Параметры подключения к SMTP-серверу
|
||
let transporter = nodemailer.createTransport({
|
||
host: this.mail.sHost,
|
||
port: this.mail.nPort,
|
||
secure: this.mail.nPort == 465,
|
||
auth: {
|
||
user: this.mail.sUser,
|
||
pass: this.mail.sPass
|
||
}
|
||
});
|
||
//Параметры отправляемого сообщения
|
||
let mailOptions = {
|
||
from: this.mail.sFrom,
|
||
to: prms.sTo,
|
||
subject: prms.sSubject,
|
||
text: prms.sMessage
|
||
};
|
||
//Отправляем сообщение
|
||
transporter.sendMail(mailOptions, (error, info) => {
|
||
if (error) {
|
||
reject(new ServerError(SERR_MAIL_FAILED, `${error.code}: ${error.response}`));
|
||
} else {
|
||
if (info.rejected && Array.isArray(info.rejected) && info.rejected.length > 0) {
|
||
reject(
|
||
new ServerError(
|
||
SERR_MAIL_FAILED,
|
||
`Сообщение не доствлено адресатам: ${info.rejected.join(", ")}`
|
||
)
|
||
);
|
||
} else {
|
||
resolve(info);
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
reject(new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult));
|
||
}
|
||
});
|
||
}
|
||
//Перезапуск опроса списка сервисов
|
||
async restartDetectingLoop() {
|
||
//Включаем опрос сервисов только если установлен флаг работы
|
||
if (this.bWorking) {
|
||
this.nDetectingLoopTimeOut = await setTimeout(async () => {
|
||
await this.serviceDetectingLoop();
|
||
}, NDETECTING_LOOP_INTERVAL);
|
||
} else {
|
||
//Если мы не работаем и просили оповстить об останове (видимо была команда на останов) - сделаем это
|
||
if (this.bNotifyStopped) this.notifyStopped();
|
||
}
|
||
}
|
||
//Опрос очереди исходящих сообщений
|
||
async serviceDetectingLoop() {
|
||
//Если есть сервисы для опроса
|
||
if (this.services && Array.isArray(this.services) && this.services.length > 0) {
|
||
//Выставим флаг - цикл опроса кативен
|
||
this.bInDetectingLoop = true;
|
||
try {
|
||
//Обходим список сервисов для проверки
|
||
for (let i = 0; i < this.services.length; i++) {
|
||
//Если сервис надо проверять на доступность и это сервис для отправки исходящих сообщений
|
||
if (
|
||
this.services[i].nUnavlblNtfSign == objServiceSchema.NUNAVLBL_NTF_SIGN_YES &&
|
||
this.services[i].nSrvType == objServiceSchema.NSRV_TYPE_SEND
|
||
) {
|
||
try {
|
||
//Отправляем проверочный запрос
|
||
await rqp({ url: this.services[i].sSrvRoot });
|
||
//Запрос прошел - фиксируем дату доступности и сбрасываем дату недоступности
|
||
this.services[i].dAvailable = new Date();
|
||
this.services[i].dUnAvailable = null;
|
||
} catch (e) {
|
||
//Зафиксируем дату и время недоступности
|
||
this.services[i].dUnAvailable = new Date();
|
||
//Сформируем текст ошибки в зависимости от того, что случилось
|
||
let sError = "Неожиданная ошибка удалённого сервиса";
|
||
if (e.error) {
|
||
sError = `Ошибка передачи данных: ${e.error.code}`;
|
||
}
|
||
if (e.response) {
|
||
sError = `Ошибка работы удалённого сервиса: ${e.response.statusCode} - ${
|
||
e.response.statusMessage
|
||
}`;
|
||
}
|
||
//Фиксируем ошибку проверки в протоколе
|
||
await this.logger.warn(
|
||
`При проверке доступности сервиса ${this.services[i].sCode}: ${makeErrorText(
|
||
new ServerError(SERR_SERVICE_UNAVAILABLE, sError)
|
||
)} (адрес - ${this.services[i].sSrvRoot})`,
|
||
{ nServiceId: this.services[i].nId }
|
||
);
|
||
}
|
||
//Если есть даты - будем проверять
|
||
if (this.services[i].dUnAvailable && this.services[i].dAvailable) {
|
||
//Выясним как долго он уже недоступен (в минутах)
|
||
let nDiffMs = this.services[i].dUnAvailable - this.services[i].dAvailable;
|
||
let nDiffMins = Math.round(((nDiffMs % 86400000) % 3600000) / 60000);
|
||
//Если простой больше указанного в настройках - будем оповещать по почте
|
||
if (nDiffMins >= this.services[i].nUnavlblNtfTime) {
|
||
let sSubject = `Удалённый сервис ${this.services[i].sCode} неотвечает на запросы`;
|
||
let sMessage = `Сервис недоступен более ${
|
||
this.services[i].nUnavlblNtfTime
|
||
} мин. (${nDiffMins} мин. с момента запуска сервера приложений).\nАдрес сервиса: ${
|
||
this.services[i].sSrvRoot
|
||
}`;
|
||
await this.logger.error(sMessage, { nServiceId: this.services[i].nId });
|
||
try {
|
||
await this.sendUnAvailableMail({
|
||
sTo: this.services[i].sUnavlblNtfMail,
|
||
sSubject,
|
||
sMessage
|
||
});
|
||
} catch (e) {
|
||
await this.logger.error(makeErrorText(e), {
|
||
nServiceId: this.services[i].nId
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
//Фиксируем ошибку в протоколе работы сервера приложений
|
||
await this.logger.error(makeErrorText(e));
|
||
}
|
||
//Выставим флаг - цикл опроса неактивен
|
||
this.bInDetectingLoop = false;
|
||
//Перезапускаем опрос
|
||
await this.restartDetectingLoop();
|
||
} else {
|
||
//Выставим флаг - цикл опроса неактивен
|
||
this.bInDetectingLoop = false;
|
||
//Опрашивать нечего - ждём и перезапускаем цикл опроса
|
||
await this.restartDetectingLoop();
|
||
}
|
||
}
|
||
//Запуск контроллера
|
||
startController(prms) {
|
||
//Проверяем структуру переданного объекта для старта
|
||
let sCheckResult = validateObject(
|
||
prms,
|
||
prmsServiceAvailableControllerSchema.startController,
|
||
"Параметры функции запуска контроллера доступности сервисов"
|
||
);
|
||
//Если структура объекта в норме
|
||
if (!sCheckResult) {
|
||
//Выставляем флаг работы
|
||
this.bWorking = true;
|
||
//Выставляем флаг необходимости оповещения об останове
|
||
this.bNotifyStopped = false;
|
||
//Выставляем флаг неактивности (пока) цикла опроса
|
||
this.bInDetectingLoop = false;
|
||
//запоминаем список обслуживаемых сервисов и инициализируем даты доступности и недоступности
|
||
this.services = _.cloneDeep(prms.services);
|
||
this.services.forEach(s => {
|
||
s.dUnAvailable = null;
|
||
s.dAvailable = new Date();
|
||
});
|
||
//Начинаем слушать очередь исходящих
|
||
setTimeout(this.serviceDetectingLoop, NDETECTING_LOOP_DELAY);
|
||
//И оповещаем всех что запустились
|
||
this.notifyStarted();
|
||
} else {
|
||
throw new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult);
|
||
}
|
||
}
|
||
//Остановка контроллера
|
||
stopController() {
|
||
//Выставляем флаг неработы
|
||
this.bWorking = false;
|
||
//Если сейчас мы не в цикле проверки
|
||
if (!this.bInDetectingLoop) {
|
||
//Сбросим его таймер, чтобы он не запустился снова
|
||
if (this.nDetectingLoopTimeOut) {
|
||
clearTimeout(this.nDetectingLoopTimeOut);
|
||
this.nDetectingLoopTimeOut = null;
|
||
}
|
||
//Выставляем флаг - не надо оповещать об останове
|
||
this.bNotifyStopped = false;
|
||
//Оповестим об останове
|
||
this.notifyStopped();
|
||
} else {
|
||
//Выставляем флаг необходимости оповещения об останове (это будет сделано автоматически по завершению цикла проверки)
|
||
this.bNotifyStopped = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
//-----------------
|
||
// Интерфейс модуля
|
||
//-----------------
|
||
|
||
exports.SEVT_SERVICE_AVAILABLE_CONTROLLER_STARTED = SEVT_SERVICE_AVAILABLE_CONTROLLER_STARTED;
|
||
exports.SEVT_SERVICE_AVAILABLE_CONTROLLER_STOPPED = SEVT_SERVICE_AVAILABLE_CONTROLLER_STOPPED;
|
||
exports.ServiceAvailableController = ServiceAvailableController;
|