From 8950c05a5b9bfebfc0432468f4f42448325bc979 Mon Sep 17 00:00:00 2001 From: George Fu Date: Mon, 9 Dec 2024 11:24:30 -0500 Subject: [PATCH] feat(util-waiter): aggregate observed responses in waiter response (#1467) * feat(util-waiter): aggregate observed responses in waiter response * formatting * changeset --- .changeset/plenty-parents-accept.md | 5 ++ packages/util-waiter/src/createWaiter.spec.ts | 6 +-- packages/util-waiter/src/poller.spec.ts | 15 ++++++ packages/util-waiter/src/poller.ts | 49 +++++++++++++++++-- packages/util-waiter/src/waiter.ts | 6 +++ 5 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 .changeset/plenty-parents-accept.md diff --git a/.changeset/plenty-parents-accept.md b/.changeset/plenty-parents-accept.md new file mode 100644 index 00000000000..bd3d3c6b059 --- /dev/null +++ b/.changeset/plenty-parents-accept.md @@ -0,0 +1,5 @@ +--- +"@smithy/util-waiter": minor +--- + +record observed responses in waiter results diff --git a/packages/util-waiter/src/createWaiter.spec.ts b/packages/util-waiter/src/createWaiter.spec.ts index 60a5cebd38c..d4592397808 100644 --- a/packages/util-waiter/src/createWaiter.spec.ts +++ b/packages/util-waiter/src/createWaiter.spec.ts @@ -53,7 +53,7 @@ describe("createWaiter", () => { ); vi.advanceTimersByTime(10 * 1000); abortController.abort(); // Abort before maxWaitTime(20s); - expect(await statusPromise).toEqual(abortedState); + expect(await statusPromise).toContain(abortedState); }); it("should success when acceptor checker returns seccess", async () => { @@ -67,7 +67,7 @@ describe("createWaiter", () => { mockAcceptorChecks ); vi.advanceTimersByTime(minimalWaiterConfig.minDelay * 1000); - expect(await statusPromise).toEqual(successState); + expect(await statusPromise).toContain(successState); }); it("should fail when acceptor checker returns failure", async () => { @@ -81,6 +81,6 @@ describe("createWaiter", () => { mockAcceptorChecks ); vi.advanceTimersByTime(minimalWaiterConfig.minDelay * 1000); - expect(await statusPromise).toEqual(failureState); + expect(await statusPromise).toContain(failureState); }); }); diff --git a/packages/util-waiter/src/poller.spec.ts b/packages/util-waiter/src/poller.spec.ts index a5a5f78d6a7..355f772af1d 100644 --- a/packages/util-waiter/src/poller.spec.ts +++ b/packages/util-waiter/src/poller.spec.ts @@ -17,25 +17,40 @@ describe(runPolling.name, () => { const input = "mockInput"; const abortedState = { state: WaiterState.ABORTED, + observedResponses: { + "AbortController signal aborted.": 1, + }, }; const failureState = { state: WaiterState.FAILURE, reason: { mockedReason: "some-failure-value", }, + observedResponses: { + [JSON.stringify({ + mockedReason: "some-failure-value", + })]: 1, + }, }; const successState = { state: WaiterState.SUCCESS, reason: { mockedReason: "some-success-value", }, + observedResponses: { + [JSON.stringify({ + mockedReason: "some-success-value", + })]: 1, + }, }; const retryState = { state: WaiterState.RETRY, reason: undefined, + observedResponses: {}, }; const timeoutState = { state: WaiterState.TIMEOUT, + observedResponses: {}, }; let mockAcceptorChecks; diff --git a/packages/util-waiter/src/poller.ts b/packages/util-waiter/src/poller.ts index 691666ad38d..7fea53a9f77 100644 --- a/packages/util-waiter/src/poller.ts +++ b/packages/util-waiter/src/poller.ts @@ -27,9 +27,17 @@ export const runPolling = async ( input: Input, acceptorChecks: (client: Client, input: Input) => Promise ): Promise => { + const observedResponses: Record = {}; + const { state, reason } = await acceptorChecks(client, input); + if (reason) { + const message = createMessageFromResponse(reason); + observedResponses[message] |= 0; + observedResponses[message] += 1; + } + if (state !== WaiterState.RETRY) { - return { state, reason }; + return { state, reason, observedResponses }; } let currentAttempt = 1; @@ -39,20 +47,53 @@ export const runPolling = async ( const attemptCeiling = Math.log(maxDelay / minDelay) / Math.log(2) + 1; while (true) { if (abortController?.signal?.aborted || abortSignal?.aborted) { - return { state: WaiterState.ABORTED }; + const message = "AbortController signal aborted."; + observedResponses[message] |= 0; + observedResponses[message] += 1; + return { state: WaiterState.ABORTED, observedResponses }; } const delay = exponentialBackoffWithJitter(minDelay, maxDelay, attemptCeiling, currentAttempt); // Resolve the promise explicitly at timeout or aborted. Otherwise this while loop will keep making API call until // `acceptorCheck` returns non-retry status, even with the Promise.race() outside. if (Date.now() + delay * 1000 > waitUntil) { - return { state: WaiterState.TIMEOUT }; + return { state: WaiterState.TIMEOUT, observedResponses }; } await sleep(delay); const { state, reason } = await acceptorChecks(client, input); + + if (reason) { + const message = createMessageFromResponse(reason); + observedResponses[message] |= 0; + observedResponses[message] += 1; + } + if (state !== WaiterState.RETRY) { - return { state, reason }; + return { state, reason, observedResponses }; } currentAttempt += 1; } }; + +/** + * @internal + * convert the result of an SDK operation, either an error or response object, to a + * readable string. + */ +const createMessageFromResponse = (reason: any): string => { + if (reason?.$responseBodyText) { + // is a deserialization error. + return `Deserialization error for body: ${reason.$responseBodyText}`; + } + if (reason?.$metadata?.httpStatusCode) { + // has a status code. + if (reason.$response || reason.message) { + // is an error object. + return `${reason.$response.statusCode ?? reason.$metadata.httpStatusCode ?? "Unknown"}: ${reason.message}`; + } + // is an output object. + return `${reason.$metadata.httpStatusCode}: OK`; + } + // is an unknown object. + return String(reason?.message ?? JSON.stringify(reason) ?? "Unknown"); +}; diff --git a/packages/util-waiter/src/waiter.ts b/packages/util-waiter/src/waiter.ts index aac53a03b84..0bb1f6471eb 100644 --- a/packages/util-waiter/src/waiter.ts +++ b/packages/util-waiter/src/waiter.ts @@ -40,6 +40,12 @@ export type WaiterResult = { * (optional) Indicates a reason for why a waiter has reached its state. */ reason?: any; + + /** + * Responses observed by the waiter during its polling, where the value + * is the count. + */ + observedResponses?: Record; }; /**