Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 30 additions & 36 deletions src/retrier.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const { isNumber } = require('./utilities')

const createLinear = function (constants = {}) {
const { m = 1, b = 0 } = constants

Expand Down Expand Up @@ -27,49 +25,45 @@ const createExponential = function (constants = {}, m = 1) {
module.exports.createExponential = createExponential

/**
* @param {Function}
* @param {Function|Number}
* @param {Number}
* @param {Function}
* Wraps an `async` function for async retries as per the logic given in `curve`.
* Curve is expected to be a callback which will be called at each failed
* iteration with `err` (the error thrown by the wrapped function) and `count`
* which is the count of total attempts to `resolve` - it should either throw
* (should no more retries be required) or return an integer dictating the
* number of milliseconds to sleep before re-attempting resolution.
*
* @param {Function} fn
* @param {Function|Object} curve
* @returns {Function}
*/
const createRetrierFn = function (fn, curve = 2, limit = 2, shouldRetry = undefined) {
if (isNumber(curve)) {
limit = curve
curve = zero
const createRetrierFn = function (fn, curve) {
if (!curve) {
curve = function (err, count) {
if (count > 1) throw err
return 0
}
}

return function () {
const args = Array.prototype.slice.call(arguments)

return new Promise(function (resolve, reject) {
(function recurse(attempt) {
function retry(error) {
const errorCount = attempt + 1

if (limit && errorCount >= limit) return reject(error)
return function (...args) {
let attempt = 0

if (shouldRetry && !shouldRetry(error)) return reject(error)
const recurse = function (resolve, reject) {
return fn(...args).then(resolve).catch(function (err) {
let timeout

return recurse(errorCount)
try {
timeout = curve(err, ++attempt)
if (timeout <= 0) return recurse(resolve, reject)
return setTimeout(recurse, timeout, resolve, reject)
} catch (_err) {
return reject(_err)
}
})
}

setTimeout(function () {
try {
Promise
.resolve(fn.apply(null, args))
.then(resolve)
.catch(function (asyncErr) {
return retry(asyncErr)
})
} catch (syncErr) {
retry(syncErr)
}
}, curve(attempt))
}(null, 0))
})
return new Promise(recurse)
}
}

module.exports.retry = createRetrierFn
module.exports.createRetrierFn = createRetrierFn
module.exports.retry = createRetrierFn
158 changes: 24 additions & 134 deletions test/retrier.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
const test = require('ava')
const {
createLinear,
createExponential,
createRetrierFn,
} = require('../src/retrier')
const { createRetrierFn } = require('../src/retrier')

test('createRetrierFn - retries async errors with delay', async function (t) {
const sleep = x => new Promise(r => setTimeout(r, x))
const sleep = ms => new Promise(r => setTimeout(r, ms))

/**
* Given 'i', create function that will return a promise
Expand All @@ -22,146 +18,40 @@ test('createRetrierFn - retries async errors with delay', async function (t) {
i--
throw new Error(i)
}
return true
}
}

// Will retry three times.
const succeeder = createRetrierFn(createFailer(2), () => 15, 3)
t.true(typeof succeeder === 'function')

const start = new Date().getTime()
const success = await succeeder()
t.true(success)
t.true(new Date().getTime() - start >= 30)

const failer = createRetrierFn(createFailer(4), 2)
t.true(typeof succeeder === 'function')

let failure
try {
failure = await failer()
} catch ({ message }) {
failure = +message // Coerce.
return i
}
}

t.true(failure === 2)
})

test('createRetrierFn - retries sync errors', async function (t) {
const responses = [
new Error('fail'),
true,
]
// NOTE: This serves as an example of a curve function.
const createCurve = function (limit, ms) {
const curve = function (err, count) {
if (count > limit) throw err
return ms
}

function failOnce() {
const response = responses.shift()
if (response instanceof Error) throw response
return response
return curve
}

const fn = createRetrierFn(failOnce, 2)
t.true(await fn())
})

test('createRetrierFn - allows opt out of retries', async function (t) {
const shouldRetry = () => false
const responses = [
new Error('fail'),
true,
]
const succeeder = createRetrierFn(createFailer(2), createCurve(3, 200))
t.true(typeof succeeder === 'function')

function failOnce() {
const response = responses.shift()
if (response instanceof Error) throw response
return response
}
const ts = new Date().getTime()

const fn = createRetrierFn(failOnce, 2, undefined, shouldRetry)
try {
await fn()
t.fail()
} catch (error) {
t.pass()
}
})
const success = await succeeder()
t.true(success === 0)
t.true((new Date().getTime() - ts) >= 400)

test('createRetrierFn - allows indefinite retries', async function (t) {
const responses = [
new Error('fail'),
new Error('fail'),
new Error('fail'),
new Error('fail'),
true,
]
const failer = createRetrierFn(createFailer(5), createCurve(2, 200))
t.true(typeof failer === 'function')

function failMany() {
const response = responses.shift()
if (response instanceof Error) throw response
return response
}
let value

const fn = createRetrierFn(failMany, 0, 0)
try {
await fn()
t.pass()
} catch (error) {
t.fail()
}
})

test('createRetrierFn - supports curves', async function (t) {
const sleep = x => new Promise(r => setTimeout(r, x))

const cache = []
const createFn = function () {
let failed = false

const fn = async function (value) {
await sleep(100)
cache.push(value)

if (!failed) {
failed = true
throw new Error('')
}

return value
}

return fn
}

let f = createFn()
let fn = createRetrierFn(f, 2)

t.true(await fn('x') === 'x')
t.true(cache.length === 2)

cache.length = 0
f = createFn()
fn = createRetrierFn(f, x => x, 2)

t.true(await fn('x') === 'x')
t.true(cache.length === 2)
})

test('createRetrierFn - and inbuilt curves', async function (t) {
const range = function* (len) {
let count = 0
while (count < len) {
yield count
count++
}
await failer()
} catch ({ message }) {
value = +message // Coerce.
}

let fx = createLinear({ m: 2, b: 0 })
t.deepEqual([...range(4)].map(fx), [0, 2, 4, 6])

fx = createExponential({ a: 2, b: 1 }, 1)
t.deepEqual([...range(6)].map(fx), [1, 2, 4, 8, 16, 32])

fx = createExponential({ a: 2, b: 1 }, 1000)
t.deepEqual([...range(6)].map(fx), [1000, 2000, 4000, 8000, 16000, 32000])
t.true(value === 2)
})