Skip to content

Commit 9d224e7

Browse files
authored
feat: retry if retryable trait is set (#1238)
1 parent 7e7d3c8 commit 9d224e7

File tree

13 files changed

+97
-18
lines changed

13 files changed

+97
-18
lines changed

Diff for: packages/middleware-retry/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
},
2121
"devDependencies": {
2222
"@aws-sdk/protocol-http": "1.0.0-gamma.1",
23+
"@aws-sdk/smithy-client": "1.0.0-gamma.1",
2324
"@types/jest": "^25.1.4",
2425
"jest": "^25.1.0",
2526
"typescript": "~3.8.3"

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
import { defaultDelayDecider } from "./delayDecider";
66
import { defaultRetryDecider } from "./retryDecider";
77
import { isThrottlingError } from "@aws-sdk/service-error-classification";
8+
import { SdkError } from "@aws-sdk/smithy-client";
89
import {
9-
SdkError,
1010
FinalizeHandler,
1111
MetadataBearer,
1212
FinalizeHandlerArguments,

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { resolveRetryConfig } from "./configurations";
77
import * as delayDeciderModule from "./delayDecider";
88
import { ExponentialBackOffStrategy, RetryDecider } from "./defaultStrategy";
99
import { HttpRequest } from "@aws-sdk/protocol-http";
10-
import { SdkError } from "@aws-sdk/types";
10+
import { SdkError } from "@aws-sdk/smithy-client";
1111

1212
describe("retryMiddleware", () => {
1313
it("should not retry when the handler completes successfully", async () => {

Diff for: packages/middleware-retry/src/retryDecider.spec.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
import {
2+
isRetryableByTrait,
23
isClockSkewError,
34
isThrottlingError,
45
isTransientError
56
} from "@aws-sdk/service-error-classification";
67
import { defaultRetryDecider } from "./retryDecider";
8+
import { SdkError } from "@aws-sdk/smithy-client";
79

810
jest.mock("@aws-sdk/service-error-classification", () => ({
11+
isRetryableByTrait: jest.fn().mockReturnValue(false),
912
isClockSkewError: jest.fn().mockReturnValue(false),
1013
isThrottlingError: jest.fn().mockReturnValue(false),
1114
isTransientError: jest.fn().mockReturnValue(false)
1215
}));
1316

1417
describe("defaultRetryDecider", () => {
15-
const createMockError = () => Object.assign(new Error(), { $metadata: {} });
18+
const createMockError = () =>
19+
Object.assign(new Error(), { $metadata: {} }) as SdkError;
1620

1721
beforeEach(() => {
1822
jest.clearAllMocks();
1923
});
2024

2125
it("should return false when the provided error is falsy", () => {
2226
expect(defaultRetryDecider(null as any)).toBe(false);
27+
expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(0);
28+
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(0);
29+
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(0);
30+
expect((isTransientError as jest.Mock).mock.calls.length).toBe(0);
31+
});
32+
33+
it("should return true for RetryableByTrait error", () => {
34+
(isRetryableByTrait as jest.Mock).mockReturnValueOnce(true);
35+
expect(defaultRetryDecider(createMockError())).toBe(true);
36+
expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1);
2337
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(0);
2438
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(0);
2539
expect((isTransientError as jest.Mock).mock.calls.length).toBe(0);
@@ -28,6 +42,7 @@ describe("defaultRetryDecider", () => {
2842
it("should return true for ClockSkewError", () => {
2943
(isClockSkewError as jest.Mock).mockReturnValueOnce(true);
3044
expect(defaultRetryDecider(createMockError())).toBe(true);
45+
expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1);
3146
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1);
3247
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(0);
3348
expect((isTransientError as jest.Mock).mock.calls.length).toBe(0);
@@ -36,6 +51,7 @@ describe("defaultRetryDecider", () => {
3651
it("should return true for ThrottlingError", () => {
3752
(isThrottlingError as jest.Mock).mockReturnValueOnce(true);
3853
expect(defaultRetryDecider(createMockError())).toBe(true);
54+
expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1);
3955
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1);
4056
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1);
4157
expect((isTransientError as jest.Mock).mock.calls.length).toBe(0);
@@ -44,13 +60,15 @@ describe("defaultRetryDecider", () => {
4460
it("should return true for TransientError", () => {
4561
(isTransientError as jest.Mock).mockReturnValueOnce(true);
4662
expect(defaultRetryDecider(createMockError())).toBe(true);
63+
expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1);
4764
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1);
4865
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1);
4966
expect((isTransientError as jest.Mock).mock.calls.length).toBe(1);
5067
});
5168

5269
it("should return false for other errors", () => {
5370
expect(defaultRetryDecider(createMockError())).toBe(false);
71+
expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1);
5472
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1);
5573
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1);
5674
expect((isTransientError as jest.Mock).mock.calls.length).toBe(1);

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import {
22
isClockSkewError,
3+
isRetryableByTrait,
34
isThrottlingError,
45
isTransientError
56
} from "@aws-sdk/service-error-classification";
6-
import { SdkError } from "@aws-sdk/types";
7+
import { SdkError } from "@aws-sdk/smithy-client";
78

89
export const defaultRetryDecider = (error: SdkError) => {
910
if (!error) {
1011
return false;
1112
}
1213

1314
return (
15+
isRetryableByTrait(error) ||
1416
isClockSkewError(error) ||
1517
isThrottlingError(error) ||
1618
isTransientError(error)

Diff for: packages/service-error-classification/package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313
"url": "https://aws.amazon.com/javascript/"
1414
},
1515
"license": "Apache-2.0",
16-
"dependencies": {
17-
"@aws-sdk/types": "1.0.0-gamma.1"
18-
},
1916
"devDependencies": {
17+
"@aws-sdk/smithy-client": "1.0.0-gamma.1",
2018
"@types/jest": "^25.1.4",
2119
"jest": "^25.1.0",
2220
"typescript": "~3.8.3"

Diff for: packages/service-error-classification/src/index.spec.ts

+42-6
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,43 @@ import {
44
TRANSIENT_ERROR_CODES,
55
TRANSIENT_ERROR_STATUS_CODES
66
} from "./constants";
7-
import { isClockSkewError, isThrottlingError, isTransientError } from "./index";
8-
import { SdkError } from "@aws-sdk/types";
7+
import {
8+
isRetryableByTrait,
9+
isClockSkewError,
10+
isThrottlingError,
11+
isTransientError
12+
} from "./index";
13+
import { SdkError, RetryableTrait } from "@aws-sdk/smithy-client";
914

1015
const checkForErrorType = (
1116
isErrorTypeFunc: (error: SdkError) => boolean,
12-
options: { name?: string; httpStatusCode?: number },
17+
options: {
18+
name?: string;
19+
httpStatusCode?: number;
20+
$retryable?: RetryableTrait;
21+
},
1322
errorTypeResult: boolean
1423
) => {
15-
const { name, httpStatusCode } = options;
24+
const { name, httpStatusCode, $retryable } = options;
1625
const error = Object.assign(new Error(), {
1726
name,
18-
$metadata: { httpStatusCode }
27+
$metadata: { httpStatusCode },
28+
$retryable
1929
});
20-
expect(isErrorTypeFunc(error)).toBe(errorTypeResult);
30+
expect(isErrorTypeFunc(error as SdkError)).toBe(errorTypeResult);
2131
};
2232

33+
describe("isRetryableByTrait", () => {
34+
it("should declare error with $retryable set to be a Retryable by trait", () => {
35+
const $retryable = {};
36+
checkForErrorType(isRetryableByTrait, { $retryable }, true);
37+
});
38+
39+
it("should not declare error with $retryable not set to be a Retryable by trait", () => {
40+
checkForErrorType(isRetryableByTrait, {}, false);
41+
});
42+
});
43+
2344
describe("isClockSkewError", () => {
2445
CLOCK_SKEW_ERROR_CODES.forEach(name => {
2546
it(`should declare error with the name "${name}" to be a ClockSkew error`, () => {
@@ -54,6 +75,21 @@ describe("isThrottlingError", () => {
5475
break;
5576
}
5677
}
78+
79+
it("should declare error with $retryable.throttling set to true to be a Throttling error", () => {
80+
const $retryable = { throttling: true };
81+
checkForErrorType(isThrottlingError, { $retryable }, true);
82+
});
83+
84+
it("should not declare error with $retryable.throttling set to false to be a Throttling error", () => {
85+
const $retryable = { throttling: false };
86+
checkForErrorType(isThrottlingError, { $retryable }, false);
87+
});
88+
89+
it("should not declare error with $retryable.throttling not set to be a Throttling error", () => {
90+
const $retryable = {};
91+
checkForErrorType(isThrottlingError, { $retryable }, false);
92+
});
5793
});
5894

5995
describe("isTransientError", () => {

Diff for: packages/service-error-classification/src/index.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import {
44
TRANSIENT_ERROR_CODES,
55
TRANSIENT_ERROR_STATUS_CODES
66
} from "./constants";
7-
import { SdkError } from "@aws-sdk/types";
7+
import { SdkError } from "@aws-sdk/smithy-client";
8+
9+
export const isRetryableByTrait = (error: SdkError) =>
10+
error.$retryable !== undefined;
811

912
export const isClockSkewError = (error: SdkError) =>
1013
CLOCK_SKEW_ERROR_CODES.includes(error.name);
1114

1215
export const isThrottlingError = (error: SdkError) =>
13-
THROTTLING_ERROR_CODES.includes(error.name);
16+
THROTTLING_ERROR_CODES.includes(error.name) ||
17+
error.$retryable?.throttling == true;
1418

1519
export const isTransientError = (error: SdkError) =>
1620
TRANSIENT_ERROR_CODES.includes(error.name) ||

Diff for: packages/smithy-client/src/exception.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { RetryableTrait } from "./retryable-trait";
2+
13
/**
24
* Type that is implemented by all Smithy shapes marked with the
35
* error trait.
@@ -17,4 +19,9 @@ export interface SmithyException {
1719
* The service that encountered the exception.
1820
*/
1921
readonly $service?: string;
22+
23+
/**
24+
* Indicates that an error MAY be retried by the client.
25+
*/
26+
readonly $retryable?: RetryableTrait;
2027
}

Diff for: packages/smithy-client/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export * from "./lazy-json";
1010
export * from "./date-utils";
1111
export * from "./split-every";
1212
export * from "./constants";
13+
export * from "./retryable-trait";
14+
export * from "./sdk-error";

Diff for: packages/smithy-client/src/retryable-trait.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* A structure shape with the error trait.
3+
* https://awslabs.github.io/smithy/spec/core.html#retryable-trait
4+
*/
5+
export interface RetryableTrait {
6+
/**
7+
* Indicates that the error is a retryable throttling error.
8+
*/
9+
readonly throttling?: boolean;
10+
}

Diff for: packages/smithy-client/src/sdk-error.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { SmithyException } from "./exception";
2+
import { MetadataBearer } from "@aws-sdk/types";
3+
4+
export type SdkError = Error & SmithyException & MetadataBearer;

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

-3
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ export interface BodyLengthCalculator {
5353
(body: any): number | undefined;
5454
}
5555

56-
// TODO Unify with the types created for the error parsers
57-
export type SdkError = Error & MetadataBearer;
58-
5956
/**
6057
* Interface that specifies the retry behavior
6158
*/

0 commit comments

Comments
 (0)