Skip to content

Commit

Permalink
tests for retry-busy behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacs committed Jan 10, 2023
1 parent 188e3ed commit 52f9370
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 6 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ Options:
- `backoff`: Windows only. Rate of exponential backoff for async
removal in case of `EBUSY`, `EMFILE`, and `ENFILE` errors.
Should be a number greater than 1. Default `1.2`
- `maxBackoff`: Windows only. Maximum backoff time in ms to
- `maxBackoff`: Windows only. Maximum total backoff time in ms to
attempt asynchronous retries in case of `EBUSY`, `EMFILE`, and
`ENFILE` errors. Default `100`
`ENFILE` errors. Default `200`. With the default `1.2` backoff
rate, this results in 14 retries, with the final retry being
delayed 33ms.

Any other options are provided to the native Node.js `fs.rm` implementation
when that is used.
Expand Down
15 changes: 11 additions & 4 deletions lib/retry-busy.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const MAXBACKOFF = 100
// note: max backoff is the maximum that any *single* backoff will do
//
const MAXBACKOFF = 200
const RATE = 1.2
const MAXRETRIES = 10

const codes = new Set(['EMFILE', 'ENFILE', 'EBUSY'])
const retryBusy = fn => {
const method = async (path, opt, backoff = 1) => {
const method = async (path, opt, backoff = 1, total = 0) => {
const mbo = opt.maxBackoff || MAXBACKOFF
const rate = opt.backoff || RATE
const max = opt.retries || MAXRETRIES
Expand All @@ -15,10 +17,11 @@ const retryBusy = fn => {
} catch (er) {
if (codes.has(er.code)) {
backoff = Math.ceil(backoff * rate)
if (backoff < mbo) {
total = backoff + total
if (total < mbo) {
return new Promise((res, rej) => {
setTimeout(() => {
method(path, opt, backoff).then(res, rej)
method(path, opt, backoff, total).then(res, rej)
}, backoff)
})
}
Expand Down Expand Up @@ -56,6 +59,10 @@ const retryBusySync = fn => {
}

module.exports = {
MAXBACKOFF,
RATE,
MAXRETRIES,
codes,
retryBusy,
retryBusySync,
}
19 changes: 19 additions & 0 deletions tap-snapshots/test/retry-busy.js.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* IMPORTANT
* This snapshot file is auto-generated, but designed for humans.
* It should be checked into source control and tracked carefully.
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
* Make sure to inspect the output below. Do not ignore changes!
*/
'use strict'
exports[`test/retry-busy.js TAP > default settings 1`] = `
Object {
"codes": Set {
"EMFILE",
"ENFILE",
"EBUSY",
},
"MAXBACKOFF": 200,
"MAXRETRIES": 10,
"RATE": 1.2,
}
`
110 changes: 110 additions & 0 deletions test/retry-busy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const {
retryBusy,
retryBusySync,
MAXBACKOFF,
RATE,
MAXRETRIES,
codes,
} = require('../lib/retry-busy.js')

const t = require('tap')

t.matchSnapshot(
{
MAXBACKOFF,
RATE,
MAXRETRIES,
codes,
},
'default settings'
)

t.test('basic working operation when no errors happen', async t => {
let calls = 0
const arg = {}
const opt = {}
const method = (a, b) => {
t.equal(a, arg, 'got first argument')
t.equal(b, undefined, 'did not get another argument')
calls++
}
const rBS = retryBusySync(method)
rBS(arg, opt)
t.equal(calls, 1)
const rB = retryBusy(method)
await rB(arg, opt).then(() => t.equal(calls, 2))
})

t.test('retry when known error code thrown', t => {
t.plan(codes.size)

for (const code of codes) {
t.test(code, async t => {
let thrown = false
let calls = 0
const arg = {}
const opt = {}
const method = (a, b) => {
t.equal(a, arg, 'got first argument')
t.equal(b, undefined, 'did not get another argument')
if (!thrown) {
thrown = true
t.equal(calls, 0, 'first call')
calls++
throw Object.assign(new Error(code), { code })
} else {
t.equal(calls, 1, 'second call')
calls++
thrown = false
}
}
const rBS = retryBusySync(method)
rBS(arg, opt)
t.equal(calls, 2)
calls = 0
const rB = retryBusy(method)
await rB(arg, opt).then(() => t.equal(calls, 2))
})
}
})

t.test('retry and eventually give up', t => {
t.plan(codes.size)
const opt = {
maxBackoff: 2,
retries: 2,
}

for (const code of codes) {
t.test(code, async t => {
let calls = 0
const arg = {}
const method = (a, b) => {
t.equal(a, arg, 'got first argument')
t.equal(b, undefined, 'did not get another argument')
calls++
throw Object.assign(new Error(code), { code })
}
const rBS = retryBusySync(method)
t.throws(() => rBS(arg, opt), { code })
t.equal(calls, 3)
calls = 0
const rB = retryBusy(method)
await t.rejects(rB(arg, opt)).then(() => t.equal(calls, 3))
})
}
})

t.test('throw unknown error gives up right away', async t => {
const arg = {}
const opt = {}
const method = (a, b) => {
t.equal(a, arg, 'got first argument')
t.equal(b, undefined, 'did not get another argument')
throw Object.assign(new Error('nope'), { code: 'nope' })
}
const rBS = retryBusySync(method)
t.throws(() => rBS(arg, opt), { code: 'nope' })
const rB = retryBusy(method)
await t.rejects(rB(arg, opt), { code: 'nope' })
})

0 comments on commit 52f9370

Please sign in to comment.