Skip to content

feat: read maxAttempts value from retry-config #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 57 additions & 20 deletions packages/middleware-retry/src/configurations.spec.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,83 @@
import { resolveRetryConfig } from "./configurations";
import { StandardRetryStrategy } from "./defaultStrategy";

jest.mock("./defaultStrategy", () => ({
StandardRetryStrategy: jest.fn().mockReturnValue({})
}));

describe("resolveRetryConfig", () => {
const maxAttemptsDefaultProvider = jest.fn();

afterEach(() => {
jest.clearAllMocks();
});

describe("maxAttempts", () => {
it("uses passed maxAttempts value if present", () => {
[1, 2, 3].forEach(maxAttempts => {
expect(resolveRetryConfig({ maxAttempts }).maxAttempts).toEqual(
maxAttempts
);
});
it("assigns maxAttempts value if present", async () => {
for (const maxAttempts of [1, 2, 3]) {
const output = await resolveRetryConfig({
maxAttempts,
maxAttemptsDefaultProvider
}).maxAttempts();
expect(output).toStrictEqual(maxAttempts.toString());
expect(maxAttemptsDefaultProvider).not.toHaveBeenCalled();
}
});

it("assigns default value of 3 if maxAttempts not passed", () => {
expect(resolveRetryConfig({}).maxAttempts).toEqual(3);
it("assigns maxAttemptsDefaultProvider if maxAttempts not present", () => {
const mockMaxAttempts = jest.fn();
maxAttemptsDefaultProvider.mockReturnValueOnce(mockMaxAttempts);

const input = { maxAttemptsDefaultProvider };
expect(resolveRetryConfig(input).maxAttempts).toStrictEqual(
mockMaxAttempts
);

expect(maxAttemptsDefaultProvider).toHaveBeenCalledTimes(1);
expect(maxAttemptsDefaultProvider).toHaveBeenCalledWith(input);
});
});

describe("retryStrategy", () => {
it("uses passed retryStrategy if present", () => {
it("passes retryStrategy if present", () => {
const mockRetryStrategy = {
maxAttempts: 2,
retry: jest.fn()
};
const { retryStrategy } = resolveRetryConfig({
retryStrategy: mockRetryStrategy
retryStrategy: mockRetryStrategy,
maxAttemptsDefaultProvider
});
expect(retryStrategy).toEqual(mockRetryStrategy);
});

describe("creates StandardRetryStrategy if retryStrategy not present", () => {
describe("uses maxAttempts if present", () => {
[1, 2, 3].forEach(maxAttempts => {
const { retryStrategy } = resolveRetryConfig({ maxAttempts });
expect(retryStrategy).toBeInstanceOf(StandardRetryStrategy);
expect(retryStrategy.maxAttempts).toBe(maxAttempts);
});
describe("passes maxAttempts if present", () => {
for (const maxAttempts of [1, 2, 3]) {
it(`when maxAttempts=${maxAttempts}`, async () => {
const { retryStrategy } = resolveRetryConfig({
maxAttempts,
maxAttemptsDefaultProvider
});
expect(retryStrategy).toBeInstanceOf(StandardRetryStrategy);
expect(StandardRetryStrategy as jest.Mock).toHaveBeenCalledTimes(1);
const output = await (StandardRetryStrategy as jest.Mock).mock.calls[0][0]();
expect(output).toStrictEqual(maxAttempts.toString());
});
}
});

it("uses default 3 if maxAttempts is not present", () => {
const { retryStrategy } = resolveRetryConfig({});
it("passes maxAttemptsDefaultProvider if maxAttempts is not present", () => {
const mockMaxAttempts = jest.fn();
maxAttemptsDefaultProvider.mockReturnValueOnce(mockMaxAttempts);

const { retryStrategy } = resolveRetryConfig({
maxAttemptsDefaultProvider
});
expect(retryStrategy).toBeInstanceOf(StandardRetryStrategy);
expect(retryStrategy.maxAttempts).toBe(3);
expect(StandardRetryStrategy as jest.Mock).toHaveBeenCalledTimes(1);
expect((StandardRetryStrategy as jest.Mock).mock.calls[0][0]).toEqual(
mockMaxAttempts
);
});
});
});
Expand Down
22 changes: 18 additions & 4 deletions packages/middleware-retry/src/configurations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RetryStrategy } from "@aws-sdk/types";
import { RetryStrategy, Provider } from "@aws-sdk/types";
import { StandardRetryStrategy } from "./defaultStrategy";

export interface RetryInputConfig {
Expand All @@ -12,18 +12,32 @@ export interface RetryInputConfig {
retryStrategy?: RetryStrategy;
}

interface PreviouslyResolved {
maxAttemptsDefaultProvider: (input: any) => Provider<string>;
}
export interface RetryResolvedConfig {
maxAttempts: number;
maxAttempts: Provider<string>;
retryStrategy: RetryStrategy;
}

export const resolveRetryConfig = <T>(
input: T & RetryInputConfig
input: T & PreviouslyResolved & RetryInputConfig
): T & RetryResolvedConfig => {
const maxAttempts = input.maxAttempts ?? 3;
const maxAttempts =
normalizeMaxAttempts(input.maxAttempts) ??
input.maxAttemptsDefaultProvider(input as any);
return {
...input,
maxAttempts,
retryStrategy: input.retryStrategy || new StandardRetryStrategy(maxAttempts)
};
};

const normalizeMaxAttempts = (
maxAttempts?: number
): Provider<string> | undefined => {
if (maxAttempts) {
const promisified = Promise.resolve(maxAttempts.toString());
return () => promisified;
}
};
138 changes: 113 additions & 25 deletions packages/middleware-retry/src/defaultStrategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ describe("defaultStrategy", () => {
output: { $metadata: {} }
});

const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
return retryStrategy.retry(next, { request: { headers: {} } } as any);
};

Expand All @@ -66,7 +68,9 @@ describe("defaultStrategy", () => {
const mockError = options?.mockError ?? new Error("mockError");
next = jest.fn().mockRejectedValue(mockError);

const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
try {
await retryStrategy.retry(next, { request: { headers: {} } } as any);
} catch (error) {
Expand All @@ -90,7 +94,9 @@ describe("defaultStrategy", () => {
.mockRejectedValueOnce(mockError)
.mockResolvedValueOnce(mockResponse);

const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
return retryStrategy.retry(next, { request: { headers: {} } } as any);
};

Expand All @@ -110,81 +116,109 @@ describe("defaultStrategy", () => {
.mockRejectedValueOnce(mockError)
.mockResolvedValueOnce(mockResponse);

const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
return retryStrategy.retry(next, { request: { headers: {} } } as any);
};

afterEach(() => {
jest.clearAllMocks();
});

it("sets maxAttempts as class member variable", () => {
[1, 2, 3].forEach(maxAttempts => {
const retryStrategy = new StandardRetryStrategy(maxAttempts);
expect(retryStrategy.maxAttempts).toBe(maxAttempts);
it("sets maxAttemptsProvider as class member variable", () => {
["1", "2", "3"].forEach(maxAttempts => {
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts)
);
expect(retryStrategy["maxAttemptsProvider"]()).resolves.toBe(maxAttempts);
});
});

describe("retryDecider init", () => {
it("sets defaultRetryDecider if options is undefined", () => {
const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
expect(retryStrategy["retryDecider"]).toBe(defaultRetryDecider);
});

it("sets defaultRetryDecider if options.retryDecider is undefined", () => {
const retryStrategy = new StandardRetryStrategy(maxAttempts, {});
const retryStrategy = new StandardRetryStrategy(
() => Promise.resolve(maxAttempts.toString()),
{}
);
expect(retryStrategy["retryDecider"]).toBe(defaultRetryDecider);
});

it("sets options.retryDecider if defined", () => {
const retryDecider = jest.fn();
const retryStrategy = new StandardRetryStrategy(maxAttempts, {
retryDecider
});
const retryStrategy = new StandardRetryStrategy(
() => Promise.resolve(maxAttempts.toString()),
{
retryDecider
}
);
expect(retryStrategy["retryDecider"]).toBe(retryDecider);
});
});

describe("delayDecider init", () => {
it("sets defaultDelayDecider if options is undefined", () => {
const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
expect(retryStrategy["delayDecider"]).toBe(defaultDelayDecider);
});

it("sets defaultDelayDecider if options.delayDecider undefined", () => {
const retryStrategy = new StandardRetryStrategy(maxAttempts, {});
const retryStrategy = new StandardRetryStrategy(
() => Promise.resolve(maxAttempts.toString()),
{}
);
expect(retryStrategy["delayDecider"]).toBe(defaultDelayDecider);
});

it("sets options.delayDecider if defined", () => {
const delayDecider = jest.fn();
const retryStrategy = new StandardRetryStrategy(maxAttempts, {
delayDecider
});
const retryStrategy = new StandardRetryStrategy(
() => Promise.resolve(maxAttempts.toString()),
{
delayDecider
}
);
expect(retryStrategy["delayDecider"]).toBe(delayDecider);
});
});

describe("retryQuota init", () => {
it("sets getDefaultRetryQuota if options is undefined", () => {
const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
expect(retryStrategy["retryQuota"]).toBe(
getDefaultRetryQuota(INITIAL_RETRY_TOKENS)
);
});

it("sets getDefaultRetryQuota if options.delayDecider undefined", () => {
const retryStrategy = new StandardRetryStrategy(maxAttempts, {});
const retryStrategy = new StandardRetryStrategy(
() => Promise.resolve(maxAttempts.toString()),
{}
);
expect(retryStrategy["retryQuota"]).toBe(
getDefaultRetryQuota(INITIAL_RETRY_TOKENS)
);
});

it("sets options.retryQuota if defined", () => {
const retryQuota = {} as RetryQuota;
const retryStrategy = new StandardRetryStrategy(maxAttempts, {
retryQuota
});
const retryStrategy = new StandardRetryStrategy(
() => Promise.resolve(maxAttempts.toString()),
{
retryQuota
}
);
expect(retryStrategy["retryQuota"]).toBe(retryQuota);
});
});
Expand Down Expand Up @@ -483,7 +517,9 @@ describe("defaultStrategy", () => {
output: { $metadata: {} }
});

const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
await retryStrategy.retry(next, { request: { headers: {} } } as any);
await retryStrategy.retry(next, { request: { headers: {} } } as any);

Expand Down Expand Up @@ -565,7 +601,9 @@ describe("defaultStrategy", () => {
throw mockError;
});

const retryStrategy = new StandardRetryStrategy(maxAttempts);
const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve(maxAttempts.toString())
);
try {
await retryStrategy.retry(next, { request: { headers: {} } } as any);
} catch (error) {
Expand All @@ -577,4 +615,54 @@ describe("defaultStrategy", () => {
((isInstance as unknown) as jest.Mock).mockReturnValue(false);
});
});

describe("defaults maxAttempts to 3", () => {
it("when maxAttemptsProvider throws error", async () => {
const maxAttempts = 3;
const { isInstance } = HttpRequest;
((isInstance as unknown) as jest.Mock).mockReturnValue(true);

next = jest.fn(args => {
expect(args.request.headers["amz-sdk-request"]).toBe(
`attempt=1; max=${maxAttempts}`
);
return Promise.resolve({
response: "mockResponse",
output: { $metadata: {} }
});
});

const retryStrategy = new StandardRetryStrategy(() =>
Promise.reject("ERROR")
);
await retryStrategy.retry(next, { request: { headers: {} } } as any);

expect(next).toHaveBeenCalledTimes(1);
((isInstance as unknown) as jest.Mock).mockReturnValue(false);
});

it("when parseInt fails on maxAttemptsProvider", async () => {
const maxAttempts = 3;
const { isInstance } = HttpRequest;
((isInstance as unknown) as jest.Mock).mockReturnValue(true);

next = jest.fn(args => {
expect(args.request.headers["amz-sdk-request"]).toBe(
`attempt=1; max=${maxAttempts}`
);
return Promise.resolve({
response: "mockResponse",
output: { $metadata: {} }
});
});

const retryStrategy = new StandardRetryStrategy(() =>
Promise.resolve("not-a-number")
);
await retryStrategy.retry(next, { request: { headers: {} } } as any);

expect(next).toHaveBeenCalledTimes(1);
((isInstance as unknown) as jest.Mock).mockReturnValue(false);
});
});
});
Loading