UNPKG

google-cloud-tasks

Version:

Nodejs package to push tasks to Google Cloud Tasks (beta). Include pushing batches.

174 lines (157 loc) 8 kB
/** * Copyright (c) 2018, Neap Pty Ltd. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ const { obj: { merge }, math, identity } = require('./core') const { arities } = require('./functional') /** * Create an empty promise that returns after a certain delay * @param {Number|[Number]} timeout If array, it must contain 2 numbers representing an interval used to select a random number * @return {[type]} [description] */ const delay = timeout => Promise.resolve(null).then(() => { let t = timeout || 100 if (Array.isArray(timeout)) { if (timeout.length != 2) throw new Error('Wrong argument exception. When \'timeout\' is an array, it must contain exactly 2 number items.') const start = timeout[0] * 1 const end = timeout[1] * 1 if (isNaN(start)) throw new Error(`Wrong argument exception. The first item of the 'timeout' array is not a number (current: ${timeout[0]})`) if (isNaN(end)) throw new Error(`Wrong argument exception. The second item of the 'timeout' array is not a number (current: ${timeout[1]})`) if (start > end) throw new Error(`Wrong argument exception. The first number of the 'timeout' array must be strictly smaller than the second number (current: [${timeout[0]}, ${timeout[1]}])`) t = math.randomNumber(start, end) } return new Promise(onSuccess => setTimeout(onSuccess, t)) }) const wait = (stopWaiting, options) => Promise.resolve(null).then(() => { const now = Date.now() const { timeout=300000, start=now, interval=2000 } = options || {} if ((now - start) > timeout) throw new Error('timeout') return Promise.resolve(null).then(() => stopWaiting()).then(stop => { if (stop) return else return delay(interval).then(() => wait(stopWaiting, { timeout, start, interval })) }) }) const check = (request, verify, options={}) => request(options.nextState).then(resp => Promise.resolve(verify(resp)).then(result => { const { interval=4000, timeOut=300000 } = options if (result === true) return resp else if (timeOut < 0) throw new Error('timeout') else if (!result || result.nextState) return delay(interval).then(() => check(request, verify, { interval, timeOut: timeOut - interval, nextState: result.nextState })) else return resp })) /** * [description] * @param {Function} fn [description] * @param {Function} successFn (res, options) => Returns a promise or a value. The value is a boolean or an object that determines * whether a response is valid or not. If the value is an object, that object might contain * a 'retryInterval' which overrides the optional value. * @param {Function} failureFn (Optional) (error, options) => Returns a promise or a value. The value is a boolean or an object that determines * whether a response is valid or not. If the value is an object, that object might contain * a 'retryInterval' which overrides the optional value. * @param {Number} options.retryAttempts default: 5. Number of retry * @param {Number} options.attemptsCount Current retry count. When that counter reaches the 'retryAttempts', the function stops. * @param {Number} options.timeOut If specified, 'retryAttempts' and 'attemptsCount' are ignored * @param {Number} options.retryInterval default: 5000. Time interval in milliseconds between each retry. It can also be a 2 items array. * In that case, the retryInterval is a random number between the 2 ranges (e.g., [10, 100] => 54). * The retry strategy increases the 'retryInterval' by a factor 1.5 after each failed attempt. * @param {Boolean} options.ignoreError In case of constant failure to pass the 'successFn' test, this function will either throw an error * or return the current result without throwing an error if this flag is set to true. * @param {String} options.errorMsg Customize the exception message in case of failure. * @param {String} options.ignoreFailure If set to true, then failure from fn will cause a retry * @return {[type]} [description] */ const retry = arities( 'function fn, function successFn, object options={}', 'function fn, function successFn, function failureFn, object options={}', ({ fn, successFn, failureFn, options={} }) => { const start = Date.now() return Promise.resolve(null) .then(() => fn()).then(data => ({ error: null, data })) .catch(error => { if (options.ignoreFailure && !failureFn) failureFn = () => true return { error, data: null } }) .then(({ error, data }) => Promise.resolve(null) .then(() => { if (error && failureFn) return failureFn(error, options) else if (error) throw error else return successFn(data, options) }) .then(passed => { if (!error && passed) return data else if ((!error && !passed) || (error && passed)) { let { retryAttempts=5, retryInterval=5000, attemptsCount=0, timeOut=null, startTime=null } = options const delayFactor = (attemptsCount+1) <= 1 ? 1 : Math.pow(1.5, attemptsCount) if (timeOut > 0) { startTime = startTime || start if (Date.now() - startTime < timeOut) { const explicitRetryInterval = passed && passed.retryInterval > 0 ? passed.retryInterval : null const i = (!explicitRetryInterval && Array.isArray(retryInterval) && retryInterval.length > 1) ? (() => { if (typeof(retryInterval[0]) != 'number' || typeof(retryInterval[1]) != 'number') throw new Error(`Wrong argument exception. When 'options.retryInterval' is an array, all elements must be numbers. Current: [${retryInterval.join(', ')}].`) if (retryInterval[0] > retryInterval[1]) throw new Error(`Wrong argument exception. When 'options.retryInterval' is an array, the first element must be strictly greater than the second. Current: [${retryInterval.join(', ')}].`) return math.randomNumber(retryInterval[0], retryInterval[1]) })() : (explicitRetryInterval || retryInterval) const delayMs = Math.round(delayFactor*i) return delay(delayMs).then(() => failureFn ? retry(fn, successFn, failureFn, merge(options, { startTime, attemptsCount:attemptsCount+1 })) : retry(fn, successFn, merge(options, { startTime, attemptsCount:attemptsCount+1 }))) } else throw new Error('timeout') } else if (attemptsCount < retryAttempts) { const delayMs = Math.round(delayFactor*retryInterval) return delay(delayMs).then(() => failureFn ? retry(fn, successFn, failureFn, merge(options, { attemptsCount:attemptsCount+1 })) : retry(fn, successFn, merge(options, { attemptsCount:attemptsCount+1 }))) } else if (options.ignoreError) return data else throw new Error(options.errorMsg ? options.errorMsg : `${retryAttempts} attempts to retry the procedure failed to pass the test`) } else throw error })) }) /** * Makes a promise throw an error if it times our * @param {Promise} p Original promise * @param {Number} timeOut Optional. Default is 30,000 milliseconds * @return {Promise} [description] */ const addTimeout = (p, timeOut=30000) => { const timeoutMsg = `timout_${identity.new()}` const timeoutTask = new Promise(onSuccess => setTimeout(() => onSuccess(timeoutMsg), timeOut)) return Promise.race([timeoutTask, p]) .then(res => { if (res == timeoutMsg) throw new Error('timeout') return res }) } module.exports = { delay, wait, check, retry, addTimeout }