From ef4023c82643ff7d529e28aaa4d5356e0ab7b2b1 Mon Sep 17 00:00:00 2001 From: Elmer Bulthuis Date: Mon, 28 Oct 2019 11:13:31 +0100 Subject: [PATCH] upgrade retry, lose lift --- src/index.ts | 4 +++ src/lift.spec.ts | 27 ---------------- src/lift.ts | 78 ----------------------------------------------- src/main.ts | 1 - src/random.ts | 3 ++ src/retry.spec.ts | 71 ++++++++++++++---------------------------- src/retry.ts | 56 +++++++++++++++++++--------------- tslint.json | 5 +-- 8 files changed, 63 insertions(+), 182 deletions(-) create mode 100644 src/index.ts delete mode 100644 src/lift.spec.ts delete mode 100644 src/lift.ts create mode 100644 src/random.ts diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1837016 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +export * from "./delay"; +export * from "./retry"; +export * from "./defer"; +export * from "./random"; diff --git a/src/lift.spec.ts b/src/lift.spec.ts deleted file mode 100644 index 079ad08..0000000 --- a/src/lift.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as test from "blue-tape"; -import { lift } from "./lift"; - -test("lift", async (t) => { - const refResult = {}; - const refError = new Error(); - - function fn(ok: boolean, cb: (error: Error | null, result: any) => void) { - if (ok) { - cb(null, refResult); - } else { - cb(refError, null); - } - } - - const lifted = lift(fn); - - await Promise.all([ - lifted(true).then((result) => { - t.equal(refResult, result); - }), - lifted(false).then(null, (error) => { - t.equal(refError, error); - }), - ]); - -}); diff --git a/src/lift.ts b/src/lift.ts deleted file mode 100644 index f64a267..0000000 --- a/src/lift.ts +++ /dev/null @@ -1,78 +0,0 @@ - -export type Callback = - (err: Error | any | undefined | any, result: TResult) => void; - -export function lift( - fn: (cb: Callback) => void, -): () => Promise; - -export function lift( - fn: (arg1: TArg1, cb: Callback) => void, -): (arg1: TArg1) => Promise; - -export function lift( - fn: (arg1: TArg1, arg2: TArg2, cb: Callback) => void, -): (arg1: TArg1, arg2: TArg2) => Promise; - -export function lift( - fn: (arg1: TArg1, arg2: TArg2, arg3: TArg3, cb: Callback) => void, -): (arg1: TArg1, arg2: TArg2, arg3: TArg3) => Promise; - -export function lift< - TResult, - TArg1, TArg2, - TArg3, TArg4 - >( - fn: ( - arg1: TArg1, arg2: TArg2, - arg3: TArg3, arg4: TArg4, - cb: Callback, - ) => void, -): ( - arg1: TArg1, arg2: TArg2, - arg3: TArg3, arg4: TArg4, - ) => Promise; - -export function lift< - TResult, - TArg1, TArg2, - TArg3, TArg4, TArg5 - >( - fn: ( - arg1: TArg1, arg2: TArg2, - arg3: TArg3, arg4: TArg4, arg5: TArg5, - cb: Callback, - ) => void, -): ( - arg1: TArg1, arg2: TArg2, - arg3: TArg3, arg4: TArg4, arg5: TArg5, - ) => Promise; - -export function lift< - TResult, - TArg1, TArg2, TArg3, - TArg4, TArg5, TArg6 - >( - fn: ( - arg1: TArg1, arg2: TArg2, arg3: TArg3, - arg4: TArg4, arg5: TArg5, arg6: TArg6, - cb: Callback, - ) => void, -): ( - arg1: TArg1, arg2: TArg2, arg3: TArg3, - arg4: TArg4, arg5: TArg5, arg6: TArg6, - ) => Promise; - -export function lift(fn: (...args: any[]) => void) { - return (...args: any[]) => { - return new Promise((resolve, reject) => { - fn(...args, (error: Error, result: TResult) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - }; -} diff --git a/src/main.ts b/src/main.ts index 836701f..682f5ca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,3 @@ -export * from "./lift"; export * from "./delay"; export * from "./retry"; export * from "./defer"; diff --git a/src/random.ts b/src/random.ts new file mode 100644 index 0000000..d54c050 --- /dev/null +++ b/src/random.ts @@ -0,0 +1,3 @@ +export function randomBetween(min: number, max: number) { + return min + (max - min) * Math.random(); +} diff --git a/src/retry.spec.ts b/src/retry.spec.ts index 443b063..e1e34ba 100644 --- a/src/retry.spec.ts +++ b/src/retry.spec.ts @@ -1,55 +1,30 @@ import * as test from "blue-tape"; import { retry } from "./retry"; -test("retry", async (t) => { - - { - let callCount = 0; - const offset = new Date().valueOf(); - const result = await retry((attempt) => { - callCount++; - return true; - }); - - const time = new Date().valueOf() - offset; - t.ok(result); - t.equal(callCount, 1); - t.ok(time >= 0 && time < 1000, `time should be 0ms`); - } - - { - let callCount = 0; - const offset = new Date().valueOf(); - const result = await retry((attempt) => { - callCount++; - if (attempt === 2) return true; - throw new Error("cancel"); - }, { intervalBase: 1000, intervalIncrement: 1000 }); - - const time = new Date().valueOf() - offset; - t.ok(result); - t.equal(callCount, 3); - t.ok(time >= 3000 && time < 4000, `time should be 3000ms`); - } - - { - let callCount = 0; - const offset = new Date().valueOf(); - - try { - const result = await retry((attempt) => { - callCount++; - throw new Error("cancel"); - }, { intervalBase: 1000, intervalIncrement: 0, retryLimit: 4 }); - t.fail("should throw"); - } - catch (err) { - t.pass("should throw"); +test("does not retry when told not to", async (t) => { + let triesLeft = 3; + const retryLogic = async () => await retry(() => { + if (triesLeft > 0) { + t.comment("Retry!"); + throw new Error("Error"); } + }, + {}, + _ => (triesLeft-- > 0), + ); - const time = new Date().valueOf() - offset; - t.equal(callCount, 4); - t.ok(time >= 4000 && time < 5000, `time should be 3000ms`); - } + t.doesNotThrow(retryLogic, "did not retry when no tries were left"); +}); +test("forwards error to caller", async (t) => { + try { + await retry(() => { + throw new Error("InnerError"); + }, + {}, + _ => false, + ); + } catch (err) { + t.equal(err.message, "InnerError", "error messages match"); + } }); diff --git a/src/retry.ts b/src/retry.ts index aa0d220..4672f5f 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -1,39 +1,47 @@ -import { delay } from "./delay"; +import { delay, randomBetween } from "."; export interface RetryConfig { - retryLimit: number; - intervalBase: number; - intervalIncrement: number; + retryLimit?: number; + intervalCap?: number; + intervalBase?: number; } -export const defaultRetryConfig = Object.freeze({ - retryLimit: 3, - intervalBase: 10 * 1000, // 10 seconds - intervalIncrement: 10 * 1000, // 10 seconds, -}); +export const defaultRetryConfig = { + retryLimit: 10, + intervalCap: 5000, + intervalBase: 100, +}; export async function retry( job: (attempt: number) => PromiseLike | T, - config: Partial = {}, + config: RetryConfig = {}, + shouldTryAgain = (error: any) => true, ): Promise { const { - retryLimit, intervalBase, intervalIncrement, - } = { ...defaultRetryConfig, ...config } as RetryConfig; - - let lastError: any; - - for (let attempt = 0; attempt < retryLimit; attempt++) { + retryLimit, + intervalBase, + intervalCap, + } = { ...defaultRetryConfig, ...config }; + let retryAttempt = 0; + let intervalCurrent = intervalBase; + while (true) { try { - const result: T = await job(attempt); + const result = await job(retryAttempt); return result; } - catch (err) { - lastError = err; - await delay( - intervalBase + intervalIncrement * attempt, - ); + catch (error) { + if ( + retryAttempt < retryLimit && + shouldTryAgain(error) + ) { + error = null; + } + if (error) throw error; } - } + // https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + intervalCurrent = Math.min(intervalCap, randomBetween(intervalBase, intervalCurrent * 3)); + await delay(intervalCurrent); - throw lastError; + retryAttempt++; + } } diff --git a/tslint.json b/tslint.json index 4bb1ab5..56d0a01 100644 --- a/tslint.json +++ b/tslint.json @@ -9,10 +9,7 @@ "one-line": false, "arrow-parens": false, "curly": false, - "object-literal-sort-keys": [ - true, - "declaration-order" - ] + "object-literal-sort-keys": false }, "rulesDirectory": [] } \ No newline at end of file