Commit 82cd414

Anton Medvedev <anton@medv.io>
2022-06-11 00:12:33
Add expBackoff()
1 parent b684d0c
Changed files (2)
src/experimental.ts
@@ -20,34 +20,44 @@ import { Duration, parseDuration } from './util.js'
 export async function retry<T>(count: number, callback: () => T): Promise<T>
 export async function retry<T>(
   count: number,
-  duration: Duration,
+  duration: Duration | Generator<number>,
   callback: () => T
 ): Promise<T>
 export async function retry<T>(
   count: number,
-  a: Duration | (() => T),
+  a: Duration | Generator<number> | (() => T),
   b?: () => T
 ): Promise<T> {
   const total = count
   let callback: () => T
-  let delay = 0
+  let delayStatic = 0
+  let delayGen: Generator<number> | undefined
   if (typeof a == 'function') {
     callback = a
   } else {
-    delay = parseDuration(a)
+    if (typeof a == 'object') {
+      delayGen = a
+    } else {
+      delayStatic = parseDuration(a)
+    }
     assert(b)
     callback = b
   }
   let lastErr: unknown
+  let attempt = 0
   while (count-- > 0) {
+    attempt++
     try {
       return await callback()
     } catch (err) {
+      let delay = 0
+      if (delayStatic > 0) delay = delayStatic
+      if (delayGen) delay = delayGen.next().value
       $.log({
         kind: 'retry',
         error:
           chalk.bgRed.white(' FAIL ') +
-          ` Attempt: ${total - count}/${total}` +
+          ` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` +
           (delay > 0 ? `; next in ${delay}ms` : ''),
       })
       lastErr = err
@@ -58,6 +68,16 @@ export async function retry<T>(
   throw lastErr
 }
 
+export function* expBackoff(max: Duration = '60s', rand: Duration = '100ms') {
+  const maxMs = parseDuration(max)
+  const randMs = parseDuration(rand)
+  let n = 1
+  while (true) {
+    const ms = Math.floor(Math.random() * randMs)
+    yield Math.min(2 ** n++, maxMs) + ms
+  }
+}
+
 export async function spinner<T>(callback: () => T): Promise<T>
 export async function spinner<T>(title: string, callback: () => T): Promise<T>
 export async function spinner<T>(
test/experimental.test.js
@@ -40,6 +40,21 @@ test('retry() works', async () => {
   assert.ok(Date.now() >= now + 50 * (5 - 1))
 })
 
+test('retry() with expBackoff() works', async () => {
+  const now = Date.now()
+  let p = await zx(`
+    try {
+      await retry(5, expBackoff('60s', 0), () => $\`exit 123\`)
+    } catch (e) {
+      echo('exitCode:', e.exitCode)
+    }
+    echo('success')
+`)
+  assert.match(p.toString(), 'exitCode: 123')
+  assert.match(p.toString(), 'success')
+  assert.ok(Date.now() >= now + 2 + 4 + 8 + 16 + 32)
+})
+
 test('spinner() works', async () => {
   let out = await zx(`
     echo(await spinner(async () => {