Skip to content

Commit 095b990

Browse files
authored
feat(middleware-retry): call retry strategy based on value in retryMode (#2456)
1 parent 6d1a555 commit 095b990

File tree

4 files changed

+120
-29
lines changed

4 files changed

+120
-29
lines changed

packages/middleware-retry/src/configurations.spec.ts

+79-12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AdaptiveRetryStrategy } from "./AdaptiveRetryStrategy";
12
import { DEFAULT_MAX_ATTEMPTS } from "./config";
23
import {
34
CONFIG_MAX_ATTEMPTS,
@@ -7,17 +8,19 @@ import {
78
} from "./configurations";
89
import { StandardRetryStrategy } from "./StandardRetryStrategy";
910

11+
jest.mock("./AdaptiveRetryStrategy");
1012
jest.mock("./StandardRetryStrategy");
1113

12-
describe("resolveRetryConfig", () => {
14+
describe(resolveRetryConfig.name, () => {
15+
const retryModeProvider = jest.fn();
1316
afterEach(() => {
1417
jest.clearAllMocks();
1518
});
1619

1720
describe("maxAttempts", () => {
1821
it("assigns maxAttempts value if present", async () => {
1922
for (const maxAttempts of [1, 2, 3]) {
20-
const output = await resolveRetryConfig({ maxAttempts }).maxAttempts();
23+
const output = await resolveRetryConfig({ maxAttempts, retryModeProvider }).maxAttempts();
2124
expect(output).toStrictEqual(maxAttempts);
2225
}
2326
});
@@ -29,21 +32,85 @@ describe("resolveRetryConfig", () => {
2932
retry: jest.fn(),
3033
};
3134
const { retryStrategy } = resolveRetryConfig({
35+
retryModeProvider,
3236
retryStrategy: mockRetryStrategy,
3337
});
34-
expect(retryStrategy).toEqual(mockRetryStrategy);
38+
expect(retryStrategy()).resolves.toEqual(mockRetryStrategy);
3539
});
3640

37-
describe("creates StandardRetryStrategy if retryStrategy not present", () => {
38-
describe("passes maxAttempts if present", () => {
39-
for (const maxAttempts of [1, 2, 3]) {
40-
it(`when maxAttempts=${maxAttempts}`, async () => {
41-
resolveRetryConfig({ maxAttempts });
42-
expect(StandardRetryStrategy as jest.Mock).toHaveBeenCalledTimes(1);
43-
const output = await (StandardRetryStrategy as jest.Mock).mock.calls[0][0]();
44-
expect(output).toStrictEqual(maxAttempts);
41+
describe("creates RetryStrategy if retryStrategy not present", () => {
42+
describe("StandardRetryStrategy", () => {
43+
describe("when retryMode=standard", () => {
44+
describe("passes maxAttempts if present", () => {
45+
const retryMode = "standard";
46+
for (const maxAttempts of [1, 2, 3]) {
47+
it(`when maxAttempts=${maxAttempts}`, async () => {
48+
const { retryStrategy } = resolveRetryConfig({ maxAttempts, retryMode, retryModeProvider });
49+
await retryStrategy();
50+
expect(StandardRetryStrategy as jest.Mock).toHaveBeenCalledTimes(1);
51+
expect(AdaptiveRetryStrategy as jest.Mock).not.toHaveBeenCalled();
52+
const output = await (StandardRetryStrategy as jest.Mock).mock.calls[0][0]();
53+
expect(output).toStrictEqual(maxAttempts);
54+
});
55+
}
4556
});
46-
}
57+
});
58+
59+
describe("when retryModeProvider returns 'standard'", () => {
60+
describe("passes maxAttempts if present", () => {
61+
beforeEach(() => {
62+
retryModeProvider.mockResolvedValueOnce("standard");
63+
});
64+
for (const maxAttempts of [1, 2, 3]) {
65+
it(`when maxAttempts=${maxAttempts}`, async () => {
66+
const { retryStrategy } = resolveRetryConfig({ maxAttempts, retryModeProvider });
67+
await retryStrategy();
68+
expect(retryModeProvider).toHaveBeenCalledTimes(1);
69+
expect(StandardRetryStrategy as jest.Mock).toHaveBeenCalledTimes(1);
70+
expect(AdaptiveRetryStrategy as jest.Mock).not.toHaveBeenCalled();
71+
const output = await (StandardRetryStrategy as jest.Mock).mock.calls[0][0]();
72+
expect(output).toStrictEqual(maxAttempts);
73+
});
74+
}
75+
});
76+
});
77+
});
78+
79+
describe("AdaptiveRetryStrategy", () => {
80+
describe("when retryMode=adaptive", () => {
81+
describe("passes maxAttempts if present", () => {
82+
const retryMode = "adaptive";
83+
for (const maxAttempts of [1, 2, 3]) {
84+
it(`when maxAttempts=${maxAttempts}`, async () => {
85+
const { retryStrategy } = resolveRetryConfig({ maxAttempts, retryMode, retryModeProvider });
86+
await retryStrategy();
87+
expect(StandardRetryStrategy as jest.Mock).not.toHaveBeenCalled();
88+
expect(AdaptiveRetryStrategy as jest.Mock).toHaveBeenCalledTimes(1);
89+
const output = await (AdaptiveRetryStrategy as jest.Mock).mock.calls[0][0]();
90+
expect(output).toStrictEqual(maxAttempts);
91+
});
92+
}
93+
});
94+
});
95+
96+
describe("when retryModeProvider returns 'adaptive'", () => {
97+
describe("passes maxAttempts if present", () => {
98+
beforeEach(() => {
99+
retryModeProvider.mockResolvedValueOnce("adaptive");
100+
});
101+
for (const maxAttempts of [1, 2, 3]) {
102+
it(`when maxAttempts=${maxAttempts}`, async () => {
103+
const { retryStrategy } = resolveRetryConfig({ maxAttempts, retryModeProvider });
104+
await retryStrategy();
105+
expect(retryModeProvider).toHaveBeenCalledTimes(1);
106+
expect(StandardRetryStrategy as jest.Mock).not.toHaveBeenCalled();
107+
expect(AdaptiveRetryStrategy as jest.Mock).toHaveBeenCalledTimes(1);
108+
const output = await (AdaptiveRetryStrategy as jest.Mock).mock.calls[0][0]();
109+
expect(output).toStrictEqual(maxAttempts);
110+
});
111+
}
112+
});
113+
});
47114
});
48115
});
49116
});

packages/middleware-retry/src/configurations.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { LoadedConfigSelectors } from "@aws-sdk/node-config-provider";
22
import { Provider, RetryStrategy } from "@aws-sdk/types";
33

4-
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_RETRY_MODE } from "./config";
4+
import { AdaptiveRetryStrategy } from "./AdaptiveRetryStrategy";
5+
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_RETRY_MODE, RETRY_MODES } from "./config";
56
import { StandardRetryStrategy } from "./StandardRetryStrategy";
67

78
export const ENV_MAX_ATTEMPTS = "AWS_MAX_ATTEMPTS";
@@ -38,9 +39,20 @@ export interface RetryInputConfig {
3839
* The strategy to retry the request. Using built-in exponential backoff strategy by default.
3940
*/
4041
retryStrategy?: RetryStrategy;
42+
/**
43+
* Specifies which retry algorithm to use.
44+
*/
45+
retryMode?: string;
46+
}
47+
48+
interface PreviouslyResolved {
49+
/**
50+
* Specifies provider for retry algorithm to use.
51+
* @internal
52+
*/
53+
retryModeProvider: Provider<string>;
4154
}
4255

43-
interface PreviouslyResolved {}
4456
export interface RetryResolvedConfig {
4557
/**
4658
* Resolved value for input config {@link RetryInputConfig.maxAttempts}
@@ -49,15 +61,24 @@ export interface RetryResolvedConfig {
4961
/**
5062
* Resolved value for input config {@link RetryInputConfig.retryStrategy}
5163
*/
52-
retryStrategy: RetryStrategy;
64+
retryStrategy: Provider<RetryStrategy>;
5365
}
5466

5567
export const resolveRetryConfig = <T>(input: T & PreviouslyResolved & RetryInputConfig): T & RetryResolvedConfig => {
5668
const maxAttempts = normalizeMaxAttempts(input.maxAttempts);
5769
return {
5870
...input,
5971
maxAttempts,
60-
retryStrategy: input.retryStrategy || new StandardRetryStrategy(maxAttempts),
72+
retryStrategy: async () => {
73+
if (input.retryStrategy) {
74+
return input.retryStrategy;
75+
}
76+
const retryMode = input.retryMode || (await input.retryModeProvider());
77+
if (retryMode === RETRY_MODES.ADAPTIVE) {
78+
return new AdaptiveRetryStrategy(maxAttempts);
79+
}
80+
return new StandardRetryStrategy(maxAttempts);
81+
},
6182
};
6283
};
6384

packages/middleware-retry/src/retryMiddleware.spec.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import { FinalizeHandlerArguments, HandlerExecutionContext, MiddlewareStack, Ret
22

33
import { getRetryPlugin, retryMiddleware, retryMiddlewareOptions } from "./retryMiddleware";
44

5-
describe("getRetryPlugin", () => {
5+
describe(getRetryPlugin.name, () => {
66
const mockClientStack = {
77
add: jest.fn(),
88
};
9+
const mockRetryStrategy = {
10+
mode: "mock",
11+
retry: jest.fn(),
12+
};
913

1014
afterEach(() => {
1115
jest.clearAllMocks();
@@ -16,7 +20,7 @@ describe("getRetryPlugin", () => {
1620
it(`when maxAttempts=${maxAttempts}`, () => {
1721
getRetryPlugin({
1822
maxAttempts: () => Promise.resolve(maxAttempts),
19-
retryStrategy: {} as RetryStrategy,
23+
retryStrategy: jest.fn().mockResolvedValue(mockRetryStrategy),
2024
}).applyToStack(mockClientStack as unknown as MiddlewareStack<any, any>);
2125
expect(mockClientStack.add).toHaveBeenCalledTimes(1);
2226
expect(mockClientStack.add.mock.calls[0][1]).toEqual(retryMiddlewareOptions);
@@ -25,7 +29,11 @@ describe("getRetryPlugin", () => {
2529
});
2630
});
2731

28-
describe("retryMiddleware", () => {
32+
describe(retryMiddleware.name, () => {
33+
const mockRetryStrategy = {
34+
mode: "mock",
35+
retry: jest.fn(),
36+
};
2937
afterEach(() => {
3038
jest.clearAllMocks();
3139
});
@@ -36,22 +44,17 @@ describe("retryMiddleware", () => {
3644
const args = {
3745
request: {},
3846
};
39-
const mockRetryStrategy = {
40-
mode: "mock",
41-
maxAttempts,
42-
retry: jest.fn(),
43-
};
4447
const context: HandlerExecutionContext = {};
4548

4649
await retryMiddleware({
4750
maxAttempts: () => Promise.resolve(maxAttempts),
48-
retryStrategy: mockRetryStrategy,
51+
retryStrategy: jest.fn().mockResolvedValue({ ...mockRetryStrategy, maxAttempts }),
4952
})(
5053
next,
5154
context
5255
)(args as FinalizeHandlerArguments<any>);
5356
expect(mockRetryStrategy.retry).toHaveBeenCalledTimes(1);
5457
expect(mockRetryStrategy.retry).toHaveBeenCalledWith(next, args);
55-
expect(context.userAgent).toContainEqual(["cfg/retry-mode", "mock"]);
58+
expect(context.userAgent).toContainEqual(["cfg/retry-mode", mockRetryStrategy.mode]);
5659
});
5760
});

packages/middleware-retry/src/retryMiddleware.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ export const retryMiddleware =
1818
context: HandlerExecutionContext
1919
): FinalizeHandler<any, Output> =>
2020
async (args: FinalizeHandlerArguments<any>): Promise<FinalizeHandlerOutput<Output>> => {
21-
if (options?.retryStrategy?.mode)
22-
context.userAgent = [...(context.userAgent || []), ["cfg/retry-mode", options.retryStrategy.mode]];
23-
return options.retryStrategy.retry(next, args);
21+
const retryStrategy = await options.retryStrategy();
22+
if (retryStrategy?.mode) context.userAgent = [...(context.userAgent || []), ["cfg/retry-mode", retryStrategy.mode]];
23+
return retryStrategy.retry(next, args);
2424
};
2525

2626
export const retryMiddlewareOptions: FinalizeRequestHandlerOptions & AbsoluteLocation = {

0 commit comments

Comments
 (0)