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 extends SdkClient> 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