promises-to-retry
Version:
Simple utility library that provides retry/reflect mechanism for a list of promises
217 lines (188 loc) • 9.72 kB
JavaScript
// node core modules
// 3rd party modules
// local modules
const { responseByMode } = require('./utils');
const delay = (t, v) => new Promise(resolve => setTimeout(resolve.bind(null, v), t));
/**
* Execute promise which is provided as a separate function in order to make a clean/original promise call.
* If there was an error, log it and return rejected promise execution so it might be used again for retrial
*
* @param {Object} [logger] Custom logger that can be provided
* @returns {function(*=): Promise<T | {rejectedPromise: *, error: any, status: string}>}
*/
const reflectFactory = (logger) =>
executePromise => {
if (typeof executePromise === 'function') {
return executePromise()
.then(data => ({ data, status: 'resolved' }))
.catch((error) => {
logger && logger.error('Reflect promise error: ', error);
return { error, status: 'rejected', rejectedPromise: executePromise };
});
}
return executePromise.then(data => ({ data, status: 'resolved' }))
.catch((error) => {
logger && logger.error('Reflect promise error: ', error);
return { error, status: 'rejected' };
});
};
/**
* This method resolves all promises without failing, logs an error if there is logger provided and displays a final status of executed promise.
* It can be "resolved" or "rejected". If "rejected", it also provides a reference to rejected function that returns a promise.
* The purpose is to continue with the execution of all promises even if some of them were rejected
*
* @param {Array} listOfPromises Array of promises, which are mapped into reflect function
* @param {Object} [logger] Custom logger that can be provided
* @returns {Promise<any[]>}
*/
const reflectAllPromises = (listOfPromises, logger) => {
const reflect = reflectFactory(logger);
return Promise.all(listOfPromises.map(reflect));
};
/**
* This method runs promises in parallel, and collects all rejected promises. Once all rejected promises are collected,
* the retry mechanism kicks-in and retries rejected promises (also in parallel) until there are no more attempts.
* If maximum retry attempts is exceeded, the method will return all rejected promises so the caller may try to
* use different strategy for resolving them.
*
* @param {Array} listOfPromises A list of functions that return a promise
* @param {Object} retryParams A configuration object, relevant for retrying mechanism
* @param {Number} retryParams.maxAttempts Maximum number of attempts by retry mechanism.
* If not provided, there will be no retries
* @param {Number} retryParams.delay Delay the method execution by certain period of time. The default value
* is 1000ms
* @param {Object} [logger] Custom logger that can be provided
* @returns {Promise<any[]>}
*/
const retryAllRejectedPromises = async (listOfPromises, retryParams, logger) => {
const allPromises = await reflectAllPromises(listOfPromises, logger);
const rejectedPromises = (allPromises || [])
.filter(singlePromise => singlePromise.status === 'rejected')
.map(({ rejectedPromise }) => rejectedPromise);
if (rejectedPromises && rejectedPromises.length) {
if (retryParams.maxAttempts > 0) {
logger && logger.debug('Trying to run [%d] rejected promise(s), attempts left %d',
rejectedPromises.length, retryParams.maxAttempts);
retryParams.maxAttempts -= 1;
await delay(retryParams.delay || 1000);
return retryAllRejectedPromises(rejectedPromises, retryParams, logger);
}
logger && logger.debug('Failed to execute [%d] promise(s)', rejectedPromises.length);
return rejectedPromises;
}
return [];
};
/**
* This method runs promises in parallel, and collects all rejected promises. Once all rejected promises are collected,
* the retry mechanism kicks-in and retries rejected promises in NEXT the event loop (also in parallel) until there are no more attempts.
* This also means that the execution will not stop, and the method will always resolve to true!
* If maximum retry attempts is exceeded, the method will log an error message about number of rejected promise executions,
* but it will NOT return rejected promises.
*
* @param {Array} listOfPromises A list of functions that return a promise
* @param {Object} retryParams A configuration object, relevant for retrying mechanism
* @param {Number} retryParams.maxAttempts Maximum number of attempts by retry mechanism.
* If not provided, there will be no retries
* @param {Number} retryParams.delay Delay the method execution by certain period of time. The default value
* is 1000ms
* @param {Object} [logger] Custom logger that can be provided
* @returns {Promise<void>}
*/
const reflectAndRetryAllRejectedPromises = async (listOfPromises, retryParams, logger) => {
const allPromises = await reflectAllPromises(listOfPromises, logger);
const rejectedPromises = (allPromises || [])
.filter(singlePromise => singlePromise.status === 'rejected')
.map(({ rejectedPromise }) => rejectedPromise);
if (rejectedPromises && rejectedPromises.length) {
if (retryParams.maxAttempts > 0) {
setTimeout(async () => {
logger && logger.debug('Trying to run [%d] rejected promise(s), attempts left %d',
rejectedPromises.length, retryParams.maxAttempts);
retryParams.maxAttempts -= 1;
await reflectAndRetryAllRejectedPromises(rejectedPromises, retryParams, logger);
}, retryParams.delay || 1000);
} else {
logger && logger.debug('Failed to execute [%d] promise(s)', rejectedPromises.length);
}
}
};
/**
* This method is batching list of promises. The batches are invoked with `reflectAllPromises`, so both resolved
* and rejected results are kept. Based on `responseMode` you can receive different data.
*
* @param {Number} maxBatchSize Number of batches to be invoked in parallel
* @param {Number} delayInMs Delay between batch execution
* @param {(
* 'ONLY_RESOLVED' | 'ONLY_REJECTED' |
* 'ALL' | 'ALL_SPLIT'
* )} responseMode Different mode will provide different responses,
* depending on caller requirements.
* @returns {Function}
*/
const batchPromises = ({ maxBatchSize = 2, delayInMs = 1000, responseMode = 'ALL' }) =>
/**
* @param {Array} promises List of promises to batch
* @returns {Promise<any[]>}
*/
async promises => {
if (!Object.keys(responseByMode).includes(responseMode)) {
throw new Error('Invalid responseMode provided');
}
let allPromises = [];
const resolvedPromises = [];
const rejectedPromises = [];
// prepare the batches
const promiseBatches = promises.reduce((result, singlePromise) => {
if (result[result.length - 1].length < maxBatchSize) {
result[result.length - 1].push(singlePromise);
return result;
}
result[result.length] = [singlePromise];
return result;
}, [[]]);
for (const promiseBatch of promiseBatches) {
const executedPromises = await reflectAllPromises(promiseBatch);
executedPromises.forEach(({ status, data, error }) =>
status === 'rejected'
? rejectedPromises.push(error)
: resolvedPromises.push(data));
allPromises.push(...executedPromises);
await delay(delayInMs);
}
allPromises = allPromises.map(({ data, error }) => data || error);
return responseByMode[responseMode]({ resolvedPromises, rejectedPromises, allPromises });
};
/**
* For provided list of promises and `raceTimeoutInMs` method will execute promises in parallel and wait for the
* response for certain amount of time. After time is out (based on `raceTimeoutInMs`) caller may decide what response
* to receive. By default method returns only promises that "won" the timeout race. Otherwise it can return all
* reflected promises including promises that "lost" the race.
*
* @param {Array} listOfPromises A list of promises to race against time
* @param {Number} [raceTimeoutInMs] The time in milliseconds that each promise will race against
* @param {String} [raceTimeoutMessage] A custom message provided for promise which "lost" the race
* @param {(
* 'ONLY_RESOLVED' | 'ONLY_WINNER_PROMISES' | 'ALL'
* )} responseMode Different mode will provide different responses,
* depending on caller requirements.
* @returns {Promise<any[]>}
*/
const racePromisesWithTime = async ({ listOfPromises, raceTimeoutInMs = 1000, raceTimeoutMessage = 'Promise timeout limit reached', responseMode = 'ONLY_WINNER_PROMISES' }) => {
const allPromises = await reflectAllPromises(listOfPromises.map(promise =>
Promise.race([promise, delay(raceTimeoutInMs, { timeoutPromise: promise, raceTimeoutMessage })]))
);
const resolvedPromises = allPromises
.map(({ data }) => data)
.filter(Boolean);
const winnerPromises = resolvedPromises
.filter(data => data && !data.timeoutPromise);
return responseByMode[responseMode]({ resolvedPromises, winnerPromises, allPromises });
};
module.exports = {
reflectAllPromises,
retryAllRejectedPromises,
reflectAndRetryAllRejectedPromises,
batchPromises,
racePromisesWithTime,
delay
};