Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(Waiters): Boolean Value support for Error Matchers for Waiters #5084

Merged
merged 3 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) != 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 the waiter does not tell what to do if
* there is an 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the response empty instead of containing the EmptyModeledException thrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is existing case coming from second part of acceptor as below where we say if matcher is error and if we get exception as EmptyModeledException then transistion to success state , since we donot have any valid response from response from server we retrurn empty response

    "ErrorMatcherWithExpectedFalseRetries": {
      "delay": 1,
      "operation": "AllTypes",
      "maxAttempts": 40,
      "acceptors": [
        {
          "matcher" : "error",
          "state" : "retry",
          "expected" : false
        },
        {
          "matcher": "error",
          "expected": "EmptyModeledException",
          "state": "success"
        }
      ]
    
    ```

}
}
Loading