Skip to content

Commit

Permalink
feature(Waiters): Added support for Waiters specifically for Matcher…
Browse files Browse the repository at this point in the history
…s with Error to accept true/false value not as string but as boolean values such that True value is to match on any error code, or boolean false to test if no errors were encountered as per the SDK Waiter specs.
  • Loading branch information
joviegas committed Apr 9, 2024
1 parent 7a27fe5 commit b494abf
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-f9cffed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Added support for Waiters specifically for Matchers with Error to accept true/false value not as string but as boolean values such that True value is to match on any error code, or boolean false to test if no errors were encountered as per the SDK Waiter specs."
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static javax.lang.model.element.Modifier.STATIC;
import static software.amazon.awssdk.utils.internal.CodegenNamingUtils.lowercaseFirstChar;

import com.fasterxml.jackson.jr.stree.JrsBoolean;
import com.fasterxml.jackson.jr.stree.JrsString;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
Expand Down Expand Up @@ -477,9 +478,13 @@ private CodeBlock acceptor(Acceptor acceptor) {
return CodeBlock.of("new $T($L, $T.$L)", waitersRuntimeClass().nestedClass("ResponseStatusAcceptor"),
expected, WaiterState.class, waiterState(acceptor));
case "error":
result.add("OnExceptionAcceptor(");
result.add(errorAcceptorBody(acceptor));
result.add(")");
if (acceptor.getExpected() instanceof JrsBoolean) {
result.add(booleanValueErrorBlock(acceptor, Boolean.parseBoolean(acceptor.getExpected().asText())).build());
} else {
result.add("OnExceptionAcceptor(");
result.add(errorAcceptorBody(acceptor));
result.add(")");
}
break;
default:
throw new IllegalArgumentException("Unsupported acceptor matcher: " + acceptor.getMatcher());
Expand All @@ -488,6 +493,19 @@ private CodeBlock acceptor(Acceptor acceptor) {
return result.build();
}

private CodeBlock.Builder booleanValueErrorBlock(Acceptor acceptor, Boolean expectedBoolean) {
CodeBlock.Builder codeBlock = CodeBlock.builder();
if (Boolean.FALSE.equals(expectedBoolean)) {
codeBlock.add("OnResponseAcceptor(");
codeBlock.add(trueForAllResponse());
} else {
codeBlock.add("OnExceptionAcceptor(");
codeBlock.add("error -> errorCode(error) " + (expectedBoolean ? "!=" : "==") + " null");
}
codeBlock.add(")");
return codeBlock;
}

private String waiterState(Acceptor acceptor) {
switch (acceptor.getState()) {
case "success":
Expand Down Expand Up @@ -546,6 +564,12 @@ private CodeBlock pathAnyAcceptorBody(Acceptor acceptor) {
.build();
}

private CodeBlock trueForAllResponse() {
return CodeBlock.builder()
.add("response -> true")
.build();
}

private CodeBlock errorAcceptorBody(Acceptor acceptor) {
String expected = acceptor.getExpected().asText();
String expectedType = acceptor.getExpected() instanceof JrsString ? "$S" : "$L";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,105 @@
"expected": 500
}
]
},
"ErrorMatcherWithExpectedTrueFails": {
"delay": 1,
"operation": "AllTypes",
"maxAttempts": 40,
"acceptors": [
{
"matcher": "error",
"expected": true,
"state": "failure"
}
]
},
"ErrorMatcherWithExpectedTrueAndStateAsSuccess": {
"delay": 1,
"operation": "AllTypes",
"maxAttempts": 40,
"acceptors": [
{
"matcher" : "error",
"state" : "success",
"expected" : true
}
]
},
"ErrorMatcherWithExpectedFalseSuccess": {
"delay": 1,
"operation": "AllTypes",
"maxAttempts": 40,
"acceptors": [
{
"matcher" : "error",
"state" : "success",
"expected" : false
}
]
},
"ErrorMatcherWithExpectedFalseFails": {
"delay": 1,
"operation": "AllTypes",
"maxAttempts": 40,
"acceptors": [
{
"expected": false,
"matcher": "error",
"state": "failure"
}
]
},
"ErrorMatcherWithExpectedFalseRetries": {
"delay": 1,
"operation": "AllTypes",
"maxAttempts": 40,
"acceptors": [
{
"matcher" : "error",
"state" : "retry",
"expected" : false
},
{
"matcher": "error",
"expected": "EmptyModeledException",
"state": "success"
}
]
},
"SuccessMatcherWith200Pass404RetryErrorMatcherWithExpectedTrueFails": {
"delay": 1,
"operation": "AllTypes",
"maxAttempts": 40,
"acceptors": [
{
"expected": 200,
"matcher": "status",
"state": "success"
},
{
"state": "retry",
"matcher": "status",
"expected": 404
},
{
"matcher": "error",
"expected": true,
"state": "failure"
}
]
},
"ErrorMatcherWithExpectedFalseRetriesAndSuccessMatcherWith200Success": {
"delay": 1,
"operation": "AllTypes",
"maxAttempts": 40,
"acceptors": [
{
"matcher" : "error",
"state" : "retry",
"expected" : true
}
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,196 @@ public void closeWaiterCreatedWithClient_clientDoesNotClose() {
verify(client, never()).close();
}

@Test
public void errorMatcherWithExpectedTrueFails_withAPISuccess() {
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
.sdkHttpResponse(SdkHttpResponse.builder()
.statusCode(200)
.build())
.build();
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedTrueFails(SdkBuilder::build))
.hasMessageContaining("The waiter has exceeded the max retry attempts: 3")
.isInstanceOf(SdkClientException.class);
}

@Test
public void errorMatcherWithExpectedTrueFails_withAPIError() {
when(client.allTypes(any(AllTypesRequest.class))).thenThrow(EmptyModeledException.builder()
.awsErrorDetails(AwsErrorDetails.builder()
.errorCode("EmptyModeledException")
.build())
.build());

assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedTrueFails(SdkBuilder::build))
.hasMessageContaining("A waiter acceptor was matched and transitioned the waiter to failure state")
.isInstanceOf(SdkClientException.class);
}

/**
* If we are not getting any errors then fail it { "expected": false, "matcher": "error", "state": "failure" }
*/
@Test
public void errorMatcherWithExpectedFalseFails() {
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
.sdkHttpResponse(SdkHttpResponse.builder()
.statusCode(200)
.build())
.build();
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);

assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build))
.hasMessageContaining("transitioned the waiter to failure state")
.isInstanceOf(SdkClientException.class);
}

@Test
public void untilSuccessMatcherWith200Pass404RetryErrorMatcherWithExpectedTrueFails() {
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
.sdkHttpResponse(SdkHttpResponse.builder()
.statusCode(200)
.build())
.build();
when(client.allTypes(any(AllTypesRequest.class))).thenThrow(SdkServiceException.builder().statusCode(404).build())
.thenReturn(response);
WaiterResponse<AllTypesResponse> waiterResponse =
waiter.waitUntilSuccessMatcherWith200Pass404RetryErrorMatcherWithExpectedTrueFails(SdkBuilder::build);
assertThat(waiterResponse.attemptsExecuted()).isEqualTo(2);
}

@Test
public void errorMatcherWithExpectedFalseSuccess_APISuccess() {
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
.sdkHttpResponse(SdkHttpResponse.builder()
.statusCode(200)
.build())
.build();
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
WaiterResponse<AllTypesResponse> allTypesResponseWaiterResponse =
waiter.waitUntilErrorMatcherWithExpectedFalseSuccess(SdkBuilder::build);
assertThat(allTypesResponseWaiterResponse.attemptsExecuted()).isEqualTo(1);
}

// Error reported but not mentioned in Waiter acceptors
@Test
public void errorMatcherWithExpectedFalseSuccess_APIFailure() {
when(client.allTypes(any(AllTypesRequest.class))).thenThrow(SdkServiceException.builder().statusCode(404).build());
assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseSuccess(SdkBuilder::build))
.hasMessageContaining("An exception was thrown and did not match any waiter acceptors")
.isInstanceOf(SdkClientException.class);
}

@Test
public void errorMatcherWithExpectedTrueAndStateAsSuccess_ApiError() {
when(client.allTypes(any(AllTypesRequest.class))).thenThrow(EmptyModeledException.builder()
.awsErrorDetails(AwsErrorDetails.builder()
.errorCode("EmptyModeledException")
.build())
.build());
WaiterResponse<AllTypesResponse> allTypesResponseWaiterResponse =
waiter.waitUntilErrorMatcherWithExpectedTrueAndStateAsSuccess(SdkBuilder::build);
assertThat(allTypesResponseWaiterResponse.attemptsExecuted()).isEqualTo(1);
}

@Test
public void errorMatcherWithExpectedTrueAndStateAsSuccess_ApiSuccess() {
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
.sdkHttpResponse(SdkHttpResponse.builder()
.statusCode(200)
.build())
.build();
when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedTrueAndStateAsSuccess(SdkBuilder::build))
.hasMessageContaining("The waiter has exceeded the max retry attempts:")
.isInstanceOf(SdkClientException.class);

}

/**
* Case with the model just defined that it should Fail when there are no Errors But waitor does not tell what to do if there
* is a error.
*/
@Test
public void errorMatcherWithExpectedFalse_TerminatesWaitingIfErrorReported() {
when(client.allTypes(any(AllTypesRequest.class))).thenThrow(SdkServiceException.builder().statusCode(404).build());
assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build))
.hasMessageContaining("An exception was thrown and did not match any waiter acceptors")
.isInstanceOf(SdkClientException.class);

}

@Test
public void errorMatcherWithExpectedFalseAndStateAsFailure_whenAPISuccess() {
AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder()
.sdkHttpResponse(SdkHttpResponse.builder()
.statusCode(200)
.build())
.build();


when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response);
assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build))
.hasMessageContaining("A waiter acceptor was matched and transitioned the waiter to failure state")
.isInstanceOf(SdkClientException.class);

}

@Test
public void errorMatcherWithExpectedFalseAndStateAsFailure_whenAPIErrors() {
when(client.allTypes(any(AllTypesRequest.class))).thenThrow(EmptyModeledException.builder()
.awsErrorDetails(AwsErrorDetails.builder()
.errorCode("EmptyModeledException")
.build())
.build());


assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build))
.hasMessageContaining("An exception was thrown and did not match any waiter acceptors")
.isInstanceOf(SdkClientException.class);

}

@Test
public void errorMatcherWithExpectedFalseRetries_exhaustAllRetries() {
AllTypesResponse response =
(AllTypesResponse) AllTypesResponse.builder()
.sdkHttpResponse(SdkHttpResponse.builder().statusCode(200)
.build())
.build();
when(client.allTypes(any(AllTypesRequest.class)))
.thenReturn(response);
assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseRetries(AllTypesRequest.builder().build()))
.isInstanceOf(SdkClientException.class).hasMessageContaining("The waiter has exceeded the max retry attempts: 3");
}

/**
* This is a case were we want to check if an item is deleted
* In this case waiter first calls will say item exist
* and then its deleted it will throw exception
*/
@Test
public void errorMatcherWithExpectedFalseRetries_passesWhenApiReturnErrors() {
AllTypesResponse response =
(AllTypesResponse) AllTypesResponse.builder()
.sdkHttpResponse(SdkHttpResponse.builder()
.statusCode(200)
.build())
.build();
when(client.allTypes(any(AllTypesRequest.class)))
.thenReturn(response)
.thenReturn(response)
.thenThrow(
EmptyModeledException.builder()
.awsErrorDetails(AwsErrorDetails.builder()
.errorCode("EmptyModeledException")
.build())
.build()

);
WaiterResponse<AllTypesResponse> waiterResponse =
waiter.waitUntilErrorMatcherWithExpectedFalseRetries(AllTypesRequest.builder().build());
assertThat(waiterResponse.attemptsExecuted()).isEqualTo(3);
// Empty because the waiter specifically waits for Error case.
assertThat(waiterResponse.matched().response()).isEmpty();
}
}

0 comments on commit b494abf

Please sign in to comment.