diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index 16109778d..038704931 100644 --- a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -60,4 +60,4 @@ public String handleRequest(Input input, Context context) { DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME.withZone(TimeZone.getTimeZone("UTC").toZoneId()); return dtf.format(Instant.now()); } -} \ No newline at end of file +} diff --git a/powertools-e2e-tests/handlers/tracing/pom.xml b/powertools-e2e-tests/handlers/tracing/pom.xml index b1bc14c05..67bcee662 100644 --- a/powertools-e2e-tests/handlers/tracing/pom.xml +++ b/powertools-e2e-tests/handlers/tracing/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 @@ -21,6 +21,14 @@ com.amazonaws aws-lambda-java-events + + com.amazonaws + aws-lambda-java-runtime-interface-client + + + com.amazonaws + aws-lambda-java-core + org.aspectj aspectjrt @@ -57,4 +65,18 @@ + + + + native-image + + + + org.graalvm.buildtools + native-maven-plugin + + + + + diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json new file mode 100644 index 000000000..2780aca09 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json @@ -0,0 +1,13 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "methods":[{"name":"","parameterTypes":[] }], + "fields":[{"name":"logger"}], + "allPublicMethods":true + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal", + "methods":[{"name":"","parameterTypes":[] }], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json new file mode 100644 index 000000000..ddda5d5f1 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json @@ -0,0 +1,35 @@ +[ + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json new file mode 100644 index 000000000..91be72f7a --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json @@ -0,0 +1,11 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientException", + "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name": "tenantId"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties new file mode 100644 index 000000000..20f8b7801 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=jdk.xml.internal.SecuritySupport \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json new file mode 100644 index 000000000..e69fa735c --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json @@ -0,0 +1,61 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.LambdaRuntime", + "fields": [{ "name": "logger" }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.logging.LogLevel", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.logging.LogFormat", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + }, + { + "name": "java.lang.Void", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "java.util.Collections$UnmodifiableMap", + "fields": [{ "name": "m" }] + }, + { + "name": "jdk.internal.module.IllegalAccessLogger", + "fields": [{ "name": "logger" }] + }, + { + "name": "sun.misc.Unsafe", + "fields": [{ "name": "theUnsafe" }] + }, + { + "name": "com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest", + "fields": [ + { "name": "id" }, + { "name": "invokedFunctionArn" }, + { "name": "deadlineTimeInMs" }, + { "name": "xrayTraceId" }, + { "name": "clientContext" }, + { "name": "cognitoIdentity" }, + { "name": "tenantId" }, + { "name": "content" } + ], + "allPublicMethods": true + } +] diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json new file mode 100644 index 000000000..1062b4249 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json @@ -0,0 +1,19 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\Qjni/libaws-lambda-jni.linux-aarch_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux-x86_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux_musl-aarch_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux_musl-x86_64.so\\E" + } + ] + }, + "bundles": [] +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json new file mode 100644 index 000000000..9890688f9 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json @@ -0,0 +1,25 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ser.Serializers[]" + }, + { + "name": "org.joda.time.DateTime", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json new file mode 100644 index 000000000..9ddd235e2 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/reflect-config.json @@ -0,0 +1,20 @@ +[ + { + "name": "software.amazon.lambda.powertools.e2e.Function", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "software.amazon.lambda.powertools.e2e.Input", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json new file mode 100644 index 000000000..be6aac3f6 --- /dev/null +++ b/powertools-e2e-tests/handlers/tracing/src/main/resources/META-INF/native-image/software.amazon.lambda.powertools.e2e/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\Qlog4j2.xml\\E" + }]}, + "bundles":[] +} diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 1b36d4701..ccd87e95e 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -40,14 +40,12 @@ log4j-slf4j2-impl test - software.amazon.awssdk lambda ${aws.sdk.version} test - software.amazon.awssdk dynamodb @@ -66,72 +64,62 @@ ${aws.sdk.version} test - software.amazon.awssdk xray ${aws.sdk.version} test - software.amazon.awssdk sqs ${aws.sdk.version} test - com.amazonaws amazon-sqs-java-extended-client-lib 2.1.2 test - software.amazon.awssdk url-connection-client test - org.junit.jupiter junit-jupiter-api test - commons-io commons-io 2.20.0 - org.junit.jupiter junit-jupiter-engine test - org.assertj assertj-core test - - com.evanlennick - retry4j - 0.15.0 + io.github.resilience4j + resilience4j-retry + + 1.7.1 test - software.amazon.awscdk aws-cdk-lib ${cdk.version} test - software.constructs constructs @@ -226,9 +214,6 @@ **/*E2ET.java - - **/TracingE2ET.java - @@ -257,10 +242,8 @@ **/MetricsE2ET.java **/LoggingE2ET.java **/ParametersE2ET.java + **/TracingE2ET.java - - **/TracingE2ET.java - true diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java index c5f74594d..181d5e583 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/BatchE2ET.java @@ -15,10 +15,7 @@ package software.amazon.lambda.powertools; import static org.assertj.core.api.Assertions.assertThat; -import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -26,11 +23,16 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -44,18 +46,18 @@ import software.amazon.awssdk.services.kinesis.KinesisClient; import software.amazon.awssdk.services.kinesis.model.PutRecordsRequest; import software.amazon.awssdk.services.kinesis.model.PutRecordsRequestEntry; -import software.amazon.awssdk.services.kinesis.model.PutRecordsResponse; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; +import software.amazon.lambda.powertools.testutils.DataNotReadyException; import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.RetryUtils; import software.amazon.lambda.powertools.utilities.JsonConfig; -public class BatchE2ET { +class BatchE2ET { private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build(); private static final Region region = Region.of(System.getProperty("AWS_DEFAULT_REGION", "eu-west-1")); private static Infrastructure infrastructure; - private static String functionName; private static String queueUrl; private static String kinesisStreamName; @@ -71,13 +73,12 @@ public BatchE2ET() { testProducts = Arrays.asList( new Product(1, "product1", 1.23), new Product(2, "product2", 4.56), - new Product(3, "product3", 6.78) - ); + new Product(3, "product3", 6.78)); } @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); String queueName = "batchqueue" + random; kinesisStreamName = "batchstream" + random; @@ -94,7 +95,6 @@ public static void setup() { .build(); Map outputs = infrastructure.deploy(); - functionName = outputs.get(FUNCTION_NAME_OUTPUT); queueUrl = outputs.get("QueueURL"); kinesisStreamName = outputs.get("KinesisStreamName"); outputTable = outputs.get("TableNameForAsyncTests"); @@ -117,21 +117,21 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @AfterEach - public void cleanUpTest() { + void cleanUpTest() { // Delete everything in the output table ScanResponse items = ddbClient.scan(ScanRequest.builder() .tableName(outputTable) .build()); for (Map item : items.items()) { - HashMap key = new HashMap() { + Map key = new HashMap<>() { { put("functionName", AttributeValue.builder() .s(item.get("functionName").s()) @@ -150,7 +150,7 @@ public void cleanUpTest() { } @Test - public void sqsBatchProcessingSucceeds() throws InterruptedException { + void sqsBatchProcessingSucceeds() { List entries = testProducts.stream() .map(p -> { try { @@ -169,17 +169,23 @@ public void sqsBatchProcessingSucceeds() throws InterruptedException { .entries(entries) .queueUrl(queueUrl) .build()); - Thread.sleep(30000); // wait for function to be executed // THEN - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); - validateAllItemsHandled(items); + ScanRequest scanRequest = ScanRequest.builder().tableName(outputTable).build(); + RetryUtils.withRetry(() -> { + ScanResponse items = ddbClient.scan(scanRequest); + if (!areAllTestProductsPresent(items)) { + throw new DataNotReadyException("sqs-batch-processing not complete yet"); + } + return null; + }, "sqs-batch-processing", DataNotReadyException.class).get(); + + ScanResponse finalItems = ddbClient.scan(scanRequest); + assertThat(areAllTestProductsPresent(finalItems)).isTrue(); } @Test - public void kinesisBatchProcessingSucceeds() throws InterruptedException { + void kinesisBatchProcessingSucceeds() { List entries = testProducts.stream() .map(p -> { try { @@ -194,21 +200,27 @@ public void kinesisBatchProcessingSucceeds() throws InterruptedException { .collect(Collectors.toList()); // WHEN - PutRecordsResponse result = kinesisClient.putRecords(PutRecordsRequest.builder() + kinesisClient.putRecords(PutRecordsRequest.builder() .streamName(kinesisStreamName) .records(entries) .build()); - Thread.sleep(30000); // wait for function to be executed // THEN - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); - validateAllItemsHandled(items); + ScanRequest scanRequest = ScanRequest.builder().tableName(outputTable).build(); + RetryUtils.withRetry(() -> { + ScanResponse items = ddbClient.scan(scanRequest); + if (!areAllTestProductsPresent(items)) { + throw new DataNotReadyException("kinesis-batch-processing not complete yet"); + } + return null; + }, "kinesis-batch-processing", DataNotReadyException.class).get(); + + ScanResponse finalItems = ddbClient.scan(scanRequest); + assertThat(areAllTestProductsPresent(finalItems)).isTrue(); } @Test - public void ddbStreamsBatchProcessingSucceeds() throws InterruptedException { + void ddbStreamsBatchProcessingSucceeds() { // GIVEN String theId = "my-test-id"; @@ -223,39 +235,43 @@ public void ddbStreamsBatchProcessingSucceeds() throws InterruptedException { } }) .build()); - Thread.sleep(90000); // wait for function to be executed // THEN - ScanResponse items = ddbClient.scan(ScanRequest.builder() - .tableName(outputTable) - .build()); + ScanRequest scanRequest = ScanRequest.builder().tableName(outputTable).build(); + RetryUtils.withRetry(() -> { + ScanResponse items = ddbClient.scan(scanRequest); + if (items.count() != 1) { + throw new DataNotReadyException("DDB streams processing not complete yet"); + } + return null; + }, "ddb-streams-batch-processing", DataNotReadyException.class).get(); - assertThat(items.count()).isEqualTo(1); - assertThat(items.items().get(0).get("id").s()).isEqualTo(theId); + ScanResponse finalItems = ddbClient.scan(scanRequest); + assertThat(finalItems.count()).isEqualTo(1); + assertThat(finalItems.items().get(0).get("id").s()).isEqualTo(theId); } - private void validateAllItemsHandled(ScanResponse items) { + private boolean areAllTestProductsPresent(ScanResponse items) { for (Product p : testProducts) { boolean foundIt = false; for (Map a : items.items()) { if (a.get("id").s().equals(Long.toString(p.id))) { foundIt = true; + break; } } - assertThat(foundIt).isTrue(); + if (!foundIt) { + return false; + } } + return true; } class Product { private long id; - private String name; - private double price; - public Product() { - } - public Product(long id, String name, double price) { this.id = id; this.name = name; diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java index 242d1a2db..292f46bfa 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java @@ -21,21 +21,23 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; + import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; + import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; -public class IdempotencyE2ET { +class IdempotencyE2ET { private static Infrastructure infrastructure; private static String functionName; @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); infrastructure = Infrastructure.builder() .testName(IdempotencyE2ET.class.getSimpleName()) @@ -47,14 +49,14 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @Test - public void test_ttlNotExpired_sameResult_ttlExpired_differentResult() throws InterruptedException { + void test_ttlNotExpired_sameResult_ttlExpired_differentResult() throws InterruptedException { // GIVEN String event = "{\"message\":\"TTL 10sec\"}"; @@ -65,6 +67,7 @@ public void test_ttlNotExpired_sameResult_ttlExpired_differentResult() throws In // Second invocation (should get same result) InvocationResult result2 = invokeFunction(functionName, event); + // Function idempotency record expiration is set to 10 seconds Thread.sleep(12000); // Third invocation (should get different result) diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java index d9c3ef749..74247ca2e 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageE2ET.java @@ -36,9 +36,11 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import software.amazon.lambda.powertools.testutils.DataNotReadyException; import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.RetryUtils; -public class LargeMessageE2ET { +class LargeMessageE2ET { private static final Logger LOG = LoggerFactory.getLogger(LargeMessageE2ET.class); private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build(); @@ -62,7 +64,7 @@ public class LargeMessageE2ET { @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); bucketName = "largemessagebucket" + random; String queueName = "largemessagequeue" + random; @@ -84,14 +86,14 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @AfterEach - public void reset() { + void reset() { if (messageId != null) { Map itemToDelete = new HashMap<>(); itemToDelete.put("functionName", AttributeValue.builder().s(functionName).build()); @@ -102,25 +104,24 @@ public void reset() { } @Test - public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, InterruptedException { - // given + void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException { + // GIVEN final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() .withPayloadSupportEnabled(s3Client, bucketName); try (AmazonSQSExtendedClient client = new AmazonSQSExtendedClient( - SqsClient.builder().region(region).httpClient(httpClient).build(), extendedClientConfig)) { - InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); + SqsClient.builder().region(region).httpClient(httpClient).build(), extendedClientConfig); + InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt");) { String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - // when + // WHEN client.sendMessage(SendMessageRequest .builder() .queueUrl(queueUrl) .messageBody(bigMessage) .build()); } - Thread.sleep(30000); // wait for function to be executed - // then + // THEN QueryRequest request = QueryRequest .builder() .tableName(tableName) @@ -128,8 +129,17 @@ public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, In .expressionAttributeValues( Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) .build(); - QueryResponse response = dynamoDbClient.query(request); - List> items = response.items(); + + RetryUtils.withRetry(() -> { + QueryResponse response = dynamoDbClient.query(request); + if (response.items().size() != 1) { + throw new DataNotReadyException("Large message processing not complete yet"); + } + return null; + }, "large-message-processing", DataNotReadyException.class).get(); + + QueryResponse finalResponse = dynamoDbClient.query(request); + List> items = finalResponse.items(); assertThat(items).hasSize(1); messageId = items.get(0).get("id").s(); assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo(300977); @@ -137,8 +147,8 @@ public void bigSQSMessageOffloadedToS3_shouldLoadFromS3() throws IOException, In } @Test - public void smallSQSMessage_shouldNotReadFromS3() throws IOException, InterruptedException { - // given + void smallSQSMessage_shouldNotReadFromS3() { + // GIVEN final ExtendedClientConfiguration extendedClientConfig = new ExtendedClientConfiguration() .withPayloadSupportEnabled(s3Client, bucketName); try (AmazonSQSExtendedClient client = new AmazonSQSExtendedClient( @@ -146,16 +156,14 @@ public void smallSQSMessage_shouldNotReadFromS3() throws IOException, Interrupte extendedClientConfig)) { String message = "Hello World"; - // when + // WHEN client.sendMessage(SendMessageRequest .builder() .queueUrl(queueUrl) .messageBody(message) .build()); - Thread.sleep(30000); // wait for function to be executed - - // then + // THEN QueryRequest request = QueryRequest .builder() .tableName(tableName) @@ -163,8 +171,17 @@ public void smallSQSMessage_shouldNotReadFromS3() throws IOException, Interrupte .expressionAttributeValues( Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) .build(); - QueryResponse response = dynamoDbClient.query(request); - List> items = response.items(); + + RetryUtils.withRetry(() -> { + QueryResponse response = dynamoDbClient.query(request); + if (response.items().size() != 1) { + throw new DataNotReadyException("Small message processing not complete yet"); + } + return null; + }, "small-message-processing", DataNotReadyException.class).get(); + + QueryResponse finalResponse = dynamoDbClient.query(request); + List> items = finalResponse.items(); assertThat(items).hasSize(1); messageId = items.get(0).get("id").s(); assertThat(Integer.valueOf(items.get(0).get("bodySize").n())).isEqualTo( diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java index ef342ea13..cd787b60a 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/LargeMessageIdempotentE2ET.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; + import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -31,6 +32,7 @@ import org.junit.jupiter.api.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; @@ -44,9 +46,11 @@ import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; import software.amazon.awssdk.services.sqs.model.SendMessageRequest; +import software.amazon.lambda.powertools.testutils.DataNotReadyException; import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.RetryUtils; -public class LargeMessageIdempotentE2ET { +class LargeMessageIdempotentE2ET { private static final Logger LOG = LoggerFactory.getLogger(LargeMessageIdempotentE2ET.class); private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build(); @@ -72,10 +76,9 @@ public class LargeMessageIdempotentE2ET { private static String queueUrl; private static String tableName; - @BeforeAll @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); bucketName = "largemessagebucket" + random; String queueName = "largemessagequeue" + random; @@ -98,34 +101,33 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @Test - public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throws InterruptedException, + void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throws InterruptedException, IOException { - int waitMs = 15000; - // GIVEN - InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt"); - String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // upload manually to S3 - String key = UUID.randomUUID().toString(); - s3Client.putObject(PutObjectRequest.builder() - .bucket(bucketName) - .key(key) - .build(), RequestBody.fromString(bigMessage)); + String s3Key = UUID.randomUUID().toString(); + try (InputStream inputStream = this.getClass().getResourceAsStream("/large_sqs_message.txt")) { + String bigMessage = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + + // upload manually to S3 + s3Client.putObject(PutObjectRequest.builder() + .bucket(bucketName) + .key(s3Key) + .build(), RequestBody.fromString(bigMessage)); + } // WHEN SendMessageRequest messageRequest = SendMessageRequest.builder() .queueUrl(queueUrl) .messageBody(String.format( "[\"software.amazon.payloadoffloading.PayloadS3Pointer\",{\"s3BucketName\":\"%s\",\"s3Key\":\"%s\"}]", - bucketName, key)) + bucketName, s3Key)) .messageAttributes(Collections.singletonMap("SQSLargePayloadSize", MessageAttributeValue.builder() .stringValue("300977") .dataType("Number") @@ -135,7 +137,6 @@ public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throw // First invocation // send message to SQS with the good pointer and metadata sqsClient.sendMessage(messageRequest); - Thread.sleep(waitMs); // wait for the function to be invoked & executed // THEN QueryRequest request = QueryRequest @@ -145,6 +146,15 @@ public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throw .expressionAttributeValues( Collections.singletonMap(":func", AttributeValue.builder().s(functionName).build())) .build(); + + RetryUtils.withRetry(() -> { + QueryResponse response = dynamoDbClient.query(request); + if (response.items().size() != 1) { + throw new DataNotReadyException("First invocation processing not complete yet"); + } + return null; + }, "first-invocation-processing", DataNotReadyException.class).get(); + QueryResponse response = dynamoDbClient.query(request); List> items = response.items(); assertThat(items).hasSize(1); @@ -156,7 +166,14 @@ public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throw // Second invocation // send the same message before ttl expires sqsClient.sendMessage(messageRequest); - Thread.sleep(waitMs); // wait for the function to be invoked & executed + + RetryUtils.withRetry(() -> { + QueryResponse resp = dynamoDbClient.query(request); + if (resp.items().size() != 1) { + throw new DataNotReadyException("Second invocation processing not complete yet"); + } + return null; + }, "second-invocation-processing", DataNotReadyException.class).get(); // THEN response = dynamoDbClient.query(request); @@ -174,7 +191,14 @@ public void test_ttlNotExpired_doesNotInsertInDDB_ttlExpired_insertInDDB() throw // Third invocation // send the same message again sqsClient.sendMessage(messageRequest); - Thread.sleep(waitMs); // wait for the function to be invoked + + RetryUtils.withRetry(() -> { + QueryResponse resp = dynamoDbClient.query(request); + if (resp.items().size() != 2) { + throw new DataNotReadyException("Third invocation processing not complete yet"); + } + return null; + }, "third-invocation-processing", DataNotReadyException.class).get(); // THEN response = dynamoDbClient.query(request); diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java index d2a5ceed1..13d8adb9b 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/TracingE2ET.java @@ -15,6 +15,7 @@ package software.amazon.lambda.powertools; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; @@ -33,21 +34,21 @@ import software.amazon.lambda.powertools.testutils.tracing.Trace; import software.amazon.lambda.powertools.testutils.tracing.TraceFetcher; -public class TracingE2ET { - private static final String service = "TracingE2EService_" + UUID.randomUUID(); +class TracingE2ET { + private static final String SERVICE = "TracingE2EService_" + UUID.randomUUID(); private static Infrastructure infrastructure; private static String functionName; @BeforeAll - @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { + @Timeout(value = 15, unit = TimeUnit.MINUTES) + static void setup() { infrastructure = Infrastructure.builder() .testName(TracingE2ET.class.getSimpleName()) .pathToFunction("tracing") .tracing(true) .environmentVariables( - Map.of("POWERTOOLS_SERVICE_NAME", service, + Map.of("POWERTOOLS_SERVICE_NAME", SERVICE, "POWERTOOLS_TRACER_CAPTURE_RESPONSE", "true")) .build(); Map outputs = infrastructure.deploy(); @@ -55,14 +56,14 @@ public static void setup() { } @AfterAll - public static void tearDown() { + static void tearDown() { if (infrastructure != null) { infrastructure.destroy(); } } @Test - public void test_tracing() { + void test_tracing() { // GIVEN final String message = "Hello World"; final String event = String.format("{\"message\":\"%s\"}", message); @@ -79,25 +80,18 @@ public void test_tracing() { .build() .fetchTrace(); - assertThat(trace.getSubsegments()).hasSize(2); - - // We need to filter segments based on name because they are not returned in-order from the X-Ray API - // The Init segment is created by default for Lambda functions in X-Ray - final SubSegment initSegment = trace.getSubsegments().stream() - .filter(subSegment -> subSegment.getName().equals("Init")) - .findFirst().orElse(null); - assertThat(initSegment.getName()).isEqualTo("Init"); - assertThat(initSegment.getAnnotations()).isNull(); + assertThat(trace.getSubsegments()).hasSize(1); final SubSegment handleRequestSegment = trace.getSubsegments().stream() - .filter(subSegment -> subSegment.getName().equals("## handleRequest")) + .filter(subSegment -> "## handleRequest".equals(subSegment.getName())) .findFirst().orElse(null); + assertNotNull(handleRequestSegment); assertThat(handleRequestSegment.getName()).isEqualTo("## handleRequest"); assertThat(handleRequestSegment.getAnnotations()).hasSize(2); assertThat(handleRequestSegment.getAnnotations()).containsEntry("ColdStart", true); - assertThat(handleRequestSegment.getAnnotations()).containsEntry("Service", service); + assertThat(handleRequestSegment.getAnnotations()).containsEntry("Service", SERVICE); assertThat(handleRequestSegment.getMetadata()).hasSize(1); - final Map metadata = (Map) handleRequestSegment.getMetadata().get(service); + final Map metadata = (Map) handleRequestSegment.getMetadata().get(SERVICE); assertThat(metadata).containsEntry("handleRequest response", result); assertThat(handleRequestSegment.getSubsegments()).hasSize(2); @@ -108,14 +102,14 @@ public void test_tracing() { assertThat(sub.getName()).isIn("## internal_stuff", "## buildMessage"); SubSegment buildMessage = handleRequestSegment.getSubsegments().stream() - .filter(subSegment -> subSegment.getName().equals("## buildMessage")) + .filter(subSegment -> "## buildMessage".equals(subSegment.getName())) .findFirst().orElse(null); assertThat(buildMessage).isNotNull(); assertThat(buildMessage.getAnnotations()).hasSize(1); assertThat(buildMessage.getAnnotations()).containsEntry("message", message); assertThat(buildMessage.getMetadata()).hasSize(1); final Map buildMessageSegmentMetadata = (Map) buildMessage.getMetadata() - .get(service); + .get(SERVICE); assertThat(buildMessageSegmentMetadata).containsEntry("buildMessage response", result); } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java index 41696943a..bf90851cb 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java @@ -38,67 +38,70 @@ class ValidationALBE2ET { - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static Infrastructure infrastructure; - private static String functionName; - - @BeforeAll - @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { - infrastructure = Infrastructure.builder().testName(ValidationALBE2ET.class.getSimpleName()) - .pathToFunction("validation-alb-event").build(); - Map outputs = infrastructure.deploy(); - functionName = outputs.get(FUNCTION_NAME_OUTPUT); - } - - @AfterAll - public static void tearDown() { - if (infrastructure != null) { - infrastructure.destroy(); - } - } - - @Test - void test_validInboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json"); - String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, validEvent); - - // THEN - // invocation should pass validation and return 200 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); - assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); - } - - @Test - void test_invalidInboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail inbound validation and return an error message - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("errorMessage").asText()).contains(": required property 'price' not found"); - } - - @Test - void test_invalidOutboundSQSEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail outbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("errorMessage").asText()).contains("/price: must have an exclusive maximum value of 1000"); - } + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationALBE2ET.class.getSimpleName()) + .pathToFunction("validation-alb-event").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + void test_validInboundSQSEvent() throws IOException { + try (InputStream is = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json")) { + String validEvent = IOUtils.toString(is, StandardCharsets.UTF_8); + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + } + + @Test + void test_invalidInboundSQSEvent() throws IOException { + try (InputStream is = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json")) { + String invalidEvent = IOUtils.toString(is, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail inbound validation and return an error message + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()).contains(": required property 'price' not found"); + } + } + + @Test + void test_invalidOutboundSQSEvent() throws IOException { + try (InputStream is = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json")) { + String invalidEvent = IOUtils.toString(is, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("errorMessage").asText()) + .contains("/price: must have an exclusive maximum value of 1000"); + } + } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java index 425399c95..2d1bf4657 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java @@ -38,70 +38,73 @@ class ValidationApiGWE2ET { - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static Infrastructure infrastructure; - private static String functionName; - - @BeforeAll - @Timeout(value = 5, unit = TimeUnit.MINUTES) - public static void setup() { - infrastructure = Infrastructure.builder().testName(ValidationApiGWE2ET.class.getSimpleName()) - .pathToFunction("validation-apigw-event").build(); - Map outputs = infrastructure.deploy(); - functionName = outputs.get(FUNCTION_NAME_OUTPUT); - } - - @AfterAll - public static void tearDown() { - if (infrastructure != null) { - infrastructure.destroy(); - } - } - - @Test - void test_validInboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json"); - String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, validEvent); - - // THEN - // invocation should pass validation and return 200 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); - assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); - } - - @Test - void test_invalidInboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail inbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); - assertThat(validJsonNode.get("body").asText()).contains(": required property 'price' not found"); - } - - @Test - void test_invalidOutboundApiGWEvent() throws IOException { - InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json"); - String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); - - // WHEN - InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); - - // THEN - // invocation should fail outbound validation and return 400 - JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); - assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); - assertThat(validJsonNode.get("body").asText()) - .contains("/price: must have an exclusive maximum value of 1000"); - } + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 5, unit = TimeUnit.MINUTES) + static void setup() { + infrastructure = Infrastructure.builder().testName(ValidationApiGWE2ET.class.getSimpleName()) + .pathToFunction("validation-apigw-event").build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + void test_validInboundApiGWEvent() throws IOException { + try (InputStream is = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json")) { + String validEvent = IOUtils.toString(is, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, validEvent); + + // THEN + // invocation should pass validation and return 200 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200); + assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}"); + } + } + + @Test + void test_invalidInboundApiGWEvent() throws IOException { + try (InputStream is = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json")) { + String invalidEvent = IOUtils.toString(is, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail inbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()).contains(": required property 'price' not found"); + } + } + + @Test + void test_invalidOutboundApiGWEvent() throws IOException { + try (InputStream is = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json")) { + String invalidEvent = IOUtils.toString(is, StandardCharsets.UTF_8); + + // WHEN + InvocationResult invocationResult = invokeFunction(functionName, invalidEvent); + + // THEN + // invocation should fail outbound validation and return 400 + JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult()); + assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400); + assertThat(validJsonNode.get("body").asText()) + .contains("/price: must have an exclusive maximum value of 1000"); + } + } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/DataNotReadyException.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/DataNotReadyException.java new file mode 100644 index 000000000..0c16bb68b --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/DataNotReadyException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.testutils; + +/** + * Exception thrown when test data is not ready yet. + * This exception is used to trigger retries in tests waiting for async operations. + */ +public class DataNotReadyException extends RuntimeException { + public DataNotReadyException(String message) { + super(message); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java index c65871a0a..ea5ac3342 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.nio.file.Paths; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -97,7 +98,7 @@ * `PutObjectRequest`) * and the CloudFormation stack is created (with the SDK `createStack`) */ -public class Infrastructure { +public final class Infrastructure { public static final String FUNCTION_NAME_OUTPUT = "functionName"; private static final Logger LOG = LoggerFactory.getLogger(Infrastructure.class); @@ -120,7 +121,6 @@ public class Infrastructure { private final String kinesisStream; private final String largeMessagesBucket; private String ddbStreamsTableName; - private String functionName; private Object cfnTemplate; private String cfnAssetDirectory; @@ -223,7 +223,7 @@ private Stack createStackWithLambda() { ? dockerConfig.createGraalVMBundlingOptions(pathToFunction, runtime) : dockerConfig.createJVMBundlingOptions(pathToFunction, runtime); - functionName = stackName + "-function"; + String functionName = stackName + "-function"; CfnOutput.Builder.create(e2eStack, FUNCTION_NAME_OUTPUT) .value(functionName) .build(); @@ -469,10 +469,18 @@ private Map findAssets() { files.iterator().forEachRemaining(file -> { String assetPath = file.get("source").get("path").asText(); String assetPackaging = file.get("source").get("packaging").asText(); - String bucketName = file.get("destinations").get("current_account-current_region").get("bucketName") - .asText(); - String objectKey = file.get("destinations").get("current_account-current_region").get("objectKey") - .asText(); + JsonNode destinations = file.get("destinations"); + String bucketName = null; + String objectKey = null; + Iterator fieldNames = destinations.fieldNames(); + while (fieldNames.hasNext()) { + String fieldName = fieldNames.next(); + if (fieldName.startsWith("current_account-current_region")) { + bucketName = destinations.get(fieldName).get("bucketName").asText(); + objectKey = destinations.get(fieldName).get("objectKey").asText(); + break; + } + } Asset asset = new Asset(assetPath, assetPackaging, bucketName.replace("${AWS::AccountId}", account) .replace("${AWS::Region}", region.toString())); assets.put(objectKey, asset); @@ -483,7 +491,7 @@ private Map findAssets() { return assets; } - public static class Builder { + public static final class Builder { public long timeoutInSeconds = 30; public String pathToFunction; public String testName; diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java new file mode 100644 index 000000000..054e9aa8e --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/RetryUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.testutils; + +import java.time.Duration; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; + +/** + * Utility class for consistent retry configuration across all test utilities. + */ +public final class RetryUtils { + private static final Logger LOG = LoggerFactory.getLogger(RetryUtils.class); + + private static final RetryConfig DEFAULT_RETRY_CONFIG = RetryConfig.custom() + .maxAttempts(60) // 60 attempts over 5 minutes + .waitDuration(Duration.ofSeconds(5)) // 5 seconds between attempts + .build(); + + private RetryUtils() { + // Utility class + } + + /** + * Creates a retry instance with default configuration for the specified throwable types. + * + * @param name the name for the retry instance + * @param retryOnThrowables the throwable classes to retry on + * @return configured Retry instance + */ + @SafeVarargs + public static Retry createRetry(String name, Class... retryOnThrowables) { + RetryConfig config = RetryConfig.from(DEFAULT_RETRY_CONFIG) + .retryExceptions(retryOnThrowables) + .build(); + + Retry retry = Retry.of(name, config); + retry.getEventPublisher().onRetry(event -> LOG.warn("Retry attempt {} for {}: {}", + event.getNumberOfRetryAttempts(), name, event.getLastThrowable().getMessage())); + + return retry; + } + + /** + * Decorates a supplier with retry logic for the specified throwable types. + * + * @param supplier the supplier to decorate + * @param name the name for the retry instance + * @param retryOnThrowables the throwable classes to retry on + * @return decorated supplier with retry logic + */ + @SafeVarargs + public static Supplier withRetry(Supplier supplier, String name, + Class... retryOnThrowables) { + Retry retry = createRetry(name, retryOnThrowables); + return Retry.decorateSupplier(retry, supplier); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java new file mode 100644 index 000000000..79478c14e --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricDataNotFoundException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.testutils.metrics; + +import software.amazon.lambda.powertools.testutils.DataNotReadyException; + +/** + * Exception thrown when metric data is not found in CloudWatch. + * This exception is used to trigger retries as metrics may not be available immediately. + */ +public class MetricDataNotFoundException extends DataNotReadyException { + public MetricDataNotFoundException(String message) { + super(message); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java index 186e72d13..f856d8f2f 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/metrics/MetricsFetcher.java @@ -14,20 +14,16 @@ package software.amazon.lambda.powertools.testutils.metrics; -import static java.time.Duration.ofSeconds; - -import com.evanlennick.retry4j.CallExecutor; -import com.evanlennick.retry4j.CallExecutorBuilder; -import com.evanlennick.retry4j.Status; -import com.evanlennick.retry4j.config.RetryConfig; -import com.evanlennick.retry4j.config.RetryConfigBuilder; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; -import java.util.concurrent.Callable; +import java.util.function.Supplier; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -39,6 +35,7 @@ import software.amazon.awssdk.services.cloudwatch.model.MetricDataQuery; import software.amazon.awssdk.services.cloudwatch.model.MetricStat; import software.amazon.awssdk.services.cloudwatch.model.StandardUnit; +import software.amazon.lambda.powertools.testutils.RetryUtils; /** * Class in charge of retrieving the actual metrics of a Lambda execution on CloudWatch @@ -73,14 +70,14 @@ public List fetchMetrics(Instant start, Instant end, int period, String dimensions.forEach((key, value) -> dimensionsList.add(Dimension.builder().name(key).value(value).build())); } - Callable> callable = () -> { + Supplier> supplier = () -> { LOG.debug("Get Metrics for namespace {}, start {}, end {}, metric {}, dimensions {}", namespace, start, end, metricName, dimensionsList); GetMetricDataResponse metricData = cloudwatch.getMetricData(GetMetricDataRequest.builder() .startTime(start) .endTime(end) .metricDataQueries(MetricDataQuery.builder() - .id(metricName.toLowerCase()) + .id(metricName.toLowerCase(Locale.ROOT)) .metricStat(MetricStat.builder() .unit(StandardUnit.COUNT) .metric(Metric.builder() @@ -96,24 +93,11 @@ public List fetchMetrics(Instant start, Instant end, int period, String .build()); List values = metricData.metricDataResults().get(0).values(); if (values == null || values.isEmpty()) { - throw new Exception("No data found for metric " + metricName); + throw new MetricDataNotFoundException("No data found for metric " + metricName); } return values; }; - RetryConfig retryConfig = new RetryConfigBuilder() - .withMaxNumberOfTries(10) - .retryOnAnyException() - .withDelayBetweenTries(ofSeconds(2)) - .withRandomExponentialBackoff() - .build(); - CallExecutor> callExecutor = new CallExecutorBuilder>() - .config(retryConfig) - .afterFailedTryListener(s -> { - LOG.warn("{}, attempts: {}", s.getLastExceptionThatCausedRetry().getMessage(), s.getTotalTries()); - }) - .build(); - Status> status = callExecutor.execute(callable); - return status.getResult(); + return RetryUtils.withRetry(supplier, "metrics-fetcher-" + metricName, MetricDataNotFoundException.class).get(); } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java index 2b5b6e15b..fc2d061cc 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceFetcher.java @@ -14,25 +14,21 @@ package software.amazon.lambda.powertools.testutils.tracing; -import static java.time.Duration.ofSeconds; - -import com.evanlennick.retry4j.CallExecutor; -import com.evanlennick.retry4j.CallExecutorBuilder; -import com.evanlennick.retry4j.Status; -import com.evanlennick.retry4j.config.RetryConfig; -import com.evanlennick.retry4j.config.RetryConfigBuilder; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.Callable; +import java.util.function.Supplier; import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -43,6 +39,7 @@ import software.amazon.awssdk.services.xray.model.GetTraceSummariesResponse; import software.amazon.awssdk.services.xray.model.TimeRangeType; import software.amazon.awssdk.services.xray.model.TraceSummary; +import software.amazon.lambda.powertools.testutils.RetryUtils; import software.amazon.lambda.powertools.testutils.tracing.SegmentDocument.SubSegment; /** @@ -50,8 +47,8 @@ */ public class TraceFetcher { - private static final ObjectMapper MAPPER = - new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private static final ObjectMapper MAPPER = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private static final Logger LOG = LoggerFactory.getLogger(TraceFetcher.class); private static final SdkHttpClient httpClient = UrlConnectionHttpClient.builder().build(); private static final Region region = Region.of(System.getProperty("AWS_DEFAULT_REGION", "eu-west-1")); @@ -88,27 +85,12 @@ public static Builder builder() { * @return traces */ public Trace fetchTrace() { - Callable callable = () -> - { + Supplier supplier = () -> { List traceIds = getTraceIds(); return getTrace(traceIds); }; - RetryConfig retryConfig = new RetryConfigBuilder() - .withMaxNumberOfTries(10) - .retryOnAnyException() - .withDelayBetweenTries(ofSeconds(5)) - .withRandomExponentialBackoff() - .build(); - CallExecutor callExecutor = new CallExecutorBuilder() - .config(retryConfig) - .afterFailedTryListener(s -> - { - LOG.warn(s.getLastExceptionThatCausedRetry().getMessage() + ", attempts: " + s.getTotalTries()); - }) - .build(); - Status status = callExecutor.execute(callable); - return status.getResult(); + return RetryUtils.withRetry(supplier, "trace-fetcher", TraceNotFoundException.class).get(); } /** @@ -122,22 +104,19 @@ private Trace getTrace(List traceIds) { .traceIds(traceIds) .build()); if (!tracesResponse.hasTraces()) { - throw new RuntimeException("No trace found"); + throw new TraceNotFoundException("No trace found"); } Trace traceRes = new Trace(); - tracesResponse.traces().forEach(trace -> - { + tracesResponse.traces().forEach(trace -> { if (trace.hasSegments()) { - trace.segments().forEach(segment -> - { + trace.segments().forEach(segment -> { try { SegmentDocument document = MAPPER.readValue(segment.document(), SegmentDocument.class); - if (document.getOrigin().equals("AWS::Lambda::Function")) { - if (document.hasSubsegments()) { - getNestedSubSegments(document.getSubsegments(), traceRes, - Collections.emptyList()); - } + if ("AWS::Lambda::Function".equals(document.getOrigin()) && document.hasSubsegments()) { + getNestedSubSegments(document.getSubsegments(), traceRes, + Collections.emptyList()); } + } catch (JsonProcessingException e) { LOG.error("Failed to parse segment document: " + e.getMessage()); throw new RuntimeException(e); @@ -149,8 +128,7 @@ private Trace getTrace(List traceIds) { } private void getNestedSubSegments(List subsegments, Trace traceRes, List idsToIgnore) { - subsegments.forEach(subsegment -> - { + subsegments.forEach(subsegment -> { List subSegmentIdsToIgnore = Collections.emptyList(); if (!excludedSegments.contains(subsegment.getName()) && !idsToIgnore.contains(subsegment.getId())) { traceRes.addSubSegment(subsegment); @@ -179,12 +157,12 @@ private List getTraceIds() { .filterExpression(filterExpression) .build()); if (!traceSummaries.hasTraceSummaries()) { - throw new RuntimeException("No trace id found"); + throw new TraceNotFoundException("No trace id found"); } - List traceIds = - traceSummaries.traceSummaries().stream().map(TraceSummary::id).collect(Collectors.toList()); + List traceIds = traceSummaries.traceSummaries().stream().map(TraceSummary::id) + .collect(Collectors.toList()); if (traceIds.isEmpty()) { - throw new RuntimeException("No trace id found"); + throw new TraceNotFoundException("No trace id found"); } return traceIds; } @@ -193,7 +171,7 @@ public static class Builder { private Instant start; private Instant end; private String filterExpression; - private List excludedSegments = Arrays.asList("Initialization", "Invocation", "Overhead"); + private List excludedSegments = Arrays.asList("Initialization", "Init", "Invocation", "Overhead"); public TraceFetcher build() { if (filterExpression == null) { @@ -205,7 +183,8 @@ public TraceFetcher build() { if (end == null) { end = start.plus(1, ChronoUnit.MINUTES); } - LOG.debug("Looking for traces from {} to {} with filter {}", start, end, filterExpression); + LOG.debug("Looking for traces from {} to {} with filter {} and excluded segments {}", start, end, + filterExpression, excludedSegments); return new TraceFetcher(start, end, filterExpression, excludedSegments); } @@ -236,8 +215,8 @@ public Builder excludeSegments(List excludedSegments) { } public Builder functionName(String functionName) { - this.filterExpression = - String.format("service(id(name: \"%s\", type: \"AWS::Lambda::Function\"))", functionName); + this.filterExpression = String.format("service(id(name: \"%s\", type: \"AWS::Lambda::Function\"))", + functionName); return this; } } diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java new file mode 100644 index 000000000..453aae669 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/tracing/TraceNotFoundException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package software.amazon.lambda.powertools.testutils.tracing; + +import software.amazon.lambda.powertools.testutils.DataNotReadyException; + +/** + * Exception thrown when trace data is not found in X-Ray. + * This exception is used to trigger retries as traces may not be available immediately. + */ +public class TraceNotFoundException extends DataNotReadyException { + public TraceNotFoundException(String message) { + super(message); + } +}