From 1bd0dcfd1068280a984d0d6e51c0cf2e56945dfe Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 6 Dec 2024 18:31:43 +0000 Subject: [PATCH 1/3] feat(util-waiter): aggregate observed responses in waiter response --- 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 +++ 4 files changed, 69 insertions(+), 7 deletions(-) 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..9db98f18d70 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..2ee4461ddf2 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 }; /** From 26dfb0a6da24f91e0c520a381a5ae6e382c1259e Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 6 Dec 2024 18:55:29 +0000 Subject: [PATCH 2/3] formatting --- packages/util-waiter/src/poller.ts | 6 +++--- packages/util-waiter/src/waiter.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/util-waiter/src/poller.ts b/packages/util-waiter/src/poller.ts index 9db98f18d70..7fea53a9f77 100644 --- a/packages/util-waiter/src/poller.ts +++ b/packages/util-waiter/src/poller.ts @@ -32,7 +32,7 @@ export const runPolling = async ( const { state, reason } = await acceptorChecks(client, input); if (reason) { const message = createMessageFromResponse(reason); - observedResponses[message] |= 0 + observedResponses[message] |= 0; observedResponses[message] += 1; } @@ -48,7 +48,7 @@ export const runPolling = async ( while (true) { if (abortController?.signal?.aborted || abortSignal?.aborted) { const message = "AbortController signal aborted."; - observedResponses[message] |= 0 + observedResponses[message] |= 0; observedResponses[message] += 1; return { state: WaiterState.ABORTED, observedResponses }; } @@ -63,7 +63,7 @@ export const runPolling = async ( if (reason) { const message = createMessageFromResponse(reason); - observedResponses[message] |= 0 + observedResponses[message] |= 0; observedResponses[message] += 1; } diff --git a/packages/util-waiter/src/waiter.ts b/packages/util-waiter/src/waiter.ts index 2ee4461ddf2..0bb1f6471eb 100644 --- a/packages/util-waiter/src/waiter.ts +++ b/packages/util-waiter/src/waiter.ts @@ -45,7 +45,7 @@ export type WaiterResult = { * Responses observed by the waiter during its polling, where the value * is the count. */ - observedResponses?: Record + observedResponses?: Record; }; /** From 8a639899bc35cd83eb6dbbc0b7b0512229e9c0fd Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 6 Dec 2024 19:01:22 +0000 Subject: [PATCH 3/3] changeset --- .changeset/plenty-parents-accept.md | 5 +++++ 1 file changed, 5 insertions(+) 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