Skip to content

Commit

Permalink
feat: read maxAttempts value from retry-config (#1286)
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr authored Jun 23, 2020
1 parent 9e2614e commit 8f3fdc0
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 83 deletions.
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

0 comments on commit 8f3fdc0

Please sign in to comment.