UNPKG

sqmicro-commons

Version:

Commons for SQ analytics microservices.

152 lines (133 loc) 4.95 kB
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.`; }