-
Notifications
You must be signed in to change notification settings - Fork 9
/
retry-promise.ts
159 lines (129 loc) · 4.84 KB
/
retry-promise.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"use strict"
import {timeout} from "./timeout";
export interface RetryConfig<T = any> {
// number of maximal retry attempts (default: 10)
retries: number | "INFINITELY";
// wait time between retries in ms (default: 100)
delay: number;
// check the result, will retry until true (default: () => true)
until: (t: T) => boolean;
// log events (default: () => undefined)
logger: (msg: string) => void;
// overall timeout in ms (default: 60 * 1000)
timeout: number | "INFINITELY";
// increase delay with every retry (default: "FIXED")
backoff: "FIXED" | "EXPONENTIAL" | "LINEAR" | ((attempt: number, delay: number) => number);
// maximal backoff in ms (default: 5 * 60 * 1000)
maxBackOff: number;
// allows to abort retrying for certain errors
retryIf: (error: any) => boolean
}
const fixedBackoff = (attempt: number, delay: number) => delay;
const linearBackoff = (attempt: number, delay: number) => attempt * delay;
const exponentialBackoff = (attempt: number, delay: number) => Math.pow(delay, attempt);
export const defaultRetryConfig: RetryConfig<any> = {
backoff: "FIXED",
delay: 100,
logger: () => undefined,
maxBackOff: 5 * 60 * 1000,
retries: 10,
timeout: 60 * 1000,
until: () => true,
retryIf: () => true
};
export async function wait(ms: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
}
export async function retry<T>(f: () => Promise<T>, config?: Partial<RetryConfig<T>>): Promise<T> {
const effectiveConfig: RetryConfig<T> = Object.assign({}, defaultRetryConfig, config) as RetryConfig<T>;
return timeout(effectiveConfig.timeout, (done) => _retry(f, effectiveConfig, done));
}
export function retryDecorator<T, F extends (...args: any[]) => Promise<T>>(func: F, config?: Partial<RetryConfig<T>>): (...funcArgs: Parameters<F>) => ReturnType<F> {
return (...args: Parameters<F>) => retry(() => func(...args), config) as ReturnType<F>;
}
export function customizeDecorator<T>(customConfig: Partial<RetryConfig<T>>): typeof retryDecorator {
return (args, config) => retryDecorator(args, Object.assign({}, customConfig, config));
}
// tslint:disable-next-line
export function customizeRetry<T>(customConfig: Partial<RetryConfig<T>>): (f: () => Promise<T>, config?: Partial<RetryConfig<T>>) => Promise<T> {
return (f, c) => {
const customized = Object.assign({}, customConfig, c);
return retry(f, customized);
};
}
async function _retry<T>(f: () => Promise<T>, config: RetryConfig<T>, done: () => boolean): Promise<T> {
let lastError: Error;
let delay: (attempt: number, delay: number) => number;
switch (config.backoff) {
case "EXPONENTIAL":
delay = exponentialBackoff;
break;
case "FIXED":
delay = fixedBackoff;
break;
case "LINEAR":
delay = linearBackoff;
break;
default:
delay = config.backoff;
}
let retries: number;
if (config.retries === "INFINITELY") {
retries = Number.MAX_SAFE_INTEGER;
} else {
retries = config.retries;
}
for (let i = 0; i <= retries; i++) {
try {
const result = await f();
if (config.until(result)) {
return result;
}
config.logger("Until condition not met by " + result);
} catch (error) {
if (!config.retryIf(error)) {
throw error;
}
if (error.name === NotRetryableError.name) {
throw new RetryError(
`Met not retryable error. Last error: ${error}`,
error
)
}
lastError = error;
config.logger("Retry failed: " + error.message);
}
const millisToWait = delay(i + 1, config.delay);
await wait(millisToWait > config.maxBackOff ? config.maxBackOff : millisToWait);
if (done()) {
break;
}
}
throw new RetryError(`All retries failed. Last error: ${lastError!}`, lastError!);
}
export const notEmpty = (result: any) => {
if (Array.isArray(result)) {
return result.length > 0;
}
return result !== null && result !== undefined;
};
export class RetryError extends Error {
/* istanbul ignore next */
constructor(message: string, public readonly lastError: Error) {
super(message);
}
}
// tslint:disable-next-line:max-classes-per-file
class BaseError {
constructor (...args: unknown[]) {
Error.apply(this, args as any);
}
}
BaseError.prototype = new Error();
// tslint:disable-next-line:max-classes-per-file
export class NotRetryableError extends BaseError {
constructor(message?: string) {
super(message);
Object.defineProperty(this, 'name', { value: this.constructor.name })
}
}