Skip to content

Commit

Permalink
Merge 72f1932 into 346bb7e
Browse files Browse the repository at this point in the history
  • Loading branch information
y13i committed Jun 5, 2017
2 parents 346bb7e + 72f1932 commit cfdefac
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 33 deletions.
73 changes: 47 additions & 26 deletions lib/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type HookFunction = (tries?: number, reason?: any) => Promise<any> | a

export interface ExecutorOptions {
maxTries?: number;
timeout?: number;
waiter?: HookFunction;
retryCondition?: HookFunction;
beforeTry?: HookFunction;
Expand All @@ -15,18 +16,22 @@ export interface ExecutorOptions {
}

export interface ExecutorInterface<T> {
execute(...args: any[]): Promise<T>;
execute(): Promise<T>;
}

export class Executor<T> implements ExecutorInterface<T> {
protected static defaultMaxTries: number = 5;
protected static defaultTimeout: number = -1;
protected static defaultRetryCondition: HookFunction = () => true;
protected static defaultWaiter: HookFunction = (tries: number) => wait(100 * tries ** 2);

protected maxTries: number = Executor.defaultMaxTries;
protected timeout: number = Executor.defaultTimeout;
protected retryCondition: HookFunction = Executor.defaultRetryCondition;
protected waiter: HookFunction = Executor.defaultWaiter;

protected args: any[];

protected beforeTry?: HookFunction;
protected afterTry?: HookFunction;
protected beforeWait?: HookFunction;
Expand All @@ -36,9 +41,13 @@ export class Executor<T> implements ExecutorInterface<T> {
constructor(
protected main: MainFunction<T>,
protected options?: ExecutorOptions,
...args: any[],
) {
this.args = args;

if (options) {
if (options.maxTries) this.maxTries = options.maxTries;
if (options.timeout) this.timeout = options.timeout;
if (options.retryCondition) this.retryCondition = options.retryCondition;
if (options.waiter) this.waiter = options.waiter;
if (options.beforeTry) this.beforeTry = options.beforeTry;
Expand All @@ -49,42 +58,54 @@ export class Executor<T> implements ExecutorInterface<T> {
}
}

async execute(...args: any[]): Promise<T> {
let successful: boolean = false;
let result: T = <any>undefined;
async execute(): Promise<T> {
const promises: Promise<T>[] = [this.tryLoop()];

if (this.timeout && this.timeout !== -1) promises.push(<Promise<never>>this.timer(this.timeout));

try {
const result = await Promise.race(promises);
return result;
} catch (reason) {
throw reason;
} finally {
if (this.doFinally) await this.doFinally();
}
}

loop: {
for (let tries = 1; tries <= this.maxTries; tries++) {
try {
if (this.beforeTry) await this.beforeTry(tries, result);
protected async tryLoop(): Promise<T> {
let result: T = <any>undefined;

result = await this.main(...args);
successful = true;
for (let tries = 1; this.maxTries === -1 || tries <= this.maxTries; tries++) {
try {
if (this.beforeTry) await this.beforeTry(tries, result);

if (this.afterTry) await this.afterTry(tries, result);
result = await this.main(...this.args);

break loop;
} catch (reason) {
result = reason;
if (this.afterTry) await this.afterTry(tries, result);

if (this.afterTry) await this.afterTry(tries, result);
break;
} catch (reason) {
if (this.afterTry) await this.afterTry(tries, reason);

if (tries === this.maxTries || !(await this.retryCondition(tries, result))) break loop;
if (false
|| tries === this.maxTries
|| !(await this.retryCondition(tries, reason))
) throw reason;

if (this.beforeWait) await this.beforeWait(tries, result);
if (this.waiter) await this.waiter(tries, result);
if (this.afterWait) await this.afterWait(tries, result);
}
if (this.beforeWait) await this.beforeWait(tries, reason);
if (this.waiter) await this.waiter(tries, reason);
if (this.afterWait) await this.afterWait(tries, reason);
}
}

if (this.doFinally) await this.doFinally();
return result;
}

if (successful) {
return result;
} else {
throw result;
}
protected async timer(duration: number): Promise<void> {
await new Promise((_, reject) => {
setTimeout(() => reject(new Error("timeout")), duration);
});
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/retryx.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Executor, {MainFunction, ExecutorOptions} from "./executor";

export function retryx<T>(main: MainFunction<T>, options?: ExecutorOptions, ...args: any[]): Promise<T> {
const executor = new Executor<T>(main, options);
return executor.execute(...args);
const executor = new Executor<T>(main, options, ...args);
return executor.execute();
}

export default retryx;
47 changes: 42 additions & 5 deletions test/executor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from "ava";
import {spy} from "sinon";
import {spy, assert} from "sinon";
import Executor from "../lib/executor";
import wait from "../lib/wait";

Expand Down Expand Up @@ -171,13 +171,30 @@ test("accept doFinally hook via options", async t => {
t.is(await t.throws(executor2.execute()), 2);
t.true(spy1.calledOnce);
t.true(spy2.calledOnce);

const spy3 = spy();
const spy4 = spy();
const spy5 = spy();

const executor3 = new Executor(() => {
spy3("in-main");
return Promise.resolve(33);
}, {
doFinally: () => spy4("doFinally"),
});

const result3 = await executor3.execute();
spy5("after retryx");

t.is(result3, 33);
assert.callOrder(spy3, spy4, spy5);
});

test("accept args to main function", async t => {
const executor1 = new Executor(arg => Promise.resolve(arg));
t.is(await executor1.execute("test!"), "test!");
const executor2 = new Executor((...args: any[]) => Promise.resolve([...args]));
t.deepEqual(await executor2.execute("TEST", 123), ["TEST", 123]);
const executor1 = new Executor(arg => Promise.resolve(arg), undefined, "test!");
t.is(await executor1.execute(), "test!");
const executor2 = new Executor((...args: any[]) => Promise.resolve([...args]), undefined, "TEST", 123);
t.deepEqual(await executor2.execute(), ["TEST", 123]);
});

test("exponential backoff from 100ms by default", async t => {
Expand All @@ -196,3 +213,23 @@ test("exponential backoff from 100ms by default", async t => {
t.true(endTime - startTime > 3000);
t.true(endTime - startTime < 3200);
});

test("rejects when timeout is set", async t => {
const startTime = Date.now();

const executor = new Executor(() => {
return new Promise((_, j) => setTimeout(() => j("impossible"), 1000));
}, {
maxTries: 1000000000000,
timeout: 2000,
});

const reason = await t.throws(executor.execute());

t.true(reason instanceof Error);

const endTime = Date.now();

t.true(endTime - startTime > 2000);
t.true(endTime - startTime < 2200);
});

0 comments on commit cfdefac

Please sign in to comment.