Skip to content

Commit

Permalink
added optional error predicate for more flex retries (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
sha1n authored Jan 18, 2022
1 parent b5cd830 commit 5c05780
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 37 deletions.
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

interval<sub>i</sub> = min(limit, (exponential<sup>i</sup>) - 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();
```
16 changes: 12 additions & 4 deletions lib/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ function exponentialBackoffRetryPolicy(
return Object.freeze(new ExponentialBackoffRetryPolicy(count, opts?.exponential, opts?.limit, opts?.units));
}

async function retryAround<T>(action: () => T | Promise<T>, policy: RetryPolicy): Promise<T> {
async function retryAround<T>(
action: () => T | Promise<T>,
policy: RetryPolicy,
predicate: (e: Error) => boolean = () => true
): Promise<T> {
let next: IteratorResult<number, undefined>;
const intervals = policy.intervals()[Symbol.iterator]();
do {
Expand All @@ -88,7 +92,7 @@ async function retryAround<T>(action: () => T | Promise<T>, policy: RetryPolicy)
} catch (e) {
next = intervals.next();

if (next.done) {
if (next.done || !predicate(e)) {
throw e;
}

Expand All @@ -99,9 +103,13 @@ async function retryAround<T>(action: () => T | Promise<T>, policy: RetryPolicy)
throw new Error('Unexpected error. This is most likely a bug.');
}

function retriable<T>(action: () => T | Promise<T>, policy: RetryPolicy): () => Promise<T> {
function retriable<T>(
action: () => T | Promise<T>,
policy: RetryPolicy,
predicate: (e: Error) => boolean = () => true
): () => Promise<T> {
return () => {
return retryAround(action, policy);
return retryAround(action, policy, predicate);
};
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
85 changes: 58 additions & 27 deletions test/retry.spec.ts
Original file line number Diff line number Diff line change
@@ -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(): {
Expand Down

0 comments on commit 5c05780

Please sign in to comment.