Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions test/warmup-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
</parent>
<artifactId>warmup-tests</artifactId>
<name>AWS Java SDK :: Test :: Warm-up Tests</name>
<description>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.</description>
<description>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.</description>
<url>https://aws.amazon.com/sdkforjava</url>

<dependencyManagement>
Expand All @@ -43,6 +45,14 @@
</dependencies>
</dependencyManagement>

<properties>
<!-- This module loads every service client on the classpath. When JaCoCo measures code coverage it rewrites
each class to add counters, and this makes the largest generated clients (e.g. SSM, Glue) exceed the
JVM class-size limit and fail to load (ClassTooLargeException). We do not need coverage numbers for this
test module, so turn JaCoCo off here. -->
<jacoco.skip>true</jacoco.skip>
</properties>

<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
Expand All @@ -55,6 +65,12 @@
<version>${awsjavasdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>http-auth-aws-crt</artifactId>
<version>${awsjavasdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache5-client</artifactId>
Expand Down Expand Up @@ -91,6 +107,31 @@
<version>${awsjavasdk.version}</version>
<scope>test</scope>
</dependency>
<!-- Every service module, so the generated SdkWarmUpProviders of all services are on the test classpath.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: any reason we want to run tests for all services? How long does it take to run all tests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, we run all services because the warm-up operation is selected per service from each service's model; only executing every generated provider proves each one works. The full module runs offline (Canned Req/Response).

These test guard the customer path: a customer with all services on the classpath calls SdkWarmUp.prime(), and a broken provider logs a WARN and skips that service's warm-up. These tests turn that customer-visible WARN into a build failure here.

New services are covered automatically, the aggregate dependency picks up each new service, and the completeness test fails the build if a service ships without a registered provider

@joviegas joviegas Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I will get the exact time once I merge the PR 7183. The issues mentioned in this PR were caught by these tests

@joviegas joviegas Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The building of all the modules without mvn -T1C maven option takes 50 mins.
The junits overall take 1 minute.
The overall codebuild takes 94m if executed as part of exsiting jdk Junit buildss (That was because a a failed build performed a retry)
Thus I am planning to move this as a separate test similar to v2-migration-test and s3-regression test that can run parallely.

I tested this on codebuild with the below mvn command similar to v2-migration-test

mvn clean install -pl :warmup-tests,:bom-internal -am -P quick -T1C $MAVEN_OPTIONS
mvn install -pl :warmup-tests -T2C $MAVEN_OPTIONS

This took 23.9 mins

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Apologies, the earlier 94 mins was a confusion caused by the new way the builds are handled now. There was a retry of another build (the SonarCloud step), and that retry is what showed the increase.

I pushed a dummy commit, and this time the build passed with no retries and the time is the same as before.

So to answer your question: these tests do not add time to the JDK build. The service modules are already built at that point, and those already built modules are reused by the JUnit tests, which take a minute or less.
IMO we donot need a separate test build just for this .

This artifact is updated automatically when a new service is added (FinalizeNewServiceModuleMain),
so new services are covered by the warm-up tests without any change to this module. -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-sdk-java</artifactId>
<version>${awsjavasdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>test-utils</artifactId>
<version>${awsjavasdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down Expand Up @@ -125,6 +166,16 @@
<skip>true</skip>
</configuration>
</plugin>
<!-- Fork a fresh JVM per test class so SdkWarmUp's per-JVM primed state does not leak between
warm-up test classes. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> 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<Named<SdkWarmUpProvider>> generatedProviders() {
return serviceProviders().map(p -> Named.of(p.getClass().getSimpleName(), p));
}

private static Stream<SdkWarmUpProvider> 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<String> 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<String> serviceWarmUpFailures(LogCaptor logCaptor) {
Set<String> 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<String> 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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> OPERATION_NAMES = new CopyOnWriteArrayList<>();

public static List<String> 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));
}
}
26 changes: 26 additions & 0 deletions test/warmup-tests/src/test/resources/log4j2.properties
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
software.amazon.awssdk.warmup.allservices.OperationRecordingInterceptor
Loading