Skip to content
This repository was archived by the owner on Aug 1, 2022. It is now read-only.

Commit e649c0a

Browse files
feat: add retriers
1 parent abd8298 commit e649c0a

File tree

5 files changed

+154
-11
lines changed

5 files changed

+154
-11
lines changed

src/stateTasks/executors/TaskExecutor.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { TaskStateDefinition } from '../../types/State';
88
import { StateInfoHandler } from '../../StateInfoHandler';
99
import { StateProcessor } from '../../StateProcessor';
1010
import { Context } from '../../Context/Context';
11+
import { Retriers } from '../../types/Retriers';
1112

1213
export class TaskExecutor extends StateTypeExecutor {
1314
public async execute(
@@ -27,7 +28,16 @@ export class TaskExecutor extends StateTypeExecutor {
2728
const functionLambda = await import(`${lambdaPath}`);
2829

2930
this.injectEnvVarsLambdaSpecific(stateInfo.environment);
30-
const output = await functionLambda[stateInfo.handlerName](input, context);
31+
32+
// eslint-disable-next-line @typescript-eslint/ban-types
33+
let output: any;
34+
if (stateDefinition.Retry) {
35+
const retrier = Retriers.create(stateDefinition.Retry);
36+
output = await retrier.retry(functionLambda[stateInfo.handlerName].bind(this, input, context));
37+
} else {
38+
output = await functionLambda[stateInfo.handlerName](input, context);
39+
}
40+
3141
this.removeEnvVarsLambdaSpecific(stateInfo.environment);
3242

3343
const outputJson = this.processOutput(input, output, stateDefinition);

src/types/Retrier.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { TaskRetryRule } from './State';
2+
3+
export enum StatesErrors {
4+
DataLimitExceeded = 'States.DataLimitExceeded',
5+
Runtime = 'States.Runtime',
6+
Timeout = 'States.Timeout',
7+
TaskFailed = 'States.TaskFailed',
8+
Permissions = 'States.Permissions',
9+
}
10+
11+
export class Retrier {
12+
private currentNumberOfRetries = 0;
13+
private currentIntervalSeconds: number;
14+
15+
private constructor(
16+
private readonly _ErrorEquals: StatesErrors[],
17+
private readonly _IntervalSeconds: number = 1,
18+
private readonly _MaxAttempts: number = 3,
19+
private readonly _BackoffRate: number = 2.0,
20+
) {
21+
this.currentIntervalSeconds = _IntervalSeconds;
22+
}
23+
24+
public static create(taskRetryRule: TaskRetryRule): Retrier {
25+
return new Retrier(
26+
taskRetryRule.ErrorEquals,
27+
taskRetryRule.IntervalSeconds,
28+
taskRetryRule.MaxAttempts,
29+
taskRetryRule.BackoffRate,
30+
);
31+
}
32+
33+
async retry(fn: () => any): Promise<any> {
34+
try {
35+
return await fn();
36+
} catch (error) {
37+
if (this.currentNumberOfRetries < this._MaxAttempts) {
38+
this.currentNumberOfRetries++;
39+
return await new Promise((resolve) => {
40+
setTimeout(async () => {
41+
this.currentIntervalSeconds = this.currentIntervalSeconds * this._BackoffRate;
42+
return resolve(await this.retry(fn));
43+
}, this.currentIntervalSeconds * 1000);
44+
});
45+
} else {
46+
throw error;
47+
}
48+
}
49+
}
50+
51+
get ErrorEquals(): StatesErrors[] {
52+
return this._ErrorEquals;
53+
}
54+
}

src/types/Retriers.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Retrier, StatesErrors } from './Retrier';
2+
import { TaskRetryRule } from './State';
3+
4+
export class Retriers {
5+
private constructor(private readonly _retriers: Retrier[]) {}
6+
7+
public static create(taskRetryRules: TaskRetryRule[]): Retriers {
8+
const retriers = taskRetryRules.map((taskRetryRule) => {
9+
return Retrier.create(taskRetryRule);
10+
});
11+
return new Retriers(retriers);
12+
}
13+
14+
private getRetrierBasedOn(statesError: StatesErrors): Retrier | undefined {
15+
const retrier = this._retriers.find((retrier) => {
16+
return retrier.ErrorEquals.includes(statesError);
17+
});
18+
if (!retrier) {
19+
return;
20+
}
21+
return retrier;
22+
}
23+
24+
// TODO: Add the number of retries to the context object
25+
async retry(fn: () => any): Promise<any> {
26+
// TODO: Add timeout error catching
27+
const retrier = this.getRetrierBasedOn(StatesErrors.TaskFailed);
28+
29+
if (retrier) {
30+
return await retrier.retry(fn);
31+
} else {
32+
return fn();
33+
}
34+
}
35+
}

src/types/State.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { StateMachineDefinition } from './StateMachine';
22
import { StateType } from '../stateTasks/StateType';
3+
import { StatesErrors } from './Retrier';
34

45
export type StateInfo = {
56
handlerPath: string;
@@ -84,23 +85,15 @@ export type PassStateDefinition = CommonStateDefinition & {
8485
Parameters?: string; // TODO: TBD
8586
};
8687

87-
export type TaskErrorName =
88-
| 'State.ALL'
89-
| 'States.DataLimitExceeded'
90-
| 'States.Runtime'
91-
| 'States.Timeout'
92-
| 'States.TaskFailed'
93-
| 'States.Permissions';
94-
9588
export type TaskRetryRule = {
96-
ErrorEquals: TaskErrorName[];
89+
ErrorEquals: StatesErrors[];
9790
IntervalSeconds?: number;
9891
MaxAttempts?: number;
9992
BackoffRate?: number;
10093
};
10194

10295
export type TaskCatchRule = {
103-
ErrorEquals: TaskErrorName[];
96+
ErrorEquals: StatesErrors[];
10497
Next: string;
10598
ResultPath?: string;
10699
};

tests/src/Retrier.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Retrier, StatesErrors } from '../../src/types/Retrier';
2+
3+
describe('Retrier', () => {
4+
describe('when the function succeeds', () => {
5+
it('should call it once', () => {
6+
const retrier = Retrier.create({ ErrorEquals: [StatesErrors.TaskFailed] });
7+
const retriedFunction = jest.fn();
8+
retrier.retry(retriedFunction);
9+
expect(retriedFunction).toHaveBeenCalledTimes(1);
10+
});
11+
12+
describe('when the function is async', () => {
13+
it('should call it once', () => {
14+
const retrier = Retrier.create({ ErrorEquals: [StatesErrors.TaskFailed] });
15+
const retriedFunction = jest.fn().mockImplementation(() => Promise.resolve);
16+
retrier.retry(retriedFunction);
17+
expect(retriedFunction).toHaveBeenCalledTimes(1);
18+
});
19+
});
20+
});
21+
22+
describe('when the function fails', () => {
23+
it('should call it 3 times', () => {
24+
const retrier = Retrier.create({ ErrorEquals: [StatesErrors.TaskFailed] });
25+
const retriedFunction = jest.fn().mockImplementation(() => {
26+
throw new Error('MyError');
27+
});
28+
29+
try {
30+
retrier.retry(retriedFunction);
31+
} catch (error) {
32+
expect(error.message).toEqual('MyError');
33+
expect(retriedFunction).toHaveBeenCalledTimes(3);
34+
}
35+
});
36+
37+
it('should call it 5 times', () => {
38+
const retrier = Retrier.create({ ErrorEquals: [StatesErrors.TaskFailed], MaxAttempts: 5 });
39+
const retriedFunction = jest.fn().mockImplementation(() => {
40+
throw new Error('MyError');
41+
});
42+
43+
try {
44+
retrier.retry(retriedFunction);
45+
} catch (error) {
46+
expect(error.message).toEqual('MyError');
47+
expect(retriedFunction).toHaveBeenCalledTimes(5);
48+
}
49+
});
50+
});
51+
});

0 commit comments

Comments
 (0)