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