diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/WaiterDocs.java b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/WaiterDocs.java index aa31ab9559c9..e4a7caecf18d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/WaiterDocs.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/WaiterDocs.java @@ -36,6 +36,12 @@ public static String waiterInterfaceJavadoc() { public static CodeBlock waiterOperationJavadoc(ClassName className, Map.Entry waiterDefinition, OperationModel operationModel) { + String returnStatement = "WaiterResponse containing either a response or an exception that has matched with the waiter " + + "success condition"; + + String asyncReturnStatement = "CompletableFuture containing the WaiterResponse. It completes successfully when the " + + "resource enters into a desired state or exceptionally when it is determined that the " + + "resource will never enter into the desired state."; String javadocs = new DocumentationBuilder().description("Polls {@link $T#$N} API until the desired condition " + "{@code $N} is met, " + "or until it is determined that the resource will never " @@ -43,11 +49,8 @@ public static CodeBlock waiterOperationJavadoc(ClassName className, Map.EntryThis is a convenience method to create an instance of " + "the request builder without the need " - + "to create one manually using {@link $T.builder()} ") + + "to create one manually using {@link $T#builder()} ") .param(operationModel.getInput().getVariableName(), "The consumer that will" + " configure the " + "request to be used" @@ -94,14 +97,18 @@ public static CodeBlock waiterBuilderMethodJavadoc(ClassName className) { .build(); } - public static CodeBlock waiterCreateMethodJavadoc(ClassName className) { + public static CodeBlock waiterCreateMethodJavadoc(ClassName waiterClassName, ClassName clientClassName) { String javadocs = new DocumentationBuilder() - .description("Create an instance of {@link $T} with the default configuration") + .description("Create an instance of {@link $T} with the default configuration. \n" + + "

A default {@link $T} will be created to poll resources. It is recommended " + + "to share a single instance of the waiter created via this method. If it is not desirable " + + "to share a waiter instance, invoke {@link #close()} to release the resources once the waiter" + + " is not needed.") .returns("an instance of {@link $T}") .build(); return CodeBlock.builder() - .add(javadocs, className, className) + .add(javadocs, waiterClassName, clientClassName, waiterClassName) .build(); } @@ -121,7 +128,7 @@ public static CodeBlock waiterBuilderPollingStrategy() { public static CodeBlock waiterBuilderPollingStrategyConsumerBuilder() { String javadocs = new DocumentationBuilder() .description("This is a convenient method to pass the override configuration without the need to " - + "create an instance manually via {@link $T.builder()}") + + "create an instance manually via {@link $T#builder()}") .param("overrideConfiguration", "The consumer that will configure the overrideConfiguration") .see("#overrideConfiguration(WaiterOverrideConfiguration)") .returns("a reference to this object so that method calls can be chained together.") @@ -149,7 +156,7 @@ public static CodeBlock waiterBuilderScheduledExecutorServiceJavadoc() { public static CodeBlock waiterBuilderClientJavadoc(ClassName className) { String javadocs = new DocumentationBuilder() .description("Defines the {@link $T} to use when polling a resource") - .description("Sets a custom {@link $T} that will be used to pool the resource \n " + .description("Sets a custom {@link $T} that will be used to poll the resource \n " + "

This SDK client must be closed by the caller when it is ready to be disposed. The" + " SDK will not close the client when the waiter is closed") .param("client", "the client to send the request") @@ -216,4 +223,18 @@ public static CodeBlock waiterOperationWithOverrideConfig(ClassName clientClassN .add(javadocs, clientClassName, opModel.getMethodName(), waiterDefinition.getKey()) .build(); } + + public static CodeBlock waiterMethodInClient(ClassName waiterClassName) { + String javadocs = new DocumentationBuilder() + .description("Create an instance of {@link $T} using this client. \n" + + "

Waiters created via this method are managed by the SDK and resources will be released " + + "when the service client is closed.") + .returns("an instance of {@link $T}") + .build(); + + return CodeBlock.builder() + .add(javadocs, waiterClassName, waiterClassName) + .build(); + } + } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java index 63ec1281f527..e9f023701841 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java @@ -36,6 +36,7 @@ import software.amazon.awssdk.codegen.docs.ClientType; import software.amazon.awssdk.codegen.docs.DocConfiguration; import software.amazon.awssdk.codegen.docs.SimpleMethodOverload; +import software.amazon.awssdk.codegen.docs.WaiterDocs; import software.amazon.awssdk.codegen.model.config.customization.UtilitiesMethod; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; @@ -452,7 +453,7 @@ private MethodSpec waiterMethod() { .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) .returns(poetExtensions.getAsyncWaiterInterface()) .addStatement("throw new $T()", UnsupportedOperationException.class) - .addJavadoc("Creates an instance of {@link $T} object", poetExtensions.getAsyncWaiterInterface()) + .addJavadoc(WaiterDocs.waiterMethodInClient(poetExtensions.getAsyncWaiterInterface())) .build(); } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java index 3e8e1e247df4..4b865879df57 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java @@ -36,6 +36,7 @@ import software.amazon.awssdk.codegen.docs.ClientType; import software.amazon.awssdk.codegen.docs.DocConfiguration; import software.amazon.awssdk.codegen.docs.SimpleMethodOverload; +import software.amazon.awssdk.codegen.docs.WaiterDocs; import software.amazon.awssdk.codegen.model.config.customization.UtilitiesMethod; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; @@ -465,7 +466,7 @@ private MethodSpec waiterMethod() { .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) .addStatement("throw new $T()", UnsupportedOperationException.class) .returns(poetExtensions.getSyncWaiterInterface()) - .addJavadoc("Creates an instance of {@link $T} object", poetExtensions.getSyncWaiterInterface()) + .addJavadoc(WaiterDocs.waiterMethodInClient(poetExtensions.getSyncWaiterInterface())) .build(); } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterInterfaceSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterInterfaceSpec.java index bad9c73e54d1..2fc232214739 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterInterfaceSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterInterfaceSpec.java @@ -67,7 +67,8 @@ public TypeSpec poetSpec() { .build()); result.addMethod(MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addJavadoc(WaiterDocs.waiterCreateMethodJavadoc(className())) + .addJavadoc(WaiterDocs.waiterCreateMethodJavadoc(className(), + clientClassName())) .returns(className()) .addStatement("return $T.builder().build()", waiterImplName()) .build()); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/waiters/query-async-waiter-interface.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/waiters/query-async-waiter-interface.java index 4af4d4670700..67c242af48cb 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/waiters/query-async-waiter-interface.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/waiters/query-async-waiter-interface.java @@ -25,8 +25,9 @@ public interface QueryAsyncWaiter extends SdkAutoCloseable { * * @param aPostOperationRequest * the request to be used for polling - * @return CompletableFuture of the WaiterResponse containing either a response or an exception that has matched - * with the waiter success condition + * @return CompletableFuture containing the WaiterResponse. It completes successfully when the resource enters into + * a desired state or exceptionally when it is determined that the resource will never enter into the + * desired state. */ default CompletableFuture> waitUntilPostOperationSuccess( APostOperationRequest aPostOperationRequest) { @@ -38,7 +39,7 @@ default CompletableFuture> waitUntilPostO * met, or until it is determined that the resource will never enter into the desired state. *

* This is a convenience method to create an instance of the request builder without the need to create one manually - * using {@link APostOperationRequest.builder()} + * using {@link APostOperationRequest#builder()} * * @param aPostOperationRequest * The consumer that will configure the request to be used for polling @@ -97,7 +98,11 @@ static Builder builder() { } /** - * Create an instance of {@link QueryAsyncWaiter} with the default configuration + * Create an instance of {@link QueryAsyncWaiter} with the default configuration. + *

+ * A default {@link QueryAsyncClient} will be created to poll resources. It is recommended to share a single + * instance of the waiter created via this method. If it is not desirable to share a waiter instance, invoke + * {@link #close()} to release the resources once the waiter is not needed. * * @return an instance of {@link QueryAsyncWaiter} */ @@ -130,7 +135,7 @@ interface Builder { /** * This is a convenient method to pass the override configuration without the need to create an instance - * manually via {@link WaiterOverrideConfiguration.builder()} + * manually via {@link WaiterOverrideConfiguration#builder()} * * @param overrideConfiguration * The consumer that will configure the overrideConfiguration @@ -144,7 +149,7 @@ default Builder overrideConfiguration(Consumer * This SDK client must be closed by the caller when it is ready to be disposed. The SDK will not close the * client when the waiter is closed diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/waiters/query-sync-waiter-interface.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/waiters/query-sync-waiter-interface.java index e6e23f0f1ecc..2f9fc13c16a0 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/waiters/query-sync-waiter-interface.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/waiters/query-sync-waiter-interface.java @@ -35,7 +35,7 @@ default WaiterResponse waitUntilPostOperationSuccess(APo * until it is determined that the resource will never enter into the desired state. *

* This is a convenience method to create an instance of the request builder without the need to create one manually - * using {@link APostOperationRequest.builder()} + * using {@link APostOperationRequest#builder()} * * @param aPostOperationRequest * The consumer that will configure the request to be used for polling @@ -94,7 +94,11 @@ static Builder builder() { } /** - * Create an instance of {@link QueryWaiter} with the default configuration + * Create an instance of {@link QueryWaiter} with the default configuration. + *

+ * A default {@link QueryClient} will be created to poll resources. It is recommended to share a single instance + * of the waiter created via this method. If it is not desirable to share a waiter instance, invoke {@link #close()} + * to release the resources once the waiter is not needed. * * @return an instance of {@link QueryWaiter} */ @@ -115,7 +119,7 @@ interface Builder { /** * This is a convenient method to pass the override configuration without the need to create an instance - * manually via {@link WaiterOverrideConfiguration.builder()} + * manually via {@link WaiterOverrideConfiguration#builder()} * * @param overrideConfiguration * The consumer that will configure the overrideConfiguration @@ -129,7 +133,7 @@ default Builder overrideConfiguration(Consumer * This SDK client must be closed by the caller when it is ready to be disposed. The SDK will not close the * client when the waiter is closed diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/DefaultWaiterResponse.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/DefaultWaiterResponse.java index cf376b1061d5..5a46da80f1ac 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/DefaultWaiterResponse.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/DefaultWaiterResponse.java @@ -31,6 +31,7 @@ public final class DefaultWaiterResponse implements WaiterResponse { private final T result; private final Throwable exception; private final int attemptsExecuted; + private final ResponseOrException matched; private DefaultWaiterResponse(Builder builder) { mutuallyExclusive("response and exception are mutually exclusive, set only one on the Builder", @@ -39,16 +40,18 @@ private DefaultWaiterResponse(Builder builder) { this.exception = builder.exception; this.attemptsExecuted = Validate.paramNotNull(builder.attemptsExecuted, "attemptsExecuted"); Validate.isPositive(builder.attemptsExecuted, "attemptsExecuted"); + matched = result != null ? + ResponseOrException.response(result) : + ResponseOrException.exception(exception); } - public static Builder builder() { return new Builder<>(); } @Override public ResponseOrException matched() { - return result != null ? ResponseOrException.response(result) : ResponseOrException.exception(exception); + return matched; } @Override @@ -89,7 +92,9 @@ public String toString() { ToString toString = ToString.builder("DefaultWaiterResponse") .add("attemptsExecuted", attemptsExecuted); - matched().apply(r -> toString.add("response", r), e -> toString.add("exception", e.getMessage())); + matched.response().ifPresent(r -> toString.add("response", result)); + matched.exception().ifPresent(r -> toString.add("exception", exception)); + return toString.build(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/ResponseOrException.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/ResponseOrException.java index 50b94b376313..5bb839d93999 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/ResponseOrException.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/waiters/ResponseOrException.java @@ -16,74 +16,53 @@ package software.amazon.awssdk.core.internal.waiters; import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; -import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.annotations.SdkPublicApi; /** * Represents a value that can be either a response or a Throwable * * @param response type */ -@SdkProtectedApi +@SdkPublicApi public final class ResponseOrException { private final Optional response; private final Optional exception; - private ResponseOrException(Optional l, Optional r) { - response = l; - exception = r; + private ResponseOrException(Optional response, Optional exception) { + this.response = response; + this.exception = exception; } /** - * Maps the Either to a type and returns the resolved value (which may be from the left or the right value). - * - * @param lFunc Function that maps the left value if present. - * @param rFunc Function that maps the right value if present. - * @param Type that both the left and right should be mapped to. - * @return Mapped value from either lFunc or rFunc depending on which value is present. + * @return the optional response that has matched with the waiter success condition */ - public T map( - Function lFunc, - Function rFunc) { - return response.map(lFunc).orElseGet(() -> exception.map(rFunc).get()); - } - public Optional response() { return response; } - public Optional exception() { - return exception; - } - /** - * Apply the consumers to the left or the right value depending on which is present. - * - * @param lFunc Consumer of left value, invoked if left value is present. - * @param rFunc Consumer of right value, invoked if right value is present. + * @return the optional exception that has matched with the waiter success condition */ - public void apply(Consumer lFunc, Consumer rFunc) { - response.ifPresent(lFunc); - exception.ifPresent(rFunc); + public Optional exception() { + return exception; } /** - * Create a new Either with the left type. + * Create a new ResponseOrException with the response * - * @param value Left value - * @param Left type + * @param value response + * @param Response type */ public static ResponseOrException response(R value) { return new ResponseOrException<>(Optional.of(value), Optional.empty()); } /** - * Create a new Either with the right type. + * Create a new ResponseOrException with the exception * - * @param value Right value - * @param Left type + * @param value exception + * @param Response type */ public static ResponseOrException exception(Throwable value) { return new ResponseOrException<>(Optional.empty(), Optional.of(value)); diff --git a/services/dynamodb/src/it/java/software/amazon/awssdk/services/dynamodb/WaitersIntegrationTest.java b/services/dynamodb/src/it/java/software/amazon/awssdk/services/dynamodb/WaitersIntegrationTest.java index e8936007d553..35b346134aac 100644 --- a/services/dynamodb/src/it/java/software/amazon/awssdk/services/dynamodb/WaitersIntegrationTest.java +++ b/services/dynamodb/src/it/java/software/amazon/awssdk/services/dynamodb/WaitersIntegrationTest.java @@ -20,6 +20,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import org.assertj.core.api.Condition; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -29,9 +30,11 @@ import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse; +import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; import software.amazon.awssdk.services.dynamodb.model.KeyType; import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; import software.amazon.awssdk.services.dynamodb.waiters.DynamoDbAsyncWaiter; import software.amazon.awssdk.services.dynamodb.waiters.DynamoDbWaiter; @@ -69,6 +72,13 @@ public static void setUp() { public static void cleanUp() { dynamo.deleteTable(DeleteTableRequest.builder().tableName(TABLE_NAME).build()); + WaiterResponse waiterResponse = + dynamo.waiter().waitUntilTableNotExists(b -> b.tableName(TABLE_NAME)); + + assertThat(waiterResponse.matched().response()).isEmpty(); + assertThat(waiterResponse.matched().exception()).hasValueSatisfying( + new Condition<>(t -> t instanceof ResourceNotFoundException, "ResourceNotFoundException")); + dynamo.close(); dynamoAsync.close(); } @@ -80,19 +90,20 @@ public void checkTableExist_withSyncWaiter() { DescribeTableRequest.builder().tableName(TABLE_NAME).build()); assertThat(response.attemptsExecuted()).isGreaterThanOrEqualTo(1); - assertThat(response.matched().response().get().table().tableName()).isEqualTo(TABLE_NAME); + assertThat(response.matched().response()).hasValueSatisfying(b -> assertThat(b.table().tableName()).isEqualTo(TABLE_NAME)); + assertThat(response.matched().exception()).isEmpty(); } @Test - public void checkTableExist_withAsyncWaiter() throws ExecutionException, InterruptedException { + public void checkTableExist_withAsyncWaiter() { DynamoDbAsyncWaiter asyncWaiter = dynamoAsync.waiter(); CompletableFuture> responseFuture = asyncWaiter.waitUntilTableExists( DescribeTableRequest.builder().tableName(TABLE_NAME).build()); - responseFuture.join(); + WaiterResponse response = responseFuture.join(); - assertThat(responseFuture.get().attemptsExecuted()).isGreaterThanOrEqualTo(1); - assertThat(responseFuture.get().matched().response().get().table().tableName()).isEqualTo(TABLE_NAME); + assertThat(response.attemptsExecuted()).isGreaterThanOrEqualTo(1); + assertThat(response.matched().response()).hasValueSatisfying(b -> assertThat(b.table().tableName()).isEqualTo(TABLE_NAME)); + assertThat(response.matched().exception()).isEmpty(); } - } \ No newline at end of file diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json index b3b096e5d483..b629e680af14 100644 --- a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json @@ -16,6 +16,11 @@ "matcher": "status", "expected": 404 }, + { + "matcher": "error", + "expected": "EmptyModeledException", + "state": "failure" + }, { "state": "failure", "matcher": "status", diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersAsyncFunctionalTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersAsyncFunctionalTest.java index 68694afdf08a..e2bde5eebf77 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersAsyncFunctionalTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersAsyncFunctionalTest.java @@ -29,6 +29,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.exception.SdkServiceException; import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; @@ -38,6 +39,7 @@ import software.amazon.awssdk.services.restjsonwithwaiters.RestJsonWithWaitersAsyncClient; import software.amazon.awssdk.services.restjsonwithwaiters.model.AllTypesRequest; import software.amazon.awssdk.services.restjsonwithwaiters.model.AllTypesResponse; +import software.amazon.awssdk.services.restjsonwithwaiters.model.EmptyModeledException; import software.amazon.awssdk.services.restjsonwithwaiters.waiters.RestJsonWithWaitersAsyncWaiter; import software.amazon.awssdk.utils.CompletableFutureUtils; import software.amazon.awssdk.utils.builder.SdkBuilder; @@ -185,6 +187,18 @@ public void failureResponse_shouldThrowException() { .hasCauseInstanceOf(SdkClientException.class); } + @Test + public void failureException_shouldThrowException() { + when(asyncClient.allTypes(any(AllTypesRequest.class))).thenReturn(CompletableFutureUtils.failedFuture(EmptyModeledException.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .errorCode("EmptyModeledException") + .build()) + .build())); + assertThatThrownBy(() -> asyncWaiter.waitUntilAllTypesSuccess(SdkBuilder::build).join()) + .hasMessageContaining("transitioned the waiter to failure state") + .hasCauseInstanceOf(SdkClientException.class); + } + @Test public void closeWaiterCreatedWithClient_clientDoesNotClose() { asyncWaiter.close(); diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java index 08e31d21ca75..3da0198d74f7 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java @@ -26,6 +26,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.exception.SdkServiceException; import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; @@ -35,6 +36,7 @@ import software.amazon.awssdk.services.restjsonwithwaiters.RestJsonWithWaitersClient; import software.amazon.awssdk.services.restjsonwithwaiters.model.AllTypesRequest; import software.amazon.awssdk.services.restjsonwithwaiters.model.AllTypesResponse; +import software.amazon.awssdk.services.restjsonwithwaiters.model.EmptyModeledException; import software.amazon.awssdk.services.restjsonwithwaiters.waiters.RestJsonWithWaitersWaiter; import software.amazon.awssdk.utils.builder.SdkBuilder; @@ -140,7 +142,19 @@ public void unexpectedException_shouldNotRetry() { .hasMessageContaining("An exception was thrown and did not match any waiter acceptors") .isInstanceOf(SdkClientException.class); } - + + @Test + public void failureException_shouldThrowException() { + when(client.allTypes(any(AllTypesRequest.class))).thenThrow(EmptyModeledException.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .errorCode("EmptyModeledException") + .build()) + .build()); + assertThatThrownBy(() -> waiter.waitUntilAllTypesSuccess(SdkBuilder::build)) + .hasMessageContaining("transitioned the waiter to failure state") + .isInstanceOf(SdkClientException.class); + } + @Test public void unexpectedResponse_shouldRetry() { AllTypesResponse response1 = (AllTypesResponse) AllTypesResponse.builder()