diff --git a/README.md b/README.md index 2e09ae2..4835da2 100644 --- a/README.md +++ b/README.md @@ -78,20 +78,27 @@ const retryPolicy = fixedRetryPolicy([3, 10, 50, 100], TimeUnit.Milliseconds); ``` #### Exponential backoff retry policy +Exponential backoff formula based retry policy with optional custom exponent base and a limit. +The optional `limit` provides control over maximum pause intervals, so they don't soar beyond reasonable values. + +**The formula used by this implementation is the following:** + +intervali = min(limit, (exponentiali) - 1) / 2 + ```ts -// 10 retries, starting with 10 milliseconds and then exponentially increases the delay based on the default power value without a limit. const retryPolicy = exponentialBackoffRetryPolicy(/* count = */10, /* opts?: { exponential?: number, limit?: number, units?: TimeUnit }*/); ``` ### RetryAround -Executes the given function with the retries based on the specified policy. +Executes the given function with retries based on the specified policy and *optional* predicate. +The predicate provides control over which errors we want to retry on. ```ts -const result = await retryAround(action, retryPolicy); +const result = await retryAround(action, retryPolicy, predicate); ``` ### Retriable -Wraps a given function with `retryAround` with the specified policy. +Wraps a given function with `retryAround` with the specified arguments. ```ts -const retriableAction = retriable(action, retryPolicy); +const retriableAction = retriable(action, retryPolicy, predicate); const result = await retriableAction(); ``` diff --git a/lib/retry.ts b/lib/retry.ts index 8fab191..2c317a3 100644 --- a/lib/retry.ts +++ b/lib/retry.ts @@ -79,7 +79,11 @@ function exponentialBackoffRetryPolicy( return Object.freeze(new ExponentialBackoffRetryPolicy(count, opts?.exponential, opts?.limit, opts?.units)); } -async function retryAround(action: () => T | Promise, policy: RetryPolicy): Promise { +async function retryAround( + action: () => T | Promise, + policy: RetryPolicy, + predicate: (e: Error) => boolean = () => true +): Promise { let next: IteratorResult; const intervals = policy.intervals()[Symbol.iterator](); do { @@ -88,7 +92,7 @@ async function retryAround(action: () => T | Promise, policy: RetryPolicy) } catch (e) { next = intervals.next(); - if (next.done) { + if (next.done || !predicate(e)) { throw e; } @@ -99,9 +103,13 @@ async function retryAround(action: () => T | Promise, policy: RetryPolicy) throw new Error('Unexpected error. This is most likely a bug.'); } -function retriable(action: () => T | Promise, policy: RetryPolicy): () => Promise { +function retriable( + action: () => T | Promise, + policy: RetryPolicy, + predicate: (e: Error) => boolean = () => true +): () => Promise { return () => { - return retryAround(action, policy); + return retryAround(action, policy, predicate); }; } diff --git a/package.json b/package.json index ccb3d3e..cd16296 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sha1n/about-time", - "version": "0.0.1", + "version": "0.0.2", "description": "A set of essential time related utilities", "repository": "https://github.com/sha1n/about-time", "author": "Shai Nagar", diff --git a/test/retry.spec.ts b/test/retry.spec.ts index 6d1a4ab..571d5b4 100644 --- a/test/retry.spec.ts +++ b/test/retry.spec.ts @@ -1,44 +1,75 @@ -import { fixedRetryPolicy, retriable, simpleRetryPolicy } from '../lib/retry'; +import { fixedRetryPolicy, retriable, retryAround, simpleRetryPolicy } from '../lib/retry'; import { anError, aString } from './randoms'; -describe('retriable', () => { +describe('retry', () => { const expectedError = anError(); - test('should throw when no retries are left', async () => { - const policy = fixedRetryPolicy([1, 1]); - const fail = () => Promise.reject(expectedError); + describe('when no predicate is specied', () => { + describe('retriable', () => { + test('should throw when no retries are left', async () => { + const policy = fixedRetryPolicy([1, 1]); + const fail = () => Promise.reject(expectedError); - await expect(retriable(fail, policy)()).rejects.toThrow(expectedError); - }); + await expect(retriable(fail, policy)()).rejects.toThrow(expectedError); + }); - test('should throw when no retries are left with sync action', async () => { - const policy = fixedRetryPolicy([1]); - const failSync = () => { - throw expectedError; - }; + test('should throw when no retries are left with sync action', async () => { + const policy = fixedRetryPolicy([1]); + const failSync = () => { + throw expectedError; + }; - await expect(retriable(failSync, policy)()).rejects.toThrow(expectedError); - }); + await expect(retriable(failSync, policy)()).rejects.toThrow(expectedError); + }); - test('should throw when the provided policy is effectively empty', async () => { - const policy = fixedRetryPolicy([]); - const { action } = givenAsyncActionThatFailsOnce(); + test('should throw when the provided policy is effectively empty', async () => { + const policy = fixedRetryPolicy([]); + const { action } = givenAsyncActionThatFailsOnce(); - await expect(retriable(action, policy)()).rejects.toThrow(expectedError); - }); + await expect(retriable(action, policy)()).rejects.toThrow(expectedError); + }); + + test('should retry and resolve with an async function resolved value', async () => { + const policy = simpleRetryPolicy(1, 1); + const { action, expectedValue } = givenAsyncActionThatFailsOnce(); - test('should retry and resolve with an async function resolved value', async () => { - const policy = simpleRetryPolicy(1, 1); - const { action, expectedValue } = givenAsyncActionThatFailsOnce(); + await expect(retriable(action, policy)()).resolves.toEqual(expectedValue); + }); - await expect(retriable(action, policy)()).resolves.toEqual(expectedValue); + test('should retry and resolve with a sync function returned value', async () => { + const policy = simpleRetryPolicy(1, 1); + const { action, expectedValue } = givenSyncActionThatFailsOnce(); + + await expect(retriable(action, policy)()).resolves.toEqual(expectedValue); + }); + }); + + describe('retryAround', () => { + test('should retry if an error is thrown', async () => { + const policy = simpleRetryPolicy(1, 1); + const { action, expectedValue } = givenSyncActionThatFailsOnce(); + + await expect(retryAround(action, policy)).resolves.toEqual(expectedValue); + }); + }); }); - test('should retry and resolve with a sync function returned value', async () => { - const policy = simpleRetryPolicy(1, 1); - const { action, expectedValue } = givenSyncActionThatFailsOnce(); + describe('when a predicate is specified', () => { + describe('retriable', () => { + test('should not retry if a specified predicate returns false', async () => { + const policy = simpleRetryPolicy(1, 1); + const { action } = givenSyncActionThatFailsOnce(); + + await expect(retriable(action, policy, () => false)()).rejects.toThrow(expectedError); + }); + + test('should retry if a specified predicate returns true', async () => { + const policy = simpleRetryPolicy(1, 1); + const { action } = givenSyncActionThatFailsOnce(); - await expect(retriable(action, policy)()).resolves.toEqual(expectedValue); + await expect(retriable(action, policy, () => true)()).toResolve(); + }); + }); }); function givenSyncActionThatFailsOnce(): {