Skip to content

Commit ff70fac

Browse files
AllanZhengYPtrivikr
authored andcommitted
feat: add RetryStrategy class and retryMiddleware implementation (#389)
* feat: add RetryStrategy class and change retryMiddleware interface
1 parent 03d120f commit ff70fac

File tree

5 files changed

+105
-56
lines changed

5 files changed

+105
-56
lines changed

Diff for: packages/retry-middleware/src/defaultStrategy.ts

+71-11
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,79 @@ import {
55
import { defaultDelayDecider } from "./delayDecider";
66
import { defaultRetryDecider } from "./retryDecider";
77
import { isThrottlingError } from "@aws-sdk/service-error-classification";
8-
import { RetryStrategy, SdkError } from "@aws-sdk/types";
8+
import {
9+
SdkError,
10+
FinalizeHandler,
11+
MetadataBearer,
12+
FinalizeHandlerArguments,
13+
RetryStrategy
14+
} from "@aws-sdk/types";
15+
16+
/**
17+
* Determines whether an error is retryable based on the number of retries
18+
* already attempted, the HTTP status code, and the error received (if any).
19+
*
20+
* @param error The error encountered.
21+
*/
22+
export interface RetryDecider {
23+
(error: SdkError): boolean;
24+
}
25+
26+
/**
27+
* Determines the number of milliseconds to wait before retrying an action.
28+
*
29+
* @param delayBase The base delay (in milliseconds).
30+
* @param attempts The number of times the action has already been tried.
31+
*/
32+
export interface DelayDecider {
33+
(delayBase: number, attempts: number): number;
34+
}
935

1036
export class ExponentialBackOffStrategy implements RetryStrategy {
11-
constructor(public readonly maxRetries: number) {}
12-
shouldRetry(error: SdkError, retryAttempted: number) {
13-
return retryAttempted < this.maxRetries && defaultRetryDecider(error);
37+
constructor(
38+
public readonly maxRetries: number,
39+
private retryDecider: RetryDecider = defaultRetryDecider,
40+
private delayDecider: DelayDecider = defaultDelayDecider
41+
) {}
42+
private shouldRetry(error: SdkError, retryAttempted: number) {
43+
return retryAttempted < this.maxRetries && this.retryDecider(error);
1444
}
15-
computeDelayBeforeNextRetry(error: SdkError, retryAttempted: number): number {
16-
return defaultDelayDecider(
17-
isThrottlingError(error)
18-
? THROTTLING_RETRY_DELAY_BASE
19-
: DEFAULT_RETRY_DELAY_BASE,
20-
retryAttempted
21-
);
45+
46+
async retry<Input extends object, Ouput extends MetadataBearer>(
47+
next: FinalizeHandler<Input, Ouput>,
48+
args: FinalizeHandlerArguments<Input>
49+
) {
50+
let retries = 0;
51+
let totalDelay = 0;
52+
while (true) {
53+
try {
54+
const { response, output } = await next(args);
55+
output.$metadata.retries = retries;
56+
output.$metadata.totalRetryDelay = totalDelay;
57+
58+
return { response, output };
59+
} catch (err) {
60+
if (this.shouldRetry(err as SdkError, retries)) {
61+
const delay = this.delayDecider(
62+
isThrottlingError(err)
63+
? THROTTLING_RETRY_DELAY_BASE
64+
: DEFAULT_RETRY_DELAY_BASE,
65+
retries++
66+
);
67+
totalDelay += delay;
68+
69+
await new Promise(resolve => setTimeout(resolve, delay));
70+
continue;
71+
}
72+
73+
if (!err.$metadata) {
74+
err.$metadata = {};
75+
}
76+
77+
err.$metadata.retries = retries;
78+
err.$metadata.totalRetryDelay = totalDelay;
79+
throw err;
80+
}
81+
}
2282
}
2383
}

Diff for: packages/retry-middleware/src/retryMiddleware.spec.ts renamed to packages/retry-middleware/src/index.spec.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
import { retryMiddleware } from "./retryMiddleware";
66
import { RetryConfig } from "./configurations";
77
import * as delayDeciderModule from "./delayDecider";
8-
import { ExponentialBackOffStrategy } from "./defaultStrategy";
8+
import { ExponentialBackOffStrategy, RetryDecider } from "./defaultStrategy";
99
import { HttpRequest } from "@aws-sdk/protocol-http";
10+
import { SdkError } from '@aws-sdk/types';
1011

1112
describe("retryMiddleware", () => {
1213
it("should not retry when the handler completes successfully", async () => {
@@ -73,8 +74,8 @@ describe("retryMiddleware", () => {
7374
delayDeciderModule,
7475
"defaultDelayDecider"
7576
);
76-
const strategy = new ExponentialBackOffStrategy(maxRetries);
77-
strategy.shouldRetry = () => true;
77+
const retryDecider: RetryDecider = (error: SdkError) => true;
78+
const strategy = new ExponentialBackOffStrategy(maxRetries, retryDecider);
7879
const retryHandler = retryMiddleware({
7980
maxRetries,
8081
retryStrategy: strategy

Diff for: packages/retry-middleware/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from "./retryMiddleware";
22
export * from "./defaultStrategy";
33
export * from "./configurations";
4+
export * from "./delayDecider";
5+
export * from "./retryDecider";

Diff for: packages/retry-middleware/src/retryMiddleware.ts

+5-37
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,18 @@ import {
33
FinalizeHandlerArguments,
44
MetadataBearer,
55
FinalizeHandlerOutput,
6-
SdkError,
76
InjectableMiddleware
87
} from "@aws-sdk/types";
98
import { RetryConfig } from "./configurations";
109

1110
export function retryMiddleware(options: RetryConfig.Resolved) {
1211
return <Output extends MetadataBearer = MetadataBearer>(
1312
next: FinalizeHandler<any, Output>
14-
): FinalizeHandler<any, Output> =>
15-
async function retry(
16-
args: FinalizeHandlerArguments<any>
17-
): Promise<FinalizeHandlerOutput<Output>> {
18-
let retries = 0;
19-
let totalDelay = 0;
20-
while (true) {
21-
try {
22-
const { response, output } = await next(args);
23-
output.$metadata.retries = retries;
24-
output.$metadata.totalRetryDelay = totalDelay;
25-
26-
return { response, output };
27-
} catch (err) {
28-
if (options.retryStrategy.shouldRetry(err as SdkError, retries)) {
29-
const delay = options.retryStrategy.computeDelayBeforeNextRetry(
30-
err,
31-
retries
32-
);
33-
retries++;
34-
totalDelay += delay;
35-
36-
await new Promise(resolve => setTimeout(resolve, delay));
37-
continue;
38-
}
39-
40-
if (!err.$metadata) {
41-
err.$metadata = {};
42-
}
43-
44-
err.$metadata.retries = retries;
45-
err.$metadata.totalRetryDelay = totalDelay;
46-
throw err;
47-
}
48-
}
49-
};
13+
): FinalizeHandler<any, Output> => async (
14+
args: FinalizeHandlerArguments<any>
15+
): Promise<FinalizeHandlerOutput<Output>> => {
16+
return options.retryStrategy.retry(next, args);
17+
};
5018
}
5119

5220
export function retryPlugin<

Diff for: packages/types/src/util.ts

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { HttpEndpoint } from "./http";
2+
import {
3+
FinalizeHandler,
4+
FinalizeHandlerArguments,
5+
FinalizeHandlerOutput
6+
} from "./middleware";
7+
import { MetadataBearer } from "./response";
28

39
/**
410
* A function that, given a TypedArray of bytes, can produce a string
@@ -50,12 +56,24 @@ export interface BodyLengthCalculator {
5056
// TODO Unify with the types created for the error parsers
5157
export type SdkError = Error & { connectionError?: boolean };
5258

59+
/**
60+
* Interface that specifies the retry behavior
61+
*/
5362
export interface RetryStrategy {
54-
shouldRetry: (error: SdkError, retryAttempted: number) => boolean;
55-
computeDelayBeforeNextRetry: (
56-
error: SdkError,
57-
retryAttempted: number
58-
) => number;
63+
/**
64+
* the maximum number of times requests that encounter potentially
65+
* transient failures should be retried
66+
*/
67+
maxRetries: number;
68+
/**
69+
* the retry behavior the will invoke the next handler and handle the retry accordingly.
70+
* This function should also update the $metadata from the response accordingly.
71+
* @see {@link ResponseMetadata}
72+
*/
73+
retry: <Input extends object, Output extends MetadataBearer>(
74+
next: FinalizeHandler<Input, Output>,
75+
args: FinalizeHandlerArguments<Input>
76+
) => Promise<FinalizeHandlerOutput<Output>>;
5977
}
6078

6179
/**

0 commit comments

Comments
 (0)