AWS Developer Tools Blog

Using Waiters in the AWS SDK for Java 2.x

We are excited to announce the general availability of the waiters feature in the AWS SDK for Java 2.x (version 2.15.0 or later). Waiters are an 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.

When interacting with AWS APIs that are asynchronous, customers often need to wait for a particular resource to become available in order to perform further actions on it. Let’s take an example of AWS DynamoDB CreateTable API. After you invoke the API, the response returns immediately with a TableStatus of CREATING, and you can’t invoke read or write operations until the table status has been transitioned to ACTIVE.

Writing logic to continuously poll the table status can be cumbersome and error-prone. This is especially true when you are dealing with multiple AWS resources, as different resources often have different polling APIs and success states that indicate the availability of that particular resource.

The waiter utility helps take the complexity out of it by providing a simple API that handles the polling task for you. It also provides polling configurations, such as the maximum number of attempts and the back-off strategy between each attempt, which you can customize based on your use-cases. With the waiters utility, you can focus on working with the resource without worrying how to make sure that this resource is ready.

Polling without waiters

Let’s imagine we have a simple application that creates a DynamoDB table and needs to perform a write operation after it’s created. Without waiters, the polling code using the synchronous DynamoDB client might look like this:

    // Polling 5 times for a table to become active
    int attempts = 0;
        while (attempts < 5) {
        try {
            DescribeTableResponse describeTableResponse = dynamo.describeTable(b -> b.tableName("myTable"));
            TableStatus status = describeTableResponse.table().tableStatus();
            if (status.equals(TableStatus.ACTIVE)) {
                break;
            }
            Thread.sleep(5000);
            attempts++;
        } catch (ResourceNotFoundException e) {
            // continue to poll
        }
    }

If you are using an asynchronous SDK client, polling code could be more complex. Rather than the while-loop used in sync code, you would need to write the polling logic in CompletableFuture#whenComplete block and use ScheduledExecutorService to schedule the delays between each attempt. The code might look like this:

dynamoDbAsyncClient.describeTable(b -> b.tableName("myTable"))
    .whenComplete((r, t) -> {
            if (t != null) {
                if (t instanceof ResourceNotFoundException) {
                // check if it exceeds max attempts and if not, schedule next attempt
                // ...
                } else {
                   future.completeExceptionally(new RuntimeException("fail to be active"));                
                }
            } else if (r.table().tableStatus().equals(TableStatus.ACTIVE)) {
                future.complete(r);
            } else {
                // check if it exceeds max attempts and if not, schedule next attempt
                // ...
            }
});

Polling with waiters

With the new addition of the waiters feature, you only need to write a few lines of code. To instantiate a waiter instance, the recommended way is to create one from an existing SDK client via waiter() method. Below is the sample code showing you how to wait until a table becomes active using synchronous waiters and how to wait until a table is deleted using asynchronous waiters.

Sync waiter

DynamoDbClient dynamo = DynamoDbClient.create();
DynamoDbWaiter waiter = dynamo.waiter();

WaiterResponse<DescribeTableResponse> waiterResponse = 
       waiter.waitUntilTableExists(r -> r.tableName("myTable"));

// print out the matched response with a tableStatus of ACTIVE
waiterResponse.matched().response().ifPresent(System.out::println); 

Async waiter

DynamoDbAsyncClient asyncDynamo = DynamoDbAsyncClient.create();
DynamoDbAsyncWaiter asyncWaiter = asyncDynamo.waiter();

CompletableFuture<WaiterResponse<DescribeTableResponse>> waiterResponse =
            asyncWaiter.waitUntilTableNotExists(r -> r.tableName("myTable"));

waiterResponse.whenComplete((r, t) -> {
    if (t == null) {
        // print out the matched ResourceNotFoundException
        r.matched().exception().ifPresent(System.out::println);
    }
}).join();

Customizing waiters

The waiters created from an existing SDK client are configured with optimal values defined by each service for different APIs. You can provide override configurations on the entire waiters or per request via WaiterOverrideConfiguration.

Waiter-level override configuration

To override the default configurations on the entire waiter, you need to use the waiter builder to instantiate the waiter.
Below is an example illustrating how to configure DynamoDbWaiter and DynamoDbAsyncWaiter:

// sync
DynamoDbWaiter waiter = 
       DynamoDbWaiter.builder()
                     .overrideConfiguration(b -> b.maxAttempts(10))
                     .client(dynamoDbClient)
                     .build();
// async
DynamoDbAsyncWaiter asyncWaiter = 
       DynamoDbAsyncWaiter.builder()
                          .client(dynamoDbAsyncClient)
                          .overrideConfiguration(o -> o.backoffStrategy(
                                 FixedDelayBackoffStrategy.create(Duration.ofSeconds(2))))
                          .scheduledExecutorService(Executors.newScheduledThreadPool(3))
                          .build();

Request-level override configuration

You can also apply configurations to certain operations. Below is an example of how to configure WaiterOverrideConfiguration per request

waiter.waitUntilTableNotExists(b -> b.tableName("myTable"), 
                               o -> o.maxAttempts(10));
                               
asyncWaiter.waitUntilTableExists(b -> b.tableName("myTable"), 
                                 o -> o.waitTimeout(Duration.ofMinutes(1)));

Conclusion

In this blog post, we showed you how easy it is to utilize waiters to wait for a resource to become available, such as DynamoDB table being active or an S3 bucket being created. To learn more, visit our Developer Guide and our API References . If you have any feedback let us know by opening a GitHub issue.