Skip to content

Commit 0bbf1b4

Browse files
authored
feature(Waiters): Add the reason for failure in the exception thrown whenever a waiter transitions to a failure state. (#5230)
* feature(Waiters): Add the reason for failure in the exception thrown whenever a waiter transitions to a failure state * Handle review comments
1 parent 4230705 commit 0bbf1b4

File tree

7 files changed

+185
-9
lines changed

7 files changed

+185
-9
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Add the reason for failure in the exception thrown whenever a waiter transitions to a failure state."
6+
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterClassSpec.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.Objects;
4141
import java.util.Optional;
4242
import java.util.function.Consumer;
43+
import java.util.function.Supplier;
4344
import java.util.stream.Collectors;
4445
import java.util.stream.Stream;
4546
import javax.lang.model.element.Modifier;
@@ -69,6 +70,12 @@
6970
*/
7071
public abstract class BaseWaiterClassSpec implements ClassSpec {
7172

73+
public static final String FAILURE_MESSAGE_FORMAT_FOR_PATH_MATCHER = "A waiter acceptor with the matcher (%s) "
74+
+ "was matched on parameter (%s=%s) and "
75+
+ "transitioned the waiter to failure state";
76+
public static final String FAILURE_MESSAGE_FORMAT_FOR_ERROR_MATCHER = "A waiter acceptor was matched on error "
77+
+ "condition (%s) and transitioned the waiter to "
78+
+ "failure state";
7279
private static final String WAITERS_USER_AGENT = "waiter";
7380
private final IntermediateModel model;
7481
private final String modelPackage;
@@ -459,16 +466,19 @@ private CodeBlock acceptor(Acceptor acceptor) {
459466
case "path":
460467
result.add("OnResponseAcceptor(");
461468
result.add(pathAcceptorBody(acceptor));
469+
addFailureMessageForPathMatcher(acceptor, result);
462470
result.add(")");
463471
break;
464472
case "pathAll":
465473
result.add("OnResponseAcceptor(");
466474
result.add(pathAllAcceptorBody(acceptor));
475+
addFailureMessageForPathMatcher(acceptor, result);
467476
result.add(")");
468477
break;
469478
case "pathAny":
470479
result.add("OnResponseAcceptor(");
471480
result.add(pathAnyAcceptorBody(acceptor));
481+
addFailureMessageForPathMatcher(acceptor, result);
472482
result.add(")");
473483
break;
474484
case "status":
@@ -483,6 +493,8 @@ private CodeBlock acceptor(Acceptor acceptor) {
483493
} else {
484494
result.add("OnExceptionAcceptor(");
485495
result.add(errorAcceptorBody(acceptor));
496+
addAcceptorFailureMessage(result, acceptor, () -> String.format(FAILURE_MESSAGE_FORMAT_FOR_ERROR_MATCHER,
497+
expectedValue(acceptor)));
486498
result.add(")");
487499
}
488500
break;
@@ -493,6 +505,27 @@ private CodeBlock acceptor(Acceptor acceptor) {
493505
return result.build();
494506
}
495507

508+
private void addFailureMessageForPathMatcher(Acceptor acceptor, CodeBlock.Builder result) {
509+
addAcceptorFailureMessage(result, acceptor,
510+
() -> String.format(FAILURE_MESSAGE_FORMAT_FOR_PATH_MATCHER,
511+
acceptor.getMatcher(),
512+
acceptor.getArgument(),
513+
expectedValue(acceptor)));
514+
}
515+
516+
private void addAcceptorFailureMessage(CodeBlock.Builder result, Acceptor acceptor, Supplier<String> messageSupplier) {
517+
if ("failure".equals(acceptor.getState())) {
518+
result.add(", ");
519+
result.add("$S", messageSupplier.get());
520+
}
521+
}
522+
523+
private static String expectedValue(Acceptor acceptor) {
524+
return acceptor.getExpected() instanceof JrsBoolean
525+
? String.valueOf(((JrsBoolean) acceptor.getExpected()).booleanValue())
526+
: acceptor.getExpected().asText();
527+
}
528+
496529
private CodeBlock.Builder booleanValueErrorBlock(Acceptor acceptor, Boolean expectedBoolean) {
497530
CodeBlock.Builder codeBlock = CodeBlock.builder();
498531
if (Boolean.FALSE.equals(expectedBoolean)) {
@@ -502,6 +535,8 @@ private CodeBlock.Builder booleanValueErrorBlock(Acceptor acceptor, Boolean expe
502535
codeBlock.add("OnExceptionAcceptor(");
503536
codeBlock.add("error -> errorCode(error) != null");
504537
}
538+
addAcceptorFailureMessage(codeBlock, acceptor, () -> String.format(FAILURE_MESSAGE_FORMAT_FOR_ERROR_MATCHER,
539+
expectedValue(acceptor)));
505540
codeBlock.add(")");
506541
return codeBlock;
507542
}

codegen/src/main/resources/software/amazon/awssdk/codegen/waiters/WaitersRuntime.java.resource

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import java.util.Arrays;
55
import java.util.Collection;
66
import java.util.Collections;
77
import java.util.List;
8+
import java.util.Optional;
89
import java.util.Objects;
910
import java.util.function.Function;
1011
import software.amazon.awssdk.annotations.Generated;
@@ -24,6 +25,10 @@ import software.amazon.awssdk.utils.ToString;
2425
@Generated("software.amazon.awssdk:codegen")
2526
@SdkInternalApi
2627
public final class WaitersRuntime {
28+
29+
private static final String FAILURE_MESSAGE_FORMAT_FOR_STATUS_MATCHER = "A waiter acceptor was matched on HTTP response "
30+
+ "status code (%d) and transitioned the waiter to "
31+
+ "failure state";
2732
/**
2833
* The default acceptors that should be matched *last* in the list of acceptors used by the SDK client waiters.
2934
*/
@@ -72,5 +77,11 @@ public final class WaitersRuntime {
7277

7378
return false;
7479
}
80+
81+
@Override
82+
public Optional<String> message() {
83+
return waiterState == WaiterState.FAILURE ? Optional.of(String.format(FAILURE_MESSAGE_FORMAT_FOR_STATUS_MATCHER,
84+
statusCode)) : Optional.empty();
85+
}
7586
}
7687
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/waiters/WaiterAcceptor.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,34 @@ public boolean matches(Throwable t) {
127127
};
128128
}
129129

130+
/**
131+
* Creates an error waiter acceptor which determines if the exception should transition the waiter to failure state
132+
* Overloaded method with errorMessage.
133+
*
134+
* @param errorPredicate the {@link Throwable} predicate
135+
* @param errorMessage Message with reason for failure.
136+
* @return a {@link WaiterAcceptor}
137+
*/
138+
static <T> WaiterAcceptor<T> errorOnExceptionAcceptor(Predicate<Throwable> errorPredicate, String errorMessage) {
139+
Validate.paramNotNull(errorPredicate, "errorPredicate");
140+
return new WaiterAcceptor<T>() {
141+
@Override
142+
public WaiterState waiterState() {
143+
return WaiterState.FAILURE;
144+
}
145+
146+
@Override
147+
public boolean matches(Throwable t) {
148+
return errorPredicate.test(t);
149+
}
150+
151+
@Override
152+
public Optional<String> message() {
153+
return Optional.of(errorMessage);
154+
}
155+
};
156+
}
157+
130158
/**
131159
* Creates a success waiter acceptor which determines if the exception should transition the waiter to success state
132160
*

test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,31 @@
126126
"expected" : true
127127
}
128128
]
129+
},
130+
"FailureForSpecificMatchers": {
131+
"delay": 1,
132+
"operation": "AllTypes",
133+
"maxAttempts": 40,
134+
"acceptors": [
135+
{
136+
"state": "failure",
137+
"matcher": "path",
138+
"argument": "StringMember",
139+
"expected": "UNEXPECTED_VALUE"
140+
},
141+
{
142+
"state": "failure",
143+
"matcher": "path",
144+
"argument": "BooleanMember",
145+
"expected": false
146+
},
147+
{
148+
"state": "failure",
149+
"matcher": "pathAny",
150+
"argument": "IntegerMember",
151+
"expected": 99
152+
}
153+
]
129154
}
130155
}
131156
}

test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersAsyncFunctionalTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import static org.mockito.Mockito.when;
2525

2626
import java.util.concurrent.CompletableFuture;
27+
import java.util.concurrent.CompletionException;
2728
import java.util.concurrent.ScheduledExecutorService;
2829
import org.junit.jupiter.api.AfterEach;
2930
import org.junit.jupiter.api.BeforeEach;
@@ -226,4 +227,23 @@ public void closeWaiterCreatedWithExecutorService_executorServiceDoesNotClose()
226227
newWaiter.close();
227228
verify(executorService, never()).shutdown();
228229
}
230+
231+
@Test
232+
public void failureResponse_withResponsePath_shouldThrowException() {
233+
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
234+
.stringMember("UNEXPECTED_VALUE")
235+
.sdkHttpResponse(SdkHttpResponse.builder()
236+
.statusCode(200)
237+
.build())
238+
.build();
239+
240+
CompletableFuture<AllTypesResponse> serviceFuture = CompletableFuture.completedFuture(response);
241+
when(asyncClient.allTypes(any(AllTypesRequest.class))).thenReturn(serviceFuture);
242+
assertThatThrownBy(() -> asyncWaiter.waitUntilFailureForSpecificMatchers(b -> b.build()).join())
243+
.hasMessageContaining("A waiter acceptor with the matcher (path) was matched on parameter "
244+
+ "(StringMember=UNEXPECTED_VALUE) and transitioned the waiter to failure state")
245+
.isInstanceOf(CompletionException.class);
246+
}
247+
248+
229249
}

test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ public void failureException_shouldThrowException() {
151151
.build())
152152
.build());
153153
assertThatThrownBy(() -> waiter.waitUntilAllTypesSuccess(SdkBuilder::build))
154-
.hasMessageContaining("transitioned the waiter to failure state")
154+
.hasMessageContaining("A waiter acceptor was matched on error condition"
155+
+ " (EmptyModeledException) and transitioned the waiter to failure state")
155156
.isInstanceOf(SdkClientException.class);
156157
}
157158

@@ -176,18 +177,20 @@ public void unexpectedResponse_shouldRetry() {
176177
}
177178

178179
@Test
179-
public void failureResponse_shouldThrowException() {
180+
public void failureResponse_withStatusCode_shouldThrowException() {
180181
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
181-
.sdkHttpResponse(SdkHttpResponse.builder()
182-
.statusCode(500)
183-
.build())
184-
.build();
182+
.sdkHttpResponse(SdkHttpResponse.builder()
183+
.statusCode(500)
184+
.build())
185+
.build();
185186
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
186187
assertThatThrownBy(() -> waiter.waitUntilAllTypesSuccess(SdkBuilder::build))
187-
.hasMessageContaining("A waiter acceptor was matched and transitioned the waiter to failure state")
188+
.hasMessageContaining("A waiter acceptor was matched on HTTP response status code (500)"
189+
+ " and transitioned the waiter to failure state")
188190
.isInstanceOf(SdkClientException.class);
189191
}
190192

193+
191194
@Test
192195
public void closeWaiterCreatedWithClient_clientDoesNotClose() {
193196
waiter.close();
@@ -216,7 +219,8 @@ public void errorMatcherWithExpectedTrueFails_withAPIError() {
216219
.build());
217220

218221
assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedTrueFails(SdkBuilder::build))
219-
.hasMessageContaining("A waiter acceptor was matched and transitioned the waiter to failure state")
222+
.hasMessageContaining("A waiter acceptor was matched on error condition (true) "
223+
+ "and transitioned the waiter to failure state")
220224
.isInstanceOf(SdkClientException.class);
221225
}
222226

@@ -323,7 +327,8 @@ public void errorMatcherWithExpectedFalseAndStateAsFailure_whenAPISuccess() {
323327

324328
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
325329
assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build))
326-
.hasMessageContaining("A waiter acceptor was matched and transitioned the waiter to failure state")
330+
.hasMessageContaining("A waiter acceptor was matched on error condition (false) "
331+
+ "and transitioned the waiter to failure state")
327332
.isInstanceOf(SdkClientException.class);
328333

329334
}
@@ -386,4 +391,50 @@ public void errorMatcherWithExpectedFalseRetries_passesWhenApiReturnErrors() {
386391
// Empty because the waiter specifically waits for Error case.
387392
assertThat(waiterResponse.matched().response()).isEmpty();
388393
}
394+
395+
@Test
396+
public void failureResponse_withResponsePath_shouldThrowException() {
397+
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
398+
.stringMember("UNEXPECTED_VALUE")
399+
.sdkHttpResponse(SdkHttpResponse.builder()
400+
.statusCode(200)
401+
.build())
402+
.build();
403+
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
404+
assertThatThrownBy(() -> waiter.waitUntilFailureForSpecificMatchers(SdkBuilder::build))
405+
.hasMessageContaining("A waiter acceptor with the matcher (path) was matched on parameter "
406+
+ "(StringMember=UNEXPECTED_VALUE) and transitioned the waiter to failure state")
407+
.isInstanceOf(SdkClientException.class);
408+
}
409+
410+
@Test
411+
public void failureResponse_withResponsePathForBoolean_shouldThrowException() {
412+
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
413+
.booleanMember(false)
414+
.sdkHttpResponse(SdkHttpResponse.builder()
415+
.statusCode(200)
416+
.build())
417+
.build();
418+
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
419+
assertThatThrownBy(() -> waiter.waitUntilFailureForSpecificMatchers(SdkBuilder::build))
420+
.hasMessageContaining("A waiter acceptor with the matcher (path) was matched on parameter "
421+
+ "(BooleanMember=false) and transitioned the waiter to failure state")
422+
.isInstanceOf(SdkClientException.class);
423+
}
424+
425+
@Test
426+
public void failureResponse_withResponsePathForInteger_shouldThrowException() {
427+
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
428+
.integerMember(99)
429+
.sdkHttpResponse(SdkHttpResponse.builder()
430+
.statusCode(200)
431+
.build())
432+
.build();
433+
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
434+
assertThatThrownBy(() -> waiter.waitUntilFailureForSpecificMatchers(SdkBuilder::build))
435+
.hasMessageContaining("A waiter acceptor with the matcher (pathAny) was matched on parameter "
436+
+ "(IntegerMember=99) and transitioned the waiter to failure state")
437+
.isInstanceOf(SdkClientException.class);
438+
}
439+
389440
}

0 commit comments

Comments
 (0)