diff --git a/test/warmup-tests/pom.xml b/test/warmup-tests/pom.xml index d292904c998f..cbc6f56afc02 100644 --- a/test/warmup-tests/pom.xml +++ b/test/warmup-tests/pom.xml @@ -26,9 +26,11 @@ warmup-tests AWS Java SDK :: Test :: Warm-up Tests - Centralized tests for the CRaC HTTP-client warm-up, run against every sync HTTP client so each client does not - need its own warm-up test. This is a leaf test module: nothing depends on it, so depending on the HTTP clients here - does not create a dependency cycle. + Centralized tests for the warm-up feature across all modules. Covers the SdkWarmUp orchestrator, HTTP-client + warm-up (run against every HTTP client), and the generated SdkWarmUpProvider of every service module: the + aws-sdk-java aggregate dependency puts all services on the test classpath, so new services are tested + automatically. This is a leaf test module: nothing depends on it, so depending on the HTTP clients and all + services here does not create a dependency cycle. https://aws.amazon.com/sdkforjava @@ -43,6 +45,14 @@ + + + true + + software.amazon.awssdk @@ -55,6 +65,12 @@ ${awsjavasdk.version} test + + software.amazon.awssdk + http-auth-aws-crt + ${awsjavasdk.version} + test + software.amazon.awssdk apache5-client @@ -91,6 +107,31 @@ ${awsjavasdk.version} test + + + software.amazon.awssdk + aws-sdk-java + ${awsjavasdk.version} + test + + + software.amazon.awssdk + test-utils + ${awsjavasdk.version} + test + + + org.apache.logging.log4j + log4j-core + test + + + org.apache.logging.log4j + log4j-slf4j-impl + test + org.junit.jupiter junit-jupiter @@ -125,6 +166,16 @@ true + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + false + + diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/warmup/allservices/AllServicesWarmUpTest.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/warmup/allservices/AllServicesWarmUpTest.java new file mode 100644 index 000000000000..e0a43a9331c4 --- /dev/null +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/warmup/allservices/AllServicesWarmUpTest.java @@ -0,0 +1,206 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.warmup.allservices; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.awssdk.core.crac.SdkWarmUp; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.testutils.LogCaptor; +import software.amazon.awssdk.utils.ClassLoaderHelper; + +/** + * Warms every service on the classpath through the public API: {@link SdkWarmUp#prime(Class[])} per generated + * client, and {@link SdkWarmUp#prime()} for all of them at once. + */ +class AllServicesWarmUpTest { + + private static final String SERVICES_PACKAGE_PREFIX = "software.amazon.awssdk.services."; + + /** + * Services with no selectable warm-up operation (all operations are streaming or deprecated); warm-up only + * builds and closes the client. + */ + private static final Set KNOWN_NO_OP_SERVICES = new HashSet<>(Arrays.asList( + // All APIs are deprecated + "cloudhsm", + "finspacedata", + "iotthingsgraph", + "lexmodelbuilding", + "proton", + + // All streaming operations + "kinesisvideomedia", + "sagemakerruntimehttp2")); + + /** + * Known warm-up failures. Document the reason per entry. + */ + private static final Set KNOWN_WARMUP_FAILURE_SERVICES = new HashSet<>(Collections.singletonList( + // Endpoint rules require a real CloudFront ARN; the generated dummy ARN is rejected. + "cloudfrontkeyvaluestore")); + + @BeforeEach + void setUp() { + OperationRecordingInterceptor.reset(); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("generatedProviders") + void prime_syncClient_invokesOperationWithoutErrors(SdkWarmUpProvider provider) { + verifyWarmUp(provider, provider.syncClientClassName()); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("generatedProviders") + void prime_asyncClient_invokesOperationWithoutErrors(SdkWarmUpProvider provider) { + verifyWarmUp(provider, provider.asyncClientClassName()); + } + + @Test + void prime_withAllServicesOnClasspath_noServiceProviderFails() { + String savedRegionProperty = System.getProperty("aws.region"); + System.setProperty("aws.region", "us-east-1"); + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + SdkWarmUp.prime(); + + assertThat(serviceWarmUpFailures(logCaptor)) + .as("SdkWarmUp.prime() must not log a warm-up failure for any generated service provider") + .isEmpty(); + } finally { + restoreRegionProperty(savedRegionProperty); + } + + // prime() runs once per JVM; an empty recording means it already ran and this test verified nothing. + assertThat(OperationRecordingInterceptor.operationNames()) + .as("prime() must have invoked warm-up operations") + .isNotEmpty(); + } + + private void verifyWarmUp(SdkWarmUpProvider provider, String clientClassName) { + assumeTrue(clientClassName != null, + () -> provider.getClass().getSimpleName() + " does not generate this client type"); + assumeTrue(!KNOWN_WARMUP_FAILURE_SERVICES.contains(serviceName(provider)), + () -> provider.getClass().getSimpleName() + + " is a known warm-up failure; see KNOWN_WARMUP_FAILURE_SERVICES"); + + List sdkWarnings; + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + SdkWarmUp.prime(clientClass(clientClassName)); + sdkWarnings = sdkWarnings(logCaptor); + } + + // prime(Class) logs warm-up failures at WARN instead of throwing. + assertThat(sdkWarnings) + .as("Warm-up of %s must not emit SDK warn/error logs", clientClassName) + .isEmpty(); + assertExpectedOperationRecorded(provider, clientClassName); + } + + private void assertExpectedOperationRecorded(SdkWarmUpProvider provider, String clientClassName) { + if (KNOWN_NO_OP_SERVICES.contains(serviceName(provider))) { + assertThat(OperationRecordingInterceptor.operationNames()) + .as("%s is listed in KNOWN_NO_OP_SERVICES but recorded an operation; remove the stale entry", + provider.getClass().getSimpleName()) + .isEmpty(); + } else { + assertThat(OperationRecordingInterceptor.operationNames()) + .as("Warm-up of %s must invoke its selected warm-up operation", clientClassName) + .isNotEmpty(); + } + } + + /** + * All generated service providers; excludes this module's hand-written test providers. + */ + static Stream> generatedProviders() { + return serviceProviders().map(p -> Named.of(p.getClass().getSimpleName(), p)); + } + + private static Stream serviceProviders() { + return StreamSupport.stream(ServiceLoader.load(SdkWarmUpProvider.class).spliterator(), false) + .filter(p -> p.getClass().getName().startsWith(SERVICES_PACKAGE_PREFIX)); + } + + private static String serviceName(SdkWarmUpProvider provider) { + String remainder = provider.getClass().getName().substring(SERVICES_PACKAGE_PREFIX.length()); + return remainder.substring(0, remainder.indexOf('.')); + } + + private static Class clientClass(String clientClassName) { + try { + return ClassLoaderHelper.loadClass(clientClassName, SdkWarmUp.class).asSubclass(SdkClient.class); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Client class not on classpath: " + clientClassName, e); + } + } + + /** + * Captured warnings from SDK loggers; warnings from the rest of the classpath are ignored. + */ + private static List sdkWarnings(LogCaptor logCaptor) { + return logCaptor.loggedEvents().stream() + .filter(e -> e.getLoggerName().startsWith("software.amazon.awssdk")) + .map(e -> e.getLoggerName() + " - " + e.getMessage().getFormattedMessage()) + .collect(Collectors.toList()); + } + + /** + * Warm-up failures logged by {@code prime()}, excluding {@link #KNOWN_WARMUP_FAILURE_SERVICES}. prime() reports + * failures by client class name, so known failures are matched by their client names. + */ + private static List serviceWarmUpFailures(LogCaptor logCaptor) { + Set knownFailureClients = knownWarmUpFailureClients(); + return logCaptor.loggedEvents().stream() + .map(e -> e.getMessage().getFormattedMessage()) + .filter(msg -> msg.contains("software.amazon")) + .filter(msg -> knownFailureClients.stream().noneMatch(msg::contains)) + .collect(Collectors.toList()); + } + + private static Set knownWarmUpFailureClients() { + return serviceProviders().filter(p -> KNOWN_WARMUP_FAILURE_SERVICES.contains(serviceName(p))) + .flatMap(p -> Stream.of(p.syncClientClassName(), p.asyncClientClassName())) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + private static void restoreRegionProperty(String savedValue) { + if (savedValue != null) { + System.setProperty("aws.region", savedValue); + } else { + System.clearProperty("aws.region"); + } + } +} diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/warmup/allservices/OperationRecordingInterceptor.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/warmup/allservices/OperationRecordingInterceptor.java new file mode 100644 index 000000000000..cafc7d836ca8 --- /dev/null +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/warmup/allservices/OperationRecordingInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.warmup.allservices; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; + +/** + * Records invoked operation names. Registered as a global interceptor via + * {@code software/amazon/awssdk/global/handlers/execution.interceptors} to observe clients built inside providers. + */ +public class OperationRecordingInterceptor implements ExecutionInterceptor { + + private static final List OPERATION_NAMES = new CopyOnWriteArrayList<>(); + + public static List operationNames() { + return OPERATION_NAMES; + } + + public static void reset() { + OPERATION_NAMES.clear(); + } + + @Override + public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) { + OPERATION_NAMES.add(executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME)); + } +} diff --git a/test/warmup-tests/src/test/resources/log4j2.properties b/test/warmup-tests/src/test/resources/log4j2.properties new file mode 100644 index 000000000000..a9839e95d9e0 --- /dev/null +++ b/test/warmup-tests/src/test/resources/log4j2.properties @@ -0,0 +1,26 @@ +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0 +# +# or in the "license" file accompanying this file. This file 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. +# + +status = warn + +appender.console.type = Console +appender.console.name = ConsoleAppender +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n%throwable + +# warn keeps CI output manageable while every generated provider warms up; +# LogCaptor adjusts the root level itself when capturing. +rootLogger.level = warn +rootLogger.appenderRef.stdout.ref = ConsoleAppender diff --git a/test/warmup-tests/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors b/test/warmup-tests/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors new file mode 100644 index 000000000000..8ac3e1bf150e --- /dev/null +++ b/test/warmup-tests/src/test/resources/software/amazon/awssdk/global/handlers/execution.interceptors @@ -0,0 +1 @@ +software.amazon.awssdk.warmup.allservices.OperationRecordingInterceptor