Skip to content

Commit e7be867

Browse files
authored
chore: Add waiters design doc to design/ folder (#498)
1 parent 22732df commit e7be867

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed

design/waiters.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Waiters Design
2+
3+
## Introduction
4+
5+
Waiters are a Smithy feature, and are defined in the Smithy specification at https://awslabs.github.io/smithy/2.0/additional-specs/waiters.html. From this spec:
6+
7+
>Waiters are a client-side abstraction used to poll a resource until a desired state is reached, or until it is determined that the resource will never enter into the desired state.
8+
9+
10+
Waiters are defined in Smithy using the `@waitable` trait on an API operation. Here is an example (from the Smithy spec document) that defines a waiter that polls until a S3 bucket exists:
11+
12+
```
13+
@waitable(
14+
BucketExists: {
15+
documentation: "Wait until a bucket exists"
16+
acceptors: [
17+
{
18+
state: "success"
19+
matcher: {
20+
success: true
21+
}
22+
}
23+
{
24+
state: "retry"
25+
matcher: {
26+
errorType: "NotFound"
27+
}
28+
}
29+
]
30+
}
31+
)
32+
operation HeadBucket {
33+
input: HeadBucketInput
34+
output: HeadBucketOutput
35+
errors: [NotFound]
36+
}
37+
```
38+
39+
The waiter name (here, `BucketExists`) is just an alphanumeric name that can be used to identify waiters that wait for specific conditions.
40+
The waiter structure defines (along with a documentation field) the conditions that will cause the waiter to continue waiting, be satisfied, or to immediately fail.
41+
42+
On client SDKs, the waiter is exposed as an instance method on a service client like S3 or EC2. Waiting behaves in a way that:
43+
44+
* If the operation is successfully completed, then the waiter transitions to success state and terminates.
45+
* If the operation encounters an error, then the waiter transitions to retry state.
46+
* In retry state, the operation is recreated and sent to service (step 1).
47+
48+
Because the `@waitable` trait is defined in the Smithy IDL and is not specific to AWS, the Swift implementation for waiters will be implemented entirely in the `smithy-swift` project. The AWS Swift SDK will use `smithy-swift` to generate waiters for specific AWS services.
49+
50+
## Public Interface
51+
52+
A waiter method is added to the service client for each waiter defined on that service. The method signature of the waiter is of the form `func waitUntil<WaiterName>(options:input:)` and the method takes two parameters: a waiter options value, and an input value (which is the same as the input for the non-waited operation.) When called, the `waitUntil...` method will immediately perform the operation, but if needed, will continue to retry the operation until the wait condition is satisfied. Only then will the `waitUntil...` method return with a output value. If a response to an operation meets a failure condition, or an error is returned which is not explicitly handled by the waiter, the waiter will terminate and a Swift error will be thrown back to the caller.
53+
54+
```
55+
do {
56+
let client = try S3Client(region: "us-east-1")
57+
let input = HeadBucketInput(bucket: "newBucket")
58+
let options = WaiterOptions(maxWaitTime: 120.0, minDelay: 5.0, maxDelay: 20.0)
59+
try await client.waitUntilBucketExists(options: options, input: input)
60+
print("Bucket now exists.")
61+
} catch {
62+
print("Error occurred while waiting: \(error.localizedDescription)")
63+
}
64+
```
65+
66+
#### Wait Options
67+
68+
The caller will supply wait options to configure certain parameters of the wait. The only required value on waiter options is the maximum wait time `maxWaitTime`, which will be in the form of a Swift `TimeInterval`. Wait options may optionally provide a `minDelay` and `maxDelay`, which, when supplied, will replace the ones specified in the waiter declaration.
69+
70+
```
71+
public struct WaiterOptions {
72+
let maxWaitTime: TimeInterval
73+
let minDelay: TimeInterval?
74+
let maxDelay: TimeInterval?
75+
}
76+
```
77+
78+
#### Retry Strategy
79+
80+
Smithy specifies an algorithm which is to be used for scheduling retries during waiting. The Smithy preferred algorithm is always used and not replaceable or customizable, other than through the `WaiterOptions` parameters. Smithy’s retry strategy is best summarized as “exponential backoff with jitter” and is defined in detail in the [Smithy docs](https://awslabs.github.io/smithy/2.0/additional-specs/waiters.html#waiter-retries).
81+
82+
#### Return Type
83+
84+
On success, the `waitUntil...` method will return a `WaiterOutcome` value that includes summary data about the wait, and the result of the final wait operation, i.e. the one that caused the waiter to succeed. Because a waiter may succeed on either a successful or an error response, the last result may be either an error or output object; the outcome’s `result` property is an enumeration that may contain either, depending on the acceptor that was matched.
85+
86+
If the caller is not interested in the contents of a waiter’s outcome, the return value may be discarded.
87+
88+
```
89+
public struct WaiterOutcome<Output> {
90+
let attempts: Int
91+
let result: Result<Output, Error>
92+
}
93+
```
94+
95+
## Implementation
96+
97+
### 1. Add waiter types to Smithy ClientRuntime (large, ~1 wk.)
98+
99+
Several additions will have to be made to the Smithy `ClientRuntime` to implement waiters. For each subitem a-d:
100+
101+
1. Build the type, along with all needed functionality
102+
2. Add unit tests to cover all waiter logic
103+
104+
Finally, as an operational test, manually construct & configure a waiter from the components that have been added to `ClientRuntime` using a live, existing AWS endpoint, and verify that it works on an actual AWS service.
105+
106+
#### a. Add a Reusable Waiter Options Type
107+
108+
See the `WaiterOptions` defined in the interface above.
109+
110+
#### b. Add a Generic, Reusable Waiter Outcome Type
111+
112+
Waiter outcome should be defined as shown in Interface above. It will be generic so that it can be used with any output type.
113+
114+
#### c. Add a WaiterConfig and Acceptor type
115+
116+
The `WaiterConfig` configures a waiter to follow the behaviors defined in a waiter’s Smithy definition (as opposed to the ones that are supplied at the time of waiting by the caller in `WaiterOptions`). A static WaiterConfig value will be code-generated for each named waiter defined for a service.
117+
118+
A `WaiterConfig` has the form:
119+
120+
```
121+
public struct WaiterConfig<Input, Output> {
122+
let name: String
123+
let minDelay: TimeInterval
124+
let maxDelay: TimeInterval
125+
let acceptors: [Acceptor<Input, Output>]
126+
}
127+
```
128+
129+
Each waiter contains an ordered list of `Acceptor`s. An instance of this type can be code-generated from the Smithy specification for each acceptor defined in a waiter. The Smithy spec defines four alternative forms for a `Matcher`, but all can be boiled down to a closure with the signature below. (If desired, convenience initializers for `Matcher` can be defined that mirror the Smithy matcher forms more closely.)
130+
131+
```
132+
public struct Acceptor<Input, Output> {
133+
134+
typealias Matcher = (Input, Result<Output, Error>) -> Bool
135+
136+
public enum State {
137+
case success
138+
case failure
139+
case retry
140+
}
141+
142+
public let state: State
143+
public let matcher: Matcher
144+
}
145+
```
146+
147+
#### d. Add a Generic Waiter Class to Coordinate Waiting
148+
149+
To perform waiting, one creates & configures a `Waiter` object, then calls a wait method on it. The signature for the `Waiter` type:
150+
151+
```
152+
public class Waiter<Input, Output> {
153+
154+
public init(config: WaiterConfig<Input, Output>,
155+
operation: @escaping (Input) async throws -> Output)
156+
157+
@discardableValue
158+
public waitUntil(options: WaiterOptions, input: Input) -> WaiterOutcome<Output>
159+
}
160+
```
161+
162+
The `Waiter` object is initialized with these params:
163+
164+
* A `WaiterConfig` that is code-generated from the Smithy definition.
165+
* The input object, supplied by the caller.
166+
* The closure to be used to access the resource (closure will take `Input` as a parameter, perform a call on the Client, asynchronously returns `Output` & may throw). This signature is the same as an operation’s function pointer and will typically be code-generated.
167+
168+
Once the `Waiter` is created, the `waitUntil(options:input:)` method on the Waiter is called with:
169+
170+
* A set of `WaiterOptions` to configure this wait
171+
* An `Input` structure that will be used for every operation during the wait
172+
173+
The wait will begin and continue asynchronously until:
174+
175+
* An acceptor with state `success` is matched and a `WaiterOutcome` is asynchronously returned, or
176+
* an acceptor with state `failure` is matched and an error is asynchronously thrown, or
177+
* an operation returns an error that is not matched by any acceptor and the error is asynchronously thrown, or
178+
* the maximum timeout for the wait is reached, and a `WaiterTimeoutError` is asynchronously thrown.
179+
180+
#### e. Add a WaiterTypedError protocol
181+
182+
The `errorType` acceptor matches errors with a string that is specified in the acceptor, and matches to the “error code” on AWS errors. Because the Swift SDK does not currently expose the error code on its errors, the `WaiterTypedError` protocol has been created to allow operation errors to provide their type for matching.
183+
184+
```
185+
public protocol WaiterTypedError: Error {
186+
var waiterErrorType: String? { get }
187+
}
188+
```
189+
190+
To minimize the effect on binary size, `WaiterTypedError` conformance is only rendered for operation errors where the operation actually has a waiter with an `errorType` acceptor.
191+
192+
Any error thrown by a failed operation is conditionally cast to `WaiterTypedError` before attempting to match it, so any error that doesn't conform to `WaiterTypedError` will always fail to match an `errorType` acceptor.
193+
194+
Because operation errors are sometimes enclosed in `SdkError` before being thrown, that type is extended with `WaiterTypedError` conformance as well, to provide the error type for the operation error it encloses. Swift error types for capturing “unknown” errors are also extended with `WaiterTypedError` conformance, returning the error code that could not be matched to a known type of error.
195+
196+
#### Summary
197+
198+
The advantage of this approach is that it keeps all code & logic needed to implement the waiter separate from the underlying operation, which remains totally unmodified, and waiters add no complexity to existing runtime or code-generated code.
199+
200+
### 2. Code-generate WaiterConfig for each waiter (large, ~1 wk)
201+
202+
The `WaiterConfig` value will be code-generated to a static variable for use in each `waitUntil...` method defined below.
203+
204+
The ordered list of acceptors in the Smithy specification for each waiter should be code-generated into an array of `Acceptor`s and stored in the waiter config. The `Acceptor` array will provide logic used at runtime to decide whether the waiter will succeed, retry, or fail in response to an operation.
205+
206+
Acceptors may include a matching predicate defined per the [JMESPath specification](https://jmespath.org/). The [main Smithy project](https://github.com/awslabs/smithy) includes a [parser](https://github.com/awslabs/smithy/tree/main/smithy-jmespath/src/main/java/software/amazon/smithy/jmespath) that breaks JMESPath expressions into a Smithy-native AST for use in code generation. This AST, in turn, will be used to generate Swift-native code (i.e. Boolean expressions) that evaluate to the intended result of the JMESPath expression.
207+
208+
### 3. Code-generate a `waitUntil...` method on the service client for each waiter (medium, ~3 days)
209+
210+
A `waitUntil...` method is code-generated to the service client for each waiter that is defined. The name of the method will be set as follows: `waitUntil<WaiterName>` (waiter names are unique within a service, so waiter name is sufficient to prevent namespace collisions among service client instance methods.) The method would take as parameters the wait options and an input object, and asynchronously return a `WaiterOutcome` which may be ignored by the caller. The `waitUntil...` method will throw if the waiter fails, times out, or if any other unhandled error occurs while waiting.
211+
212+
The body of the `waitUntil...` method will create & configure a specialized instance of the generic `Waiter` described above and use it to perform the waited operation:
213+
214+
```
215+
@discardableResult
216+
public func waitUntilBucketExists(options: WaiterOptions,
217+
input: HeadBucketInput) async throws
218+
-> WaiterOutcome<HeadBucketOutputResponse> {
219+
let waiter = Waiter<HeadBucketInput, HeadBucketOutputResponse>(
220+
config: Self.waitUntilBucketExistsConfig,
221+
operation: self.headBucket(input:)
222+
)
223+
return try async waiter.wait(options: options, input: input)
224+
}
225+
```
226+
227+
### 4. Integrate Smithy additions into AWS Swift SDK (medium, ~3-4 days)
228+
229+
Integrate the smithy-swift changes made above into the AWS Swift SDK. Code-generate the SDK, ensuring that waiters code-generate, build, and operate as expected, using representative AWS API operations. At the completion of this stage, waiters should be merged and ready to include in the next SDK release.
230+

0 commit comments

Comments
 (0)