README.md
1**Design:** New Feature, **Status:** [Released](../../README.md)
2
3# Waiters
4
5"Waiters" are an abstraction used to poll a resource until a desired state is reached or until it is determined that
6the resource will never enter into the desired state. This feature is supported in the AWS Java SDK 1.x and this document proposes
7how waiters should be implemented in the Java SDK 2.x.
8
9## Introduction
10
11A waiter makes it easier for customers to wait for a resource to transition into a desired state. It comes handy when customers are
12interacting with operations that are asynchronous on the service side.
13
14For example, when you invoke `dynamodb#createTable`, the service immediately returns a response with a TableStatus of `CREATING`
15and the table will not be available to perform write or read until the status has transitioned to `ACTIVE`. Waiters can be used to help
16you handle the task of waiting for the table to become available.
17
18## Proposed APIs
19
20The SDK 2.x will support both sync and async waiters for service clients that have waiter-eligible operations. It will also provide a generic `Waiter` class
21which makes it possible for customers to customize polling function, define expected success, failure and retry conditions as well as configurations such as `maxAttempts`.
22
23### Usage Examples
24
25#### Example 1: Using sync waiters
26
27- instantiate a waiter object from an existing service client
28
29```Java
30DynamoDbClient client = DynamoDbClient.create();
31DynamodbWaiter waiter = client.waiter();
32
33WaiterResponse<DescribeTableResponse> response = waiter.waitUntilTableExists(b -> b.tableName("table"));
34```
35
36- instantiate a waiter object from builder
37
38```java
39DynamodbWaiter waiter = DynamoDbWaiter.builder()
40 .client(client)
41 .overrideConfiguration(p -> p.maxAttempts(10))
42 .build();
43
44WaiterResponse<DescribeTableResponse> response = waiter.waitUntilTableExists(b -> b.tableName("table"));
45
46```
47
48#### Example 2: Using async waiters
49
50- instantiate a waiter object from an existing service client
51
52```Java
53DynamoDbAsyncClient asyncClient = DynamoDbAsyncClient.create();
54DynamoDbAsyncWaiter waiter = asyncClient.waiter();
55
56CompletableFuture<WaiterResponse<DescribeTableResponse>> responseFuture = waiter.waitUntilTableExists(b -> b.tableName("table"));
57
58```
59
60- instantiate a waiter object from builder
61
62```java
63DynamoDbAsyncWaiter waiter = DynamoDbAsyncWaiter.builder()
64 .client(asyncClient)
65 .overrideConfiguration(p -> p.maxAttempts(10))
66 .build();
67
68CompletableFuture<WaiterResponse<DescribeTableResponse>> responseFuture = waiter.waitUntilTableExists(b -> b.tableName("table"));
69```
70
71
72*FAQ Below: "Why not create waiter operations directly on the client?"*
73
74#### Example 3: Using the generic waiter
75
76```Java
77Waiter<DescribeTableResponse> waiter =
78 Waiter.builder(DescribeTableResponse.class)
79 .addAcceptor(WaiterAcceptor.successAcceptor(r -> r.table().tableStatus().equals(TableStatus.ACTIVE)))
80 .addAcceptor(WaiterAcceptor.retryAcceptor(t -> t instanceof ResourceNotFoundException))
81 .addAcceptor(WaiterAcceptor.errorAcceptor(t -> t instanceof InternalServerErrorException))
82 .overrideConfiguration(p -> p.maxAttemps(20).backoffStrategy(BackoffStrategy.defaultStrategy())
83 .build();
84
85// run synchronously
86WaiterResponse<DescribeTableResponse> response = waiter.run(() -> client.describeTable(describeTableRequest));
87
88// run asynchronously
89CompletableFuture<WaiterResponse<DescribeTableResponse>> responseFuture =
90 waiter.runAsync(() -> asyncClient.describeTable(describeTableRequest));
91```
92
93### `{Service}Waiter` and `{Service}AsyncWaiter`
94
95Two classes will be created for each waiter-eligible service: `{Service}Waiter` and `{Service}AsyncWaiter` (e.g. `DynamoDbWaiter`, `DynamoDbAsyncWaiter`).
96This follows the naming strategy established by the current `{Service}Client` and `{Service}Utilities` classes.
97
98#### Example
99
100```Java
101/**
102 * Waiter utility class that waits for a resource to transition to the desired state.
103 */
104@SdkPublicApi
105@Generated("software.amazon.awssdk:codegen")
106public interface DynamoDbWaiter extends SdkAutoCloseable {
107
108 /**
109 * Poller method that waits for the table status to transition to <code>ACTIVE</code> by
110 * invoking {@link DynamoDbClient#describeTable}. It returns when the resource enters into a desired state or
111 * it is determined that the resource will never enter into the desired state.
112 *
113 * @param describeTableRequest Represents the input of a <code>DescribeTable</code> operation.
114 * @return {@link DescribeTableResponse}
115 */
116 default WaiterResponse<DescribeTableResponse> waitUntilTableExists(DescribeTableRequest describeTableRequest) {
117 throw new UnsupportedOperationException();
118 }
119
120 default WaiterResponse<DescribeTableResponse> waitUntilTableExists(Consumer<DescribeTableRequest.Builder> describeTableRequest) {
121 return waitUntilTableExists(DescribeTableRequest.builder().applyMutation(describeTableRequest).build());
122 }
123
124 /**
125 * Polls {@link DynamoDbAsyncClient#describeTable} API until the desired condition {@code TableExists} is met, or
126 * until it is determined that the resource will never enter into the desired state
127 *
128 * @param describeTableRequest
129 * The request to be used for polling
130 * @param overrideConfig
131 * Per request override configuration for waiters
132 * @return WaiterResponse containing either a response or an exception that has matched with the waiter success
133 * condition
134 */
135 default CompletableFuture<WaiterResponse<DescribeTableResponse>> waitUntilTableExists(
136 DescribeTableRequest describeTableRequest, WaiterOverrideConfiguration overrideConfig) {
137 throw new UnsupportedOperationException();
138 }
139
140 /**
141 * Polls {@link DynamoDbAsyncClient#describeTable} API until the desired condition {@code TableExists} is met, or
142 * until it is determined that the resource will never enter into the desired state.
143 * <p>
144 * This is a convenience method to create an instance of the request builder and instance of the override config
145 * builder
146 *
147 * @param describeTableRequest
148 * The consumer that will configure the request to be used for polling
149 * @param overrideConfig
150 * The consumer that will configure the per request override configuration for waiters
151 * @return WaiterResponse containing either a response or an exception that has matched with the waiter success
152 * condition
153 */
154 default CompletableFuture<WaiterResponse<DescribeTableResponse>> waitUntilTableExists(
155 Consumer<DescribeTableRequest.Builder> describeTableRequest,
156 Consumer<WaiterOverrideConfiguration.Builder> overrideConfig) {
157 return waitUntilTableExists(DescribeTableRequest.builder().applyMutation(describeTableRequest).build(),
158 WaiterOverrideConfiguration.builder().applyMutation(overrideConfig).build());
159 }
160
161 // other waiter operations omitted
162 // ...
163
164
165 interface Builder {
166
167 Builder client(DynamoDbClient client);
168
169 /**
170 * Defines overrides to the default SDK waiter configuration that should be used for waiters created from this
171 * builder
172 *
173 * @param overrideConfiguration
174 * the override configuration to set
175 * @return a reference to this object so that method calls can be chained together.
176 */
177 Builder overrideConfiguration(WaiterOverrideConfiguration overrideConfiguration);
178
179 DynamoDbWaiter build();
180 }
181}
182
183/**
184 * Waiter utility class that waits for a resource to transition to the desired state asynchronously.
185 */
186@SdkPublicApi
187@Generated("software.amazon.awssdk:codegen")
188public interface DynamoDbAsyncWaiter extends SdkAutoCloseable {
189
190 /**
191 * Poller method that waits for the table status to transition to <code>ACTIVE</code> by
192 * invoking {@link DynamoDbClient#describeTable}. It returns when the resource enters into a desired state or
193 * it is determined that the resource will never enter into the desired state.
194 *
195 * @param describeTableRequest Represents the input of a <code>DescribeTable</code> operation.
196 * @return A CompletableFuture containing the result of the DescribeTable operation returned by the service. It completes
197 * successfully when the resource enters into a desired state or it completes exceptionally when it is determined that the
198 * resource will never enter into the desired state.
199 */
200 default CompletableFuture<WaiterResponse<DescribeTableResponse>> waitUntilTableExists(DescribeTableRequest describeTableRequest) {
201 throw new UnsupportedOperationException();
202 }
203
204 default CompletableFuture<WaiterResponse<DescribeTableResponse>> waitUntilTableExists(Consumer<DescribeTableRequest.Builder> describeTableRequest) {
205 return waitUntilTableExists(DescribeTableRequest.builder().applyMutation(describeTableRequest).build());
206 }
207
208 // other waiter operations omitted
209 // ...
210
211
212 interface Builder {
213
214 Builder client(DynamoDbAsyncClient client);
215
216 Builder scheduledExecutorService(ScheduledExecutorService executorService);
217
218 Builder overrideConfiguration(WaiterOverrideConfiguration overrideConfiguration);
219
220 DynamoDbAsyncWaiter build();
221 }
222
223}
224```
225
226*FAQ Below: "Why returning a WaiterResponse wrapper class"*.
227
228#### Instantiation
229
230This class can be instantiated from an existing service client or builder
231
232- from an existing service client
233
234```Java
235// sync waiter
236DynamoDbClient dynamo = DynamoDbClient.create();
237DynamoDbWaiter dynamoWaiter = dynamo.waiter();
238
239// async waiter
240DynamoDbClient dynamoAsync = DynamoDbAsyncClient.create();
241DynamoDbAsyncWaiter dynamoAsyncWaiter = dynamoAsync.waiter();
242```
243
244- from waiter builder
245
246```java
247// sync waiter
248DynamodbWaiter waiter = DynamoDbWaiter.builder()
249 .client(client)
250 .overrideConfiguration(p -> p.maxAttempts(10))
251 .build();
252
253
254// async waiter
255DynamoDbAsyncWaiter asyncWaiter = DynamoDbAsyncWaiter.builder()
256 .client(asyncClient)
257 .overrideConfiguration(p -> p.maxAttempts(10))
258 .build();
259
260
261```
262
263#### Methods
264
265A method will be generated for each operation that needs waiter support. There are two categories depending on the expected success state.
266
267 - sync: `WaiterResponse<{Operation}Response> waitUntil{DesiredState}({Operation}Request)`
268 ```java
269 WaiterResponse<DescribeTableResponse> waitUntilTableExists(DescribeTableRequest describeTableRequest)
270 ```
271 - async: `CompletableFuture<WaiterResponse<{Operation}Response>> waitUntil{DesiredState}({Operation}Request)`
272 ```java
273 CompletableFuture<WaiterResponse<DescribeTableResponse>> waitUntilTableExists(DescribeTableRequest describeTableRequest)
274 ```
275
276### `WaiterResponse<T>`
277```java
278/**
279 * The response returned from a waiter operation
280 * @param <T> the type of the response
281 */
282@SdkPublicApi
283public interface WaiterResponse<T> {
284
285 /**
286 * @return the ResponseOrException union received that has matched with the waiter success condition
287 */
288 ResponseOrException<T> matched();
289
290 /**
291 * @return the number of attempts executed
292 */
293 int attemptsExecuted();
294
295}
296```
297
298*FAQ Below: "Why making response and exception optional"*.
299
300### `Waiter<T>`
301
302The generic `Waiter` class enables users to customize waiter configurations and provide their own `WaiterAcceptor`s which define the expected states and controls the terminal state of the waiter.
303
304#### Methods
305
306```java
307@SdkPublicApi
308public interface Waiter<T> {
309
310 /**
311 * It returns when the resource enters into a desired state or
312 * it is determined that the resource will never enter into the desired state.
313 *
314 * @param pollingFunction the polling function
315 * @return the {@link WaiterResponse} containing either a response or an exception that has matched with the
316 * waiter success condition
317 */
318 default WaiterResponse<T> run(Supplier<T> pollingFunction) {
319 throw new UnsupportedOperationException();
320 }
321
322 /**
323 * It returns when the resource enters into a desired state or
324 * it is determined that the resource will never enter into the desired state.
325 *
326 * @param pollingFunction the polling function
327 * @param overrideConfig per request override configuration
328 * @return the {@link WaiterResponse} containing either a response or an exception that has matched with the
329 * waiter success condition
330 */
331 default WaiterResponse<T> run(Supplier<T> pollingFunction, WaiterOverrideConfiguration overrideConfig) {
332 throw new UnsupportedOperationException();
333 }
334
335 default WaiterResponse<T> run(Supplier<T> pollingFunction, Consumer<WaiterOverrideConfiguration.Builder> overrideConfig) {
336 return run(pollingFunction, WaiterOverrideConfiguration.builder().applyMutation(overrideConfig).build());
337 }
338
339 /**
340 * Creates a newly initialized builder for the waiter object.
341 *
342 * @param responseClass the response class
343 * @param <T> the type of the response
344 * @return a Waiter builder
345 */
346 static <T> Builder<T> builder(Class<? extends T> responseClass) {
347 return DefaultWaiter.builder();
348 }
349}
350```
351#### Inner-Class: `Waiter.Builder`
352
353```java
354 public interface Builder<T> {
355
356 /**
357 * Defines a list of {@link WaiterAcceptor}s to check if an expected state has met after executing an operation.
358 *
359 * @param waiterAcceptors the waiter acceptors
360 * @return the chained builder
361 */
362 Builder<T> acceptors(List<WaiterAcceptor<T>> waiterAcceptors);
363
364 /**
365 * Add a {@link WaiterAcceptor}s
366 *
367 * @param waiterAcceptors the waiter acceptors
368 * @return the chained builder
369 */
370 Builder<T> addAcceptor(WaiterAcceptor<T> waiterAcceptors);
371
372 /**
373 * Defines overrides to the default SDK waiter configuration that should be used
374 * for waiters created by this builder.
375 *
376 * @param overrideConfiguration the override configuration
377 * @return a reference to this object so that method calls can be chained together.
378 */
379 Builder<T> overrideConfiguration(WaiterOverrideConfiguration overrideConfiguration);
380 }
381```
382### `AsyncWaiter<T>`
383
384#### Methods
385```java
386@SdkPublicApi
387public interface AsyncWaiter<T> {
388
389 /**
390 * Runs the provided polling function. It completes successfully when the resource enters into a desired state or
391 * exceptionally when it is determined that the resource will never enter into the desired state.
392 *
393 * @param asyncPollingFunction the polling function to trigger
394 * @return A {@link CompletableFuture} containing the {@link WaiterResponse}
395 */
396 default CompletableFuture<WaiterResponse<T>> runAsync(Supplier<CompletableFuture<T>> asyncPollingFunction) {
397 throw new UnsupportedOperationException();
398 }
399
400 /**
401 * Runs the provided polling function. It completes successfully when the resource enters into a desired state or
402 * exceptionally when it is determined that the resource will never enter into the desired state.
403 *
404 * @param asyncPollingFunction the polling function to trigger
405 * @param overrideConfig per request override configuration
406 * @return A {@link CompletableFuture} containing the {@link WaiterResponse}
407 */
408 default CompletableFuture<WaiterResponse<T>> runAsync(Supplier<CompletableFuture<T>> asyncPollingFunction,
409 WaiterOverrideConfiguration overrideConfig) {
410 throw new UnsupportedOperationException();
411 }
412
413 default CompletableFuture<WaiterResponse<T>> runAsync(Supplier<CompletableFuture<T>> asyncPollingFunction,
414 Consumer<WaiterOverrideConfiguration.Builder> overrideConfig) {
415 return runAsync(asyncPollingFunction, WaiterOverrideConfiguration.builder().applyMutation(overrideConfig).build());
416 }
417}
418```
419
420#### Inner-Class: `AsyncWaiter.Builder`
421#### Methods
422```java
423 public interface Builder<T> {
424
425 /**
426 * Defines a list of {@link WaiterAcceptor}s to check if an expected state has met after executing an operation.
427 *
428 * @param waiterAcceptors the waiter acceptors
429 * @return the chained builder
430 */
431 Builder<T> acceptors(List<WaiterAcceptor<T>> waiterAcceptors);
432
433 /**
434 * Add a {@link WaiterAcceptor}s
435 *
436 * @param waiterAcceptors the waiter acceptors
437 * @return the chained builder
438 */
439 Builder<T> addAcceptor(WaiterAcceptor<T> waiterAcceptors);
440
441 /**
442 * Defines overrides to the default SDK waiter configuration that should be used
443 * for waiters created by this builder.
444 *
445 * @param overrideConfiguration the override configuration
446 * @return a reference to this object so that method calls can be chained together.
447 */
448 Builder<T> overrideConfiguration(WaiterOverrideConfiguration overrideConfiguration);
449
450 /**
451 * Define the {@link ScheduledExecutorService} used to schedule async attempts
452 *
453 * @param scheduledExecutorService the schedule executor service
454 * @return the chained builder
455 */
456 Builder<T> scheduledExecutorService(ScheduledExecutorService scheduledExecutorService);
457 }
458```
459
460#### `WaiterOverrideConfiguration`
461
462WaiterOverrideConfiguration specifies how the waiter polls the resources.
463
464```java
465public final class WaiterOverrideConfiguration {
466 //...
467
468 /**
469 * @return the optional maximum number of attempts that should be used when polling the resource
470 */
471 public Optional<Integer> maxAttempts() {
472 return Optional.ofNullable(maxAttempts);
473 }
474
475 /**
476 * @return the optional {@link BackoffStrategy} that should be used when polling the resource
477 */
478 public Optional<BackoffStrategy> backoffStrategy() {
479 return Optional.ofNullable(backoffStrategy);
480 }
481
482 /**
483 * @return the optional amount of time to wait that should be used when polling the resource
484 *
485 */
486 public Optional<Duration> waitTimeout() {
487 return Optional.ofNullable(waitTimeout);
488 }
489}
490
491```
492
493### `WaiterState`
494
495`WaiterState` is an enum that defines possible states of a waiter to be transitioned to if a condition is met
496
497```java
498public enum WaiterState {
499 /**
500 * Indicates the waiter succeeded and must no longer continue waiting.
501 */
502 SUCCESS,
503
504 /**
505 * Indicates the waiter failed and must not continue waiting.
506 */
507 FAILURE,
508
509 /**
510 * Indicates that the waiter encountered an expected failure case and should retry if possible.
511 */
512 RETRY
513}
514```
515
516### `WaiterAcceptor`
517
518`WaiterAcceptor` is a class that inspects the response or error returned from the operation and determines whether an expected condition
519is met and indicates the next state that the waiter should be transitioned to if there is a match.
520
521```java
522@SdkPublicApi
523public interface WaiterAcceptor<T> {
524
525 /**
526 * @return the next {@link WaiterState} that the waiter should be transitioned to if this acceptor matches with the response or error
527 */
528 WaiterState waiterState();
529
530 /**
531 * Check to see if the response matches with the expected state defined by the acceptor
532 *
533 * @param response the response to inspect
534 * @return whether it accepts the response
535 */
536 default boolean matches(T response) {
537 return false;
538 }
539```
540
541## FAQ
542
543### For which services will we generate waiters?
544
545We will generate a `{Service}Waiter` class if the service has any operations that need waiter support.
546
547### Why not create waiter operations directly on the client?
548
549The options are: (1) create separate waiter utility classes or (2) create waiter operations on the client
550
551The following compares Option 1 to Option 2, in the interest of illustrating why Option 1 was chosen.
552
553**Option 1:** create separate waiter utility classes
554
555```Java
556dynamodb.waiter().untilUntilTableExists(describeTableRequest)
557```
558
559**Option 2:** create waiter operations on each service client
560
561```Java
562dynamodb.waitUntilTableExists(describeTableRequest)
563```
564
565**Option 1 Pros:**
566
5671. consistent with existing s3 utilities and presigner method approach, eg: s3Client.utilities()
5682. similar api to v1 waiter, and it might be easier for customers who are already using v1 waiter to migrate to v2.
569
570**Option 2 Pros:**
571
5721. slightly better discoverability
573
574**Decision:** Option 1 will be used, because it is consistent with existing features and option2 might bloat the size
575of the client, making it more difficult to use.
576
577### Why returning `WaiterResponse`?
578
579For waiter operations that awaits a resource to be created, the last successful response sometimes contains important metadata such as resourceId, which is often required for customers to perform other actions with the resource. Without returning the response, customers will have to send an extra request to retrieve the response. This is a [feature request](https://github.com/aws/aws-sdk-java/issues/815) from v1 waiter implementation.
580
581For waiter operations that treats a specific exception as the success state, some customers might still want to access the exception to retrieve the requestId or raw response.
582
583A `WaiterResposne` wrapper class is created to provide either the response or exception depending on what triggers the waiter to reach the desired state. It also provides flexibility to add more metadata such as `attemptExecuted` in the future if needed.
584
585
586### Why making response and exception optional in `WaiterResponse`?
587
588Per the SDK's style guideline `UseOfOptional`,
589
590> `Optional` should be used when it isn't obvious to a caller whether a result will be null.
591
592we make `response` and `exception` optional in `WaiterResponse` because only one of them can be present and it cannot be determined which is present at compile time.
593
594The following example shows how to retrieve a response from `WaiterResponse`
595
596```java
597waiterResponse.matched.response().ifPresent(r -> ...);
598
599```
600
601Another approach is to create a flag field, say `isResponseAvailable`, to indicate if the response is null or not. Customers can check this before accessing `response` to avoid NPE.
602
603```java
604if (waiterResponse.isResponseAvailable()) {
605 DescribeTableResponse response = waiterResponse.response();
606 ...
607}
608
609```
610
611The issue with this approach is that `isResponseAvailable` might not be discovered by customers when they access `WaiterResponse` and they'll have to add null pointer check, otherwise they will end up getting NPEs. It also violates our guideline for the use of optional.
612
613## References
614
615Github feature request links:
616- [Waiters](https://github.com/aws/aws-sdk-java-v2/issues/24)
617- [Async requests that complete when the operation is complete](https://github.com/aws/aws-sdk-java-v2/issues/286)
618
619