Skip to content

Commit

Permalink
feat(util-waiter): aggregate observed responses in waiter response (s…
Browse files Browse the repository at this point in the history
…mithy-lang#1467)

* feat(util-waiter): aggregate observed responses in waiter response

* formatting

* changeset
  • Loading branch information
kuhe authored Dec 9, 2024
1 parent 50d8c54 commit 8950c05
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-parents-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/util-waiter": minor
---

record observed responses in waiter results
6 changes: 3 additions & 3 deletions packages/util-waiter/src/createWaiter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -81,6 +81,6 @@ describe("createWaiter", () => {
mockAcceptorChecks
);
vi.advanceTimersByTime(minimalWaiterConfig.minDelay * 1000);
expect(await statusPromise).toEqual(failureState);
expect(await statusPromise).toContain(failureState);
});
});
15 changes: 15 additions & 0 deletions packages/util-waiter/src/poller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
49 changes: 45 additions & 4 deletions packages/util-waiter/src/poller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,17 @@ export const runPolling = async <Client, Input>(
input: Input,
acceptorChecks: (client: Client, input: Input) => Promise<WaiterResult>
): Promise<WaiterResult> => {
const observedResponses: Record<string, number> = {};

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;
Expand All @@ -39,20 +47,53 @@ export const runPolling = async <Client, Input>(
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");
};
6 changes: 6 additions & 0 deletions packages/util-waiter/src/waiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>;
};

/**
Expand Down

0 comments on commit 8950c05

Please sign in to comment.