-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathRetry.ts
More file actions
95 lines (80 loc) · 3.09 KB
/
Retry.ts
File metadata and controls
95 lines (80 loc) · 3.09 KB
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
import { Logger } from 'pino';
import { extractErrorMessage } from './Errors';
export type RetryOptions = {
maxRetries?: number;
initialDelayMs?: number;
maxDelayMs?: number;
backoffMultiplier?: number;
jitterFactor?: number;
operationName: string;
totalTimeoutMs: number;
};
export function sleep(ms: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(resolve, ms);
});
}
function calculateDelay(
attempt: number,
initialDelayMs: number,
jitterFactor: number,
backoffMultiplier: number,
maxDelayMs: number,
): number {
// 1. Exponential Backoff: initial * 2^0, initial * 2^1, etc.
const exponentialDelay = initialDelayMs * Math.pow(backoffMultiplier, attempt);
// 2. Cap at Max Delay
const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
// 3. Add Jitter (randomized percentage of the current delay), capped at maxDelayMs
const jitter = jitterFactor > 0 ? Math.random() * jitterFactor * cappedDelay : 0;
return Math.min(cappedDelay + jitter, maxDelayMs);
}
export async function retryWithExponentialBackoff<T>(
fn: () => Promise<T>,
options: RetryOptions,
log: Logger,
sleepFn: (ms: number) => Promise<void> = (ms: number) => {
return sleep(ms);
},
): Promise<T> {
const {
maxRetries = 3,
initialDelayMs = 1000,
maxDelayMs = 5000,
backoffMultiplier = 2,
jitterFactor = 0.1,
operationName,
totalTimeoutMs,
} = options;
if (backoffMultiplier < 1) {
throw new Error('Backoff multiplier must be greater than or equal to 1');
}
if (totalTimeoutMs <= 0) {
throw new Error('Total timeout must be greater than 0');
}
let lastError: Error | undefined = undefined;
const startTime = performance.now();
// If maxRetries is 3, we run: 0 (initial), 1, 2, 3. Total 4 attempts.
const attempts = maxRetries + 1;
for (let attemptIdx = 0; attemptIdx < attempts; attemptIdx++) {
if (attemptIdx > 0 && performance.now() - startTime >= totalTimeoutMs) {
const message = `${operationName} timed out after ${performance.now() - startTime}ms, on attempt #${attemptIdx + 1}/${attempts}`;
const errorMsg = lastError ? `${message}. Last error: ${lastError.message}` : message;
throw new Error(errorMsg);
}
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(extractErrorMessage(error));
if (attemptIdx === attempts - 1) {
throw new Error(`${operationName} failed after ${attempts} attempts. Last error: ${lastError.message}`);
}
const delay = calculateDelay(attemptIdx, initialDelayMs, jitterFactor, backoffMultiplier, maxDelayMs);
log.warn(
`${operationName} attempt ${attemptIdx + 1} failed: ${lastError.message}. Retrying in ${Math.round(delay)}ms...`,
);
await sleepFn(delay);
}
}
throw new Error('Something went wrong, this is not reachable');
}