sqmicro-commons
Version:
Commons for SQ analytics microservices.
152 lines (133 loc) • 4.95 kB
JavaScript
const { promisify } = require('util');
const wait = promisify(setTimeout);
const Private = Symbol('Private');
/**
* Повторные попытки выполнения асинхронного вызова.
*/
module.exports = class Retrier {
/**
* Нужны ли дополнительные попытки выполнить асинхронный вызов?
* @returns {boolean} false - либо ещё не пытаемся, либо превышен лимит
* количества попыток.
*/
get doMoreAttempts() {
const pvt = this[Private];
return pvt.isTrying && (
pvt.maxAttempts === 0 ||
pvt.attemptCount < pvt.maxAttempts
);
}
/**
* Создать экземпляр класса повторных попыток.
* @param {number} [delay=5000] Задержка перед следующей попыткой в миллисекундах.
* @param {number} [maxAttempts=0] Максимальное число попыток. 0 - не ограничено.
* @param {Logger} [log] Логгер, если нужно логировать попытки.
*/
constructor(delay = 5000, maxAttempts = 0, log) {
const pvt = this[Private] = {
delay,
maxAttempts,
log: log || require('./log')
};
clean(pvt);
}
/**
* Выполнить асинхронный вызов, при неудаче - повторить согласно настройкам.
* @param {function} action Асинхронный вызов, возвращает промис.
* @param {string} description Описание вызова для логирования.
* @returns {Promise} Результат вызова, либо последняя ошибка.
*/
async retry(action, description = 'perform action') {
const pvt = this[Private];
if (pvt.isTrying) {
pvt.log.error(`Aready trying to ${description}`);
throw Error(`Aready trying to ${description}`);
};
pvt.isTrying = true;
let result;
while(this.doMoreAttempts) {
result = await perform(pvt, action, description);
}
return throwOrReturn(pvt, result);
}
}
/**
* Выполнить асинхронный вызов. При повторной попытке происходит задержка,
* согласно конфигурации.
* @inner
* @param {object} pvt Приватное состояние экземпляра.
* @param {function} action Асинхронный вызов.
* @param {string} description Описание для логирования.
*/
async function perform(pvt, action, description) {
await waitBeforeRetry(pvt, description);
try {
result = await action();
pvt.log.info(`Success: ${description}`);
clean(pvt);
}
catch(error) {
result = null;
error.stack;
pvt.log.error(`Failure: ${description}`, error);
failure(pvt, error);
}
return result;
}
/**
* Выполнить задержку для повторной попытки выполнить асинхронный вызов.
* @inner
* @param {object} pvt Приватное состояние экземпляра.
* @param {string} description Описание.
* @returns {Promise}
*/
async function waitBeforeRetry(pvt, description) {
if (pvt.attemptCount === 0) {
return;
}
pvt.log.info(`Retry to ${description} in ${formatDelay(pvt.delay)}`);
await wait(pvt.delay);
}
/**
* Очистить приватное состояние.
* @inner
* @param {object} pvt
*/
function clean(pvt) {
pvt.isTrying = false;
pvt.attemptCount = 0;
pvt.lastError = null;
}
/**
* Записать ошибку очередной попытки в приватоное состояние экземпляра.
* @inner
* @param {object} pvt
* @param {Error} error
*/
function failure(pvt, error) {
pvt.attemptCount++;
pvt.lastError = error;
}
/**
* Вернуть результат асинхронного вызова или вызвать исключительную ситуацию, в
* зависимости от результата всех попыток вызова.
* @inner
* @param {object} pvt
* @param {*} result
*/
function throwOrReturn(pvt, result) {
const error = pvt.lastError;
clean(pvt);
if (error) {
throw error;
}
return result;
}
/**
* Отформатировать время задержки для логирования.
* @param {number} delay Задержка в миллисекундах.
* @returns {string} отформатированное время.
*/
function formatDelay(delay) {
return `${(delay / 1000).toFixed()} sec.`;
}