diff --git a/packages/util-waiter/package.json b/packages/util-waiter/package.json index 2ac73d7be55e..494fe7a0fdf6 100644 --- a/packages/util-waiter/package.json +++ b/packages/util-waiter/package.json @@ -4,7 +4,7 @@ "description": "Shared utilities for client waiters for the AWS SDK", "dependencies": { "tslib": "^1.8.0", - "@aws-sdk/abort-controller": "1.0.0-rc.7" + "@aws-sdk/abort-controller": "1.0.0-rc.8" }, "devDependencies": { "@types/jest": "^26.0.4", diff --git a/packages/util-waiter/src/poller.spec.ts b/packages/util-waiter/src/poller.spec.ts index 0508f439b985..aa212cafff4e 100644 --- a/packages/util-waiter/src/poller.spec.ts +++ b/packages/util-waiter/src/poller.spec.ts @@ -26,10 +26,12 @@ describe(runPolling.name, () => { beforeEach(() => { (sleep as jest.Mock).mockResolvedValueOnce(""); + jest.spyOn(global.Math, "random").mockReturnValue(0.5); }); afterEach(() => { jest.clearAllMocks(); + jest.spyOn(global.Math, "random").mockRestore(); }); it("should returns state in case of failure", async () => { @@ -80,12 +82,12 @@ describe(runPolling.name, () => { expect(sleep).toHaveBeenCalled(); expect(sleep).toHaveBeenCalledTimes(7); - expect(sleep).toHaveBeenNthCalledWith(1, 2); - expect(sleep).toHaveBeenNthCalledWith(2, 4); - expect(sleep).toHaveBeenNthCalledWith(3, 8); - expect(sleep).toHaveBeenNthCalledWith(4, 16); - expect(sleep).toHaveBeenNthCalledWith(5, 30); - expect(sleep).toHaveBeenNthCalledWith(6, 30); - expect(sleep).toHaveBeenNthCalledWith(7, 30); + expect(sleep).toHaveBeenNthCalledWith(1, 2); // min delay + expect(sleep).toHaveBeenNthCalledWith(2, 3); // +random() * 2 + expect(sleep).toHaveBeenNthCalledWith(3, 5); // +random() * 4 + expect(sleep).toHaveBeenNthCalledWith(4, 9); // +random() * 8 + expect(sleep).toHaveBeenNthCalledWith(5, 17); // +random() * 16 + expect(sleep).toHaveBeenNthCalledWith(6, 30); // max delay + expect(sleep).toHaveBeenNthCalledWith(7, 30); // max delay }); }); diff --git a/packages/util-waiter/src/poller.ts b/packages/util-waiter/src/poller.ts index 700c58296c01..0c2d26b987fc 100644 --- a/packages/util-waiter/src/poller.ts +++ b/packages/util-waiter/src/poller.ts @@ -1,9 +1,14 @@ import { sleep } from "./utils/sleep"; import { WaiterOptions, WaiterResult, WaiterState } from "./waiter"; -function exponentialBackoff(floor: number, ciel: number, attempt: number): number { - return Math.min(ciel, floor * 2 ** attempt); -} +/** + * Reference: https://github.com/awslabs/smithy/pull/656 + * The theoretical limit to the attempt is max delay cannot be > Number.MAX_VALUE, but it's unlikely because of + * `maxWaitTime` + */ +const exponentialBackoff = (floor: number, ciel: number, attempt: number) => + Math.floor(Math.min(ciel, randomInRange(floor, floor * 2 ** (attempt - 1)))); +const randomInRange = (min: number, max: number) => min + Math.random() * (max - min); /** * Function that runs indefinite polling as part of waiters. @@ -13,22 +18,19 @@ function exponentialBackoff(floor: number, ciel: number, attempt: number): numbe * @param stateChecker function that checks the acceptor states on each poll. */ export const runPolling = async ( - params: WaiterOptions, + { minDelay, maxDelay }: WaiterOptions, client: T, input: S, acceptorChecks: (client: T, input: S) => Promise ): Promise => { let currentAttempt = 1; - let currentDelay = params.minDelay; while (true) { - await sleep(currentDelay); + await sleep(exponentialBackoff(minDelay, maxDelay, currentAttempt)); const { state } = await acceptorChecks(client, input); if (state === WaiterState.SUCCESS || state === WaiterState.FAILURE) { return { state }; } - - currentDelay = exponentialBackoff(params.minDelay, params.maxDelay, currentAttempt); currentAttempt += 1; } };