-
Notifications
You must be signed in to change notification settings - Fork 48
Expand file tree
/
Copy pathHttpRetryPolicy.ts
More file actions
102 lines (84 loc) · 3.63 KB
/
HttpRetryPolicy.ts
File metadata and controls
102 lines (84 loc) · 3.63 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
96
97
98
99
100
101
102
import IRetryPolicy, { ShouldRetryResult, RetryableOperation } from '../contracts/IRetryPolicy';
import { HttpTransactionDetails } from '../contracts/IConnectionProvider';
import IClientContext from '../../contracts/IClientContext';
import RetryError, { RetryErrorCode } from '../../errors/RetryError';
function delay(milliseconds: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => resolve(), milliseconds);
});
}
export default class HttpRetryPolicy implements IRetryPolicy<HttpTransactionDetails> {
private context: IClientContext;
private readonly startTime: number; // in milliseconds
private attempt: number;
constructor(context: IClientContext) {
this.context = context;
this.startTime = Date.now();
this.attempt = 0;
}
public async shouldRetry(details: HttpTransactionDetails): Promise<ShouldRetryResult> {
if (this.isRetryable(details)) {
const clientConfig = this.context.getConfig();
// Don't retry if overall retry timeout exceeded
const timeoutExceeded = Date.now() - this.startTime >= clientConfig.retriesTimeout;
if (timeoutExceeded) {
throw new RetryError(RetryErrorCode.TimeoutExceeded, details);
}
this.attempt += 1;
// Don't retry if max attempts count reached
const attemptsExceeded = this.attempt >= clientConfig.retryMaxAttempts;
if (attemptsExceeded) {
throw new RetryError(RetryErrorCode.AttemptsExceeded, details);
}
// If possible, use `Retry-After` header as a floor for a backoff algorithm
const retryAfterHeader = this.getRetryAfterHeader(details, clientConfig.retryDelayMin);
const retryAfter = this.getBackoffDelay(
this.attempt,
retryAfterHeader ?? clientConfig.retryDelayMin,
clientConfig.retryDelayMax,
);
return { shouldRetry: true, retryAfter };
}
return { shouldRetry: false };
}
public async invokeWithRetry(operation: RetryableOperation<HttpTransactionDetails>): Promise<HttpTransactionDetails> {
for (;;) {
const details = await operation(); // eslint-disable-line no-await-in-loop
const status = await this.shouldRetry(details); // eslint-disable-line no-await-in-loop
if (!status.shouldRetry) {
return details;
}
await delay(status.retryAfter); // eslint-disable-line no-await-in-loop
}
}
protected isRetryable({ response }: HttpTransactionDetails): boolean {
const statusCode = response.status;
const result =
// Retry on all codes below 100
statusCode < 100 ||
// ...and on `429 Too Many Requests`
statusCode === 429 ||
// ...and on all `5xx` codes except for `501 Not Implemented`
(statusCode >= 500 && statusCode !== 501);
return result;
}
protected getRetryAfterHeader({ response }: HttpTransactionDetails, delayMin: number): number | undefined {
// `Retry-After` header may contain a date after which to retry, or delay seconds. We support only delay seconds.
// Value from `Retry-After` header is used when:
// 1. it's available and is non-empty
// 2. it could be parsed as a number, and is greater than zero
// 3. additionally, we clamp it to not be smaller than minimal retry delay
const header = response.headers.get('Retry-After') || '';
if (header !== '') {
const value = Number(header);
if (Number.isFinite(value) && value > 0) {
return Math.max(delayMin, value);
}
}
return undefined;
}
protected getBackoffDelay(attempt: number, delayMin: number, delayMax: number): number {
const value = 2 ** attempt * delayMin;
return Math.min(value, delayMax);
}
}