-
Notifications
You must be signed in to change notification settings - Fork 85
/
StandardRetryStrategy.ts
144 lines (122 loc) · 4.62 KB
/
StandardRetryStrategy.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
import { HttpRequest, HttpResponse } from "@smithy/protocol-http";
import { isThrottlingError } from "@smithy/service-error-classification";
import { SdkError } from "@smithy/types";
import { FinalizeHandler, FinalizeHandlerArguments, MetadataBearer, Provider, RetryStrategy } from "@smithy/types";
import {
DEFAULT_MAX_ATTEMPTS,
DEFAULT_RETRY_DELAY_BASE,
INITIAL_RETRY_TOKENS,
INVOCATION_ID_HEADER,
REQUEST_HEADER,
RETRY_MODES,
THROTTLING_RETRY_DELAY_BASE,
} from "@smithy/util-retry";
import { v4 } from "uuid";
import { getDefaultRetryQuota } from "./defaultRetryQuota";
import { defaultDelayDecider } from "./delayDecider";
import { defaultRetryDecider } from "./retryDecider";
import { DelayDecider, RetryDecider, RetryQuota } from "./types";
import { asSdkError } from "./util";
/**
* Strategy options to be passed to StandardRetryStrategy
*/
export interface StandardRetryStrategyOptions {
retryDecider?: RetryDecider;
delayDecider?: DelayDecider;
retryQuota?: RetryQuota;
}
/**
* @deprecated use StandardRetryStrategy from @smithy/util-retry
*/
export class StandardRetryStrategy implements RetryStrategy {
private retryDecider: RetryDecider;
private delayDecider: DelayDecider;
private retryQuota: RetryQuota;
public mode: string = RETRY_MODES.STANDARD;
constructor(private readonly maxAttemptsProvider: Provider<number>, options?: StandardRetryStrategyOptions) {
this.retryDecider = options?.retryDecider ?? defaultRetryDecider;
this.delayDecider = options?.delayDecider ?? defaultDelayDecider;
this.retryQuota = options?.retryQuota ?? getDefaultRetryQuota(INITIAL_RETRY_TOKENS);
}
private shouldRetry(error: SdkError, attempts: number, maxAttempts: number) {
return attempts < maxAttempts && this.retryDecider(error) && this.retryQuota.hasRetryTokens(error);
}
private async getMaxAttempts() {
let maxAttempts: number;
try {
maxAttempts = await this.maxAttemptsProvider();
} catch (error) {
maxAttempts = DEFAULT_MAX_ATTEMPTS;
}
return maxAttempts;
}
async retry<Input extends object, Ouput extends MetadataBearer>(
next: FinalizeHandler<Input, Ouput>,
args: FinalizeHandlerArguments<Input>,
options?: {
beforeRequest: Function;
afterRequest: Function;
}
) {
let retryTokenAmount;
let attempts = 0;
let totalDelay = 0;
const maxAttempts = await this.getMaxAttempts();
const { request } = args;
if (HttpRequest.isInstance(request)) {
request.headers[INVOCATION_ID_HEADER] = v4();
}
while (true) {
try {
if (HttpRequest.isInstance(request)) {
request.headers[REQUEST_HEADER] = `attempt=${attempts + 1}; max=${maxAttempts}`;
}
if (options?.beforeRequest) {
await options.beforeRequest();
}
const { response, output } = await next(args);
if (options?.afterRequest) {
options.afterRequest(response);
}
this.retryQuota.releaseRetryTokens(retryTokenAmount);
output.$metadata.attempts = attempts + 1;
output.$metadata.totalRetryDelay = totalDelay;
return { response, output };
} catch (e) {
const err = asSdkError(e);
attempts++;
if (this.shouldRetry(err as SdkError, attempts, maxAttempts)) {
retryTokenAmount = this.retryQuota.retrieveRetryTokens(err);
const delayFromDecider = this.delayDecider(
isThrottlingError(err) ? THROTTLING_RETRY_DELAY_BASE : DEFAULT_RETRY_DELAY_BASE,
attempts
);
const delayFromResponse = getDelayFromRetryAfterHeader(err.$response);
const delay = Math.max(delayFromResponse || 0, delayFromDecider);
totalDelay += delay;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
if (!err.$metadata) {
err.$metadata = {};
}
err.$metadata.attempts = attempts;
err.$metadata.totalRetryDelay = totalDelay;
throw err;
}
}
}
}
/**
* Returns number of milliseconds to wait based on "Retry-After" header value.
*/
const getDelayFromRetryAfterHeader = (response: unknown): number | undefined => {
if (!HttpResponse.isInstance(response)) return;
const retryAfterHeaderName = Object.keys(response.headers).find((key) => key.toLowerCase() === "retry-after");
if (!retryAfterHeaderName) return;
const retryAfter = response.headers[retryAfterHeaderName];
const retryAfterSeconds = Number(retryAfter);
if (!Number.isNaN(retryAfterSeconds)) return retryAfterSeconds * 1000;
const retryAfterDate = new Date(retryAfter);
return retryAfterDate.getTime() - Date.now();
};