From 5831ecdc65909371fc9ad16c10eb6fe67b8ab493 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Thu, 9 Jul 2026 16:02:41 -0700 Subject: [PATCH 1/4] Add overloaded menthod for SdkWarmUp.prime overloaded to accept specific clients that needs to be warmedUp --- .../amazon/awssdk/core/crac/SdkWarmUp.java | 71 +++++++ .../internal/crac/TargetedWarmUpInvoker.java | 105 +++++++++ .../core/crac/RegisteredAsyncClient.java | 24 +++ .../core/crac/RegisteredSyncClient.java | 25 +++ .../core/crac/RegisteredWarmUpProvider.java | 12 +- .../awssdk/core/crac/SdkWarmUpTest.java | 36 +++- .../crac/TargetedWarmUpInvokerTest.java | 170 +++++++++++++++ .../CodingConventionWithSuppressionTest.java | 3 +- .../warmup/SdkWarmUpPrimeTargetedTest.java | 201 ++++++++++++++++++ 9 files changed, 644 insertions(+), 3 deletions(-) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredAsyncClient.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredSyncClient.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java create mode 100644 test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeTargetedTest.java diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java index 9bd34cab3122..e76a5f4b180f 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java @@ -15,11 +15,21 @@ package software.amazon.awssdk.core.crac; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.ServiceLoader; +import java.util.Set; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.SdkClient; import software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker; +import software.amazon.awssdk.core.internal.crac.TargetedWarmUpInvoker; +import software.amazon.awssdk.core.internal.http.loader.AsyncHttpClientWarmer; import software.amazon.awssdk.core.internal.http.loader.ClasspathHttpWarmupInvoker; +import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; +import software.amazon.awssdk.utils.Logger; /** * Entry point for warming up SDK service request paths before a Coordinated Restore at Checkpoint (CRaC) @@ -45,10 +55,17 @@ @SdkPublicApi public final class SdkWarmUp { + private static final Logger log = Logger.loggerFor(SdkWarmUp.class); + private static final Object PRIME_LOCK = new Object(); private static volatile boolean primed = false; + // Client class names already primed by prime(Class...). Access guarded by TARGETED_LOCK. + private static final Set PRIMED_CLIENTS = new HashSet<>(); + + private static final Object TARGETED_LOCK = new Object(); + private SdkWarmUp() { } @@ -71,4 +88,58 @@ public static void prime() { primed = true; } } + + /** + * Primes only the given service clients, warming the sync path for a sync client and the async path for an async + * client. A class matched by no provider is logged at warn and skipped. Best-effort and safe to call concurrently; + * a client already primed by a previous call is skipped. + * + * @param clients the service client classes to prime, for example {@code S3Client.class} or + * {@code S3AsyncClient.class}. + */ + @SafeVarargs + public static void prime(Class... clients) { + if (clients == null || clients.length == 0) { + log.debug(() -> "SdkWarmUp.prime(Class...) called with no clients; nothing to do."); + return; + } + + Set toPrime = new LinkedHashSet<>(); + synchronized (TARGETED_LOCK) { + for (Class client : clients) { + if (client == null) { + continue; + } + String name = client.getName(); + if (!PRIMED_CLIENTS.contains(name)) { + toPrime.add(name); + } + } + } + if (toPrime.isEmpty()) { + return; + } + + // Warm outside TARGETED_LOCK so no lock is held across network I/O. Concurrent calls racing on the same + // client may warm it twice; warming is idempotent, so that is harmless. + Set matched = TargetedWarmUpInvoker.create().invoke(toPrime); + if (matched.contains(ClientType.SYNC)) { + SyncHttpClientWarmer.create().warmAll(); + } + if (matched.contains(ClientType.ASYNC)) { + AsyncHttpClientWarmer.create().warmAll(); + } + + // Record names only after warming completes, so a run that throws is retried by a later call. + synchronized (TARGETED_LOCK) { + PRIMED_CLIENTS.addAll(toPrime); + } + } + + @SdkTestInternalApi + public static void resetTargetedPrimeStateForTesting() { + synchronized (TARGETED_LOCK) { + PRIMED_CLIENTS.clear(); + } + } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java new file mode 100644 index 000000000000..b36614657d91 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java @@ -0,0 +1,105 @@ +/* + * 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.core.internal.crac; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.utils.Logger; + +/** + * Warms only the service clients named by {@code SdkWarmUp.prime(Class...)}. Matches each requested client class name + * against the discovered {@link SdkWarmUpProvider}s' sync and async class names, then warms the matched transport only. + */ +@SdkInternalApi +public final class TargetedWarmUpInvoker { + + private static final Logger log = Logger.loggerFor(TargetedWarmUpInvoker.class); + + private final WarmUpServiceLoader serviceLoader; + + @SdkTestInternalApi + TargetedWarmUpInvoker(WarmUpServiceLoader serviceLoader) { + this.serviceLoader = serviceLoader; + } + + public static TargetedWarmUpInvoker create() { + return new TargetedWarmUpInvoker(WarmUpServiceLoader.INSTANCE); + } + + /** + * Warms the provider transport matching each requested client class name. Best-effort: an unmatched name is logged + * at warn and skipped, and a provider that fails does not stop the others. + * + * @param requestedClassNames the fully qualified sync or async client class names to warm. + * @return the transports that were matched, for narrowing the follow-on HTTP-client warm-up. + */ + public Set invoke(Collection requestedClassNames) { + Set matchedTransports = EnumSet.noneOf(ClientType.class); + if (requestedClassNames.isEmpty()) { + return matchedTransports; + } + + List providers = loadProviders(); + for (String requested : requestedClassNames) { + Set matched = warmMatching(requested, providers); + if (matched.isEmpty()) { + log.warn(() -> "No warm-up provider matched client class " + requested + "; skipping."); + } + matchedTransports.addAll(matched); + } + return matchedTransports; + } + + private Set warmMatching(String requested, List providers) { + Set matched = EnumSet.noneOf(ClientType.class); + for (SdkWarmUpProvider provider : providers) { + ClientType transport = transportFor(requested, provider); + if (transport == null) { + continue; + } + matched.add(transport); + try { + provider.warmUpClient(transport); + } catch (RuntimeException | LinkageError e) { + log.warn(() -> "Warm-up failed for " + provider.getClass().getName() + " and was skipped.", e); + } + } + return matched; + } + + private ClientType transportFor(String requested, SdkWarmUpProvider provider) { + if (requested.equals(provider.syncClientClassName())) { + return ClientType.SYNC; + } + if (requested.equals(provider.asyncClientClassName())) { + return ClientType.ASYNC; + } + return null; + } + + private List loadProviders() { + List providers = new ArrayList<>(); + WarmUpDiscovery.forEachDiscovered(serviceLoader.loadProviders(), providers::add); + return providers; + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredAsyncClient.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredAsyncClient.java new file mode 100644 index 000000000000..77e92477d628 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredAsyncClient.java @@ -0,0 +1,24 @@ +/* + * 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.core.crac; + +import software.amazon.awssdk.core.SdkClient; + +/** + * Test-only client whose fully qualified name matches {@link RegisteredWarmUpProvider#asyncClientClassName()}. + */ +public interface RegisteredAsyncClient extends SdkClient { +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredSyncClient.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredSyncClient.java new file mode 100644 index 000000000000..39ea86c34876 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredSyncClient.java @@ -0,0 +1,25 @@ +/* + * 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.core.crac; + +import software.amazon.awssdk.core.SdkClient; + +/** + * Test-only client whose fully qualified name matches {@link RegisteredWarmUpProvider#syncClientClassName()}, so + * {@code SdkWarmUp.prime(RegisteredSyncClient.class)} matches that provider's sync transport. + */ +public interface RegisteredSyncClient extends SdkClient { +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java index 726e83b9812f..270d3e91af85 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java @@ -15,18 +15,27 @@ package software.amazon.awssdk.core.crac; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import software.amazon.awssdk.core.ClientType; /** * Test-only {@link SdkWarmUpProvider} registered in test-scoped {@code META-INF/services} so a real {@link - * java.util.ServiceLoader} discovers and instantiates it by name. {@code INVOCATIONS} is static because the loader + * java.util.ServiceLoader} discovers and instantiates it by name. Both static fields are static because the loader * builds its own instance. Must be public with a no-arg constructor for ServiceLoader. + * + *

{@code INVOCATIONS} counts full {@link #warmUp()} calls; {@code WARMED_CLIENTS} records the transports passed to + * {@link #warmUpClient(ClientType)}, so a test can tell the targeted path from the full one. */ public final class RegisteredWarmUpProvider implements SdkWarmUpProvider { public static final AtomicInteger INVOCATIONS = new AtomicInteger(); + public static final Set WARMED_CLIENTS = + Collections.synchronizedSet(EnumSet.noneOf(ClientType.class)); + @Override public void warmUp() { INVOCATIONS.incrementAndGet(); @@ -44,5 +53,6 @@ public String asyncClientClassName() { @Override public void warmUpClient(ClientType clientType) { + WARMED_CLIENTS.add(clientType); } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java index e21cdf3dd657..0e886e64e2fe 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.core.crac; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import java.util.ArrayList; import java.util.List; @@ -23,6 +24,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.SdkClient; /** * Tests the static {@link SdkWarmUp#prime()} entry point end to end through {@link java.util.ServiceLoader}, @@ -38,6 +41,9 @@ void setup() { // Dummy region so prime()'s HTTP warm-up resolves a non-existent STS host and fails DNS immediately, keeping the test offline. savedRegionProperty = System.getProperty("aws.region"); System.setProperty("aws.region", "warmup-unit-test"); + SdkWarmUp.resetTargetedPrimeStateForTesting(); + RegisteredWarmUpProvider.INVOCATIONS.set(0); + RegisteredWarmUpProvider.WARMED_CLIENTS.clear(); } @AfterEach @@ -51,7 +57,6 @@ void teardown() { @Test void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws InterruptedException { - RegisteredWarmUpProvider.INVOCATIONS.set(0); int threadCount = 16; CountDownLatch start = new CountDownLatch(1); CountDownLatch done = new CountDownLatch(threadCount); @@ -80,4 +85,33 @@ void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws Interrup assertThat(RegisteredWarmUpProvider.INVOCATIONS.get()).isEqualTo(1); } + + @Test + void prime_withMatchingSyncClient_warmsSyncTransportThroughWarmUpClient() { + SdkWarmUp.prime(RegisteredSyncClient.class); + + assertThat(RegisteredWarmUpProvider.INVOCATIONS.get()).isEqualTo(0); + assertThat(RegisteredWarmUpProvider.WARMED_CLIENTS).containsExactly(ClientType.SYNC); + } + + @Test + void prime_withMatchingAsyncClient_warmsAsyncTransportThroughWarmUpClient() { + SdkWarmUp.prime(RegisteredAsyncClient.class); + + assertThat(RegisteredWarmUpProvider.INVOCATIONS.get()).isEqualTo(0); + assertThat(RegisteredWarmUpProvider.WARMED_CLIENTS).containsExactly(ClientType.ASYNC); + } + + @Test + void prime_withUnmatchedClient_doesNotThrow() { + assertThatCode(() -> SdkWarmUp.prime(UnmatchedClient.class)).doesNotThrowAnyException(); + } + + @Test + void prime_withEmptyArray_isNoOp() { + assertThatCode(() -> SdkWarmUp.prime(new Class[0])).doesNotThrowAnyException(); + } + + interface UnmatchedClient extends SdkClient { + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java new file mode 100644 index 000000000000..e0a172d195f9 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java @@ -0,0 +1,170 @@ +/* + * 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.core.internal.crac; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Set; +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.testutils.LogCaptor; + +class TargetedWarmUpInvokerTest { + + private static final String S3_SYNC = "software.amazon.awssdk.services.s3.S3Client"; + private static final String S3_ASYNC = "software.amazon.awssdk.services.s3.S3AsyncClient"; + private static final String DDB_SYNC = "software.amazon.awssdk.services.dynamodb.DynamoDbClient"; + + @Test + void invoke_syncClassRequested_warmsSyncTransportOnly() { + RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + + Set matched = invokerLoading(s3).invoke(Arrays.asList(S3_SYNC)); + + assertThat(s3.warmedTypes()).containsExactly(ClientType.SYNC); + assertThat(matched).containsExactly(ClientType.SYNC); + } + + @Test + void invoke_asyncClassRequested_warmsAsyncTransportOnly() { + RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + + Set matched = invokerLoading(s3).invoke(Arrays.asList(S3_ASYNC)); + + assertThat(s3.warmedTypes()).containsExactly(ClientType.ASYNC); + assertThat(matched).containsExactly(ClientType.ASYNC); + } + + @Test + void invoke_bothClassesRequested_warmsBothTransports() { + RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + + Set matched = invokerLoading(s3).invoke(Arrays.asList(S3_SYNC, S3_ASYNC)); + + assertThat(s3.warmedTypes()).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); + assertThat(matched).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); + } + + @Test + void invoke_classAcrossTwoProviders_warmsOnlyTheMatchingProvider() { + RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + RecordingProvider ddb = new RecordingProvider(DDB_SYNC, null); + + invokerLoading(s3, ddb).invoke(Arrays.asList(DDB_SYNC)); + + assertThat(s3.warmedTypes()).isEmpty(); + assertThat(ddb.warmedTypes()).containsExactly(ClientType.SYNC); + } + + @Test + void invoke_unmatchedClass_logsWarnAndDoesNotThrow() { + RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + Set matched = invokerLoading(s3).invoke(Arrays.asList("com.example.NotAClient")); + + assertThat(matched).isEmpty(); + assertThat(s3.warmedTypes()).isEmpty(); + assertThat(logCaptor.loggedEvents()) + .anyMatch(event -> event.getLevel() == Level.WARN + && event.getMessage().getFormattedMessage().contains("com.example.NotAClient")); + } + } + + @Test + void invoke_whenProviderWarmUpThrows_stillReturnsMatchedTransport() { + SdkWarmUpProvider throwing = new TestProvider(S3_SYNC, S3_ASYNC) { + @Override + public void warmUpClient(ClientType clientType) { + throw new RuntimeException("boom"); + } + }; + + // A throwing warmUpClient must not stop the follow-on HTTP warm-up: the matched transport is still returned so + // the caller can narrow the HTTP warmers. + Set matched = invokerLoading(throwing).invoke(Arrays.asList(S3_SYNC)); + + assertThat(matched).containsExactly(ClientType.SYNC); + } + + @Test + void invoke_emptyRequest_isNoOp() { + RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + + Set matched = invokerLoading(s3).invoke(Collections.emptyList()); + + assertThat(matched).isEmpty(); + assertThat(s3.warmedTypes()).isEmpty(); + } + + private TargetedWarmUpInvoker invokerLoading(SdkWarmUpProvider... providers) { + WarmUpServiceLoader loader = new WarmUpServiceLoader() { + @Override + Iterator loadProviders() { + return Arrays.asList(providers).iterator(); + } + }; + return new TargetedWarmUpInvoker(loader); + } + + private static class TestProvider implements SdkWarmUpProvider { + private final String syncName; + private final String asyncName; + + TestProvider(String syncName, String asyncName) { + this.syncName = syncName; + this.asyncName = asyncName; + } + + @Override + public String syncClientClassName() { + return syncName; + } + + @Override + public String asyncClientClassName() { + return asyncName; + } + + @Override + public void warmUpClient(ClientType clientType) { + } + } + + private static final class RecordingProvider extends TestProvider { + private final Set warmed = EnumSet.noneOf(ClientType.class); + + RecordingProvider(String syncName, String asyncName) { + super(syncName, asyncName); + } + + @Override + public void warmUpClient(ClientType clientType) { + warmed.add(clientType); + } + + Set warmedTypes() { + return warmed; + } + } +} diff --git a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java index edfff3c08bf5..90f476490950 100644 --- a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java +++ b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java @@ -61,7 +61,8 @@ public class CodingConventionWithSuppressionTest { ArchUtils.classNameToPattern(KnownContentLengthAsyncRequestBodySubscriber.class), ArchUtils.classNameToPattern(UnknownContentLengthAsyncRequestBodySubscriber.class), ArchUtils.classNameToPattern(CopyObjectHelper.class), - ArchUtils.classNameToPattern("software.amazon.awssdk.core.internal.crac.WarmUpDiscovery"))); + ArchUtils.classNameToPattern("software.amazon.awssdk.core.internal.crac.WarmUpDiscovery"), + ArchUtils.classNameToPattern("software.amazon.awssdk.core.internal.crac.TargetedWarmUpInvoker"))); private static final Set ALLOWED_ERROR_LOG_SUPPRESSION = new HashSet<>( Arrays.asList( diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeTargetedTest.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeTargetedTest.java new file mode 100644 index 000000000000..ed0a04eebf43 --- /dev/null +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeTargetedTest.java @@ -0,0 +1,201 @@ +/* + * 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.http.warmup; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkClient; +import software.amazon.awssdk.core.crac.SdkWarmUp; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.sts.StsAsyncClient; +import software.amazon.awssdk.services.sts.StsClient; + +/** + * End-to-end integration test of {@link SdkWarmUp#prime(Class...)} with real service clients (STS, DynamoDB) whose + * generated {@link SdkWarmUpProvider} implementations are on the classpath via ServiceLoader. Verifies that: + *

    + *
  • Passing a sync client class routes through {@code warmUpClient(SYNC)} and triggers the sync HTTP warmer.
  • + *
  • Passing an async client class routes through {@code warmUpClient(ASYNC)} and triggers the async HTTP warmer.
  • + *
  • Idempotency: a second call with the same client is a no-op.
  • + *
  • Multiple services can be primed in one call.
  • + *
  • Concurrent callers do not throw or corrupt state.
  • + *
  • An unmatched client is a safe no-op.
  • + *
+ * + *

The HTTP warmers resolve their endpoint via {@code RegionEndpointResolver}, which reads {@code aws.region}. Tests + * set a dummy region so DNS fails fast and no real network traffic leaves the host. The generated providers use + * {@code CannedResponseHttpClient}, so their {@code warmUpClient} does not need the network either. + */ +class SdkWarmUpPrimeTargetedTest { + + private WireMockServer mockServer; + private String savedRegionProperty; + + @BeforeEach + void setUp() { + mockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + mockServer.start(); + mockServer.stubFor(any(anyUrl()).willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "https://aws.amazon.com/iam"))); + + // Use a non-routable region so the HTTP warmer's STS endpoint fails DNS immediately; the warm-up is best-effort + // and swallows the failure, keeping the test offline. + savedRegionProperty = System.getProperty("aws.region"); + System.setProperty("aws.region", "warmup-integration-test"); + + SdkWarmUp.resetTargetedPrimeStateForTesting(); + } + + @AfterEach + void tearDown() { + mockServer.stop(); + if (savedRegionProperty != null) { + System.setProperty("aws.region", savedRegionProperty); + } else { + System.clearProperty("aws.region"); + } + } + + /** + * Sanity check: ServiceLoader must discover at least the STS and DynamoDB providers we depend on. + */ + @Test + void serviceLoader_discoversExpectedProviders() { + int count = 0; + for (SdkWarmUpProvider ignored : ServiceLoader.load(SdkWarmUpProvider.class)) { + count++; + } + // STS has 1 provider, DynamoDB has 2 (DynamoDb + DynamoDbStreams). + assertThat(count).isGreaterThanOrEqualTo(3); + } + + @Test + void prime_withStsSyncClient_completesWithoutError() { + assertThatCode(() -> SdkWarmUp.prime(StsClient.class)).doesNotThrowAnyException(); + } + + @Test + void prime_withStsAsyncClient_completesWithoutError() { + assertThatCode(() -> SdkWarmUp.prime(StsAsyncClient.class)).doesNotThrowAnyException(); + } + + @Test + void prime_withDynamoDbSyncClient_completesWithoutError() { + assertThatCode(() -> SdkWarmUp.prime(DynamoDbClient.class)).doesNotThrowAnyException(); + } + + @Test + void prime_withDynamoDbAsyncClient_completesWithoutError() { + assertThatCode(() -> SdkWarmUp.prime(DynamoDbAsyncClient.class)).doesNotThrowAnyException(); + } + + @Test + void prime_withMultipleServices_completesWithoutError() { + assertThatCode(() -> SdkWarmUp.prime(StsClient.class, DynamoDbClient.class, StsAsyncClient.class)) + .doesNotThrowAnyException(); + } + + @Test + void prime_calledTwiceWithSameClient_isIdempotent() { + SdkWarmUp.prime(StsClient.class); + + // Second call should be a no-op (client already recorded as primed). + assertThatCode(() -> SdkWarmUp.prime(StsClient.class)).doesNotThrowAnyException(); + } + + @Test + void prime_calledWithNewClientAfterPrevious_primesOnlyNewClient() { + SdkWarmUp.prime(StsClient.class); + + // DynamoDB is new; STS should not be re-primed. + assertThatCode(() -> SdkWarmUp.prime(DynamoDbClient.class, StsClient.class)).doesNotThrowAnyException(); + } + + @Test + void prime_concurrentCalls_doNotThrow() throws InterruptedException { + int threadCount = 8; + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(threadCount); + List threads = new ArrayList<>(); + + Class[] clients = {StsClient.class, StsAsyncClient.class, DynamoDbClient.class, DynamoDbAsyncClient.class}; + for (int i = 0; i < threadCount; i++) { + @SuppressWarnings("unchecked") + Class client = (Class) clients[i % clients.length]; + Thread thread = new Thread(() -> { + try { + start.await(); + SdkWarmUp.prime(client); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + done.countDown(); + } + }); + threads.add(thread); + thread.start(); + } + + start.countDown(); + done.await(); + for (Thread thread : threads) { + thread.join(); + } + // If we get here without exceptions or hangs, concurrency is safe. + } + + @Test + void prime_withUnmatchedClient_isNoOpAndSendsNoRequest() { + mockServer.stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200))); + + assertThatCode(() -> SdkWarmUp.prime(UnregisteredClient.class)).doesNotThrowAnyException(); + + // No provider matches, so no HTTP warmer runs and nothing hits the mock server. + mockServer.verify(0, anyRequestedFor(anyUrl())); + } + + @Test + void prime_withNullArray_isNoOp() { + assertThatCode(() -> SdkWarmUp.prime((Class[]) null)).doesNotThrowAnyException(); + } + + @Test + void prime_withEmptyArray_isNoOp() { + assertThatCode(() -> SdkWarmUp.prime(new Class[0])).doesNotThrowAnyException(); + } + + interface UnregisteredClient extends SdkClient { + } +} From ccf1b3a0aca660696439a11a7fb40a865db4ebb8 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 10 Jul 2026 10:02:27 -0700 Subject: [PATCH 2/4] Updated idempotency testing --- .../amazon/awssdk/core/crac/SdkWarmUp.java | 37 ++------ .../internal/crac/PrimedClientRegistry.java | 55 +++++++++++ .../awssdk/core/crac/SdkWarmUpTest.java | 2 +- .../crac/PrimedClientRegistryTest.java | 91 +++++++++++++++++++ .../crac/TargetedWarmUpInvokerTest.java | 51 +++++------ test/warmup-tests/pom.xml | 13 ++- .../http/warmup/IdempotenceSyncClient.java | 25 +++++ .../warmup/IdempotenceWarmUpProvider.java | 54 +++++++++++ .../http/warmup/SelectiveAsyncClient.java | 25 +++++ .../http/warmup/SelectiveSyncClient.java | 25 +++++ .../http/warmup/SelectiveWarmUpProvider.java | 61 +++++++++++++ ....amazon.awssdk.core.crac.SdkWarmUpProvider | 2 + .../warmup/SdkWarmUpPrimeIdempotenceTest.java | 62 +++++++++++++ .../warmup/SdkWarmUpPrimeSelectiveTest.java | 65 +++++++++++++ .../warmup/SdkWarmUpPrimeTargetedTest.java | 81 ++++++++--------- 15 files changed, 550 insertions(+), 99 deletions(-) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/PrimedClientRegistry.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/PrimedClientRegistryTest.java create mode 100644 test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/IdempotenceSyncClient.java create mode 100644 test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/IdempotenceWarmUpProvider.java create mode 100644 test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveAsyncClient.java create mode 100644 test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveSyncClient.java create mode 100644 test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveWarmUpProvider.java create mode 100644 test/warmup-tests/src/main/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider create mode 100644 test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeIdempotenceTest.java create mode 100644 test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeSelectiveTest.java diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java index e76a5f4b180f..ca1a280cce04 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java @@ -15,16 +15,15 @@ package software.amazon.awssdk.core.crac; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.ServiceLoader; import java.util.Set; import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.SdkClient; import software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker; +import software.amazon.awssdk.core.internal.crac.PrimedClientRegistry; import software.amazon.awssdk.core.internal.crac.TargetedWarmUpInvoker; import software.amazon.awssdk.core.internal.http.loader.AsyncHttpClientWarmer; import software.amazon.awssdk.core.internal.http.loader.ClasspathHttpWarmupInvoker; @@ -61,10 +60,7 @@ public final class SdkWarmUp { private static volatile boolean primed = false; - // Client class names already primed by prime(Class...). Access guarded by TARGETED_LOCK. - private static final Set PRIMED_CLIENTS = new HashSet<>(); - - private static final Object TARGETED_LOCK = new Object(); + private static final PrimedClientRegistry PRIMED_CLIENTS = new PrimedClientRegistry(); private SdkWarmUp() { } @@ -104,24 +100,18 @@ public static void prime(Class... clients) { return; } - Set toPrime = new LinkedHashSet<>(); - synchronized (TARGETED_LOCK) { - for (Class client : clients) { - if (client == null) { - continue; - } - String name = client.getName(); - if (!PRIMED_CLIENTS.contains(name)) { - toPrime.add(name); - } + Set requested = new LinkedHashSet<>(); + for (Class client : clients) { + if (client != null) { + requested.add(client.getName()); } } + Set toPrime = PRIMED_CLIENTS.selectUnprimed(requested); if (toPrime.isEmpty()) { return; } - // Warm outside TARGETED_LOCK so no lock is held across network I/O. Concurrent calls racing on the same - // client may warm it twice; warming is idempotent, so that is harmless. + // Racing calls may double-warm the same client; warming is idempotent, so that is harmless. Set matched = TargetedWarmUpInvoker.create().invoke(toPrime); if (matched.contains(ClientType.SYNC)) { SyncHttpClientWarmer.create().warmAll(); @@ -131,15 +121,6 @@ public static void prime(Class... clients) { } // Record names only after warming completes, so a run that throws is retried by a later call. - synchronized (TARGETED_LOCK) { - PRIMED_CLIENTS.addAll(toPrime); - } - } - - @SdkTestInternalApi - public static void resetTargetedPrimeStateForTesting() { - synchronized (TARGETED_LOCK) { - PRIMED_CLIENTS.clear(); - } + PRIMED_CLIENTS.markPrimed(toPrime); } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/PrimedClientRegistry.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/PrimedClientRegistry.java new file mode 100644 index 000000000000..77931b4f87d4 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/PrimedClientRegistry.java @@ -0,0 +1,55 @@ +/* + * 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.core.internal.crac; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; + +/** + * Tracks which service clients {@code SdkWarmUp.prime(Class...)} has already warmed, so each is warmed at most once per + * JVM. State is per-instance, so it is unit-testable with fresh instances; production uses a single long-lived instance. + */ +@ThreadSafe +@SdkInternalApi +public final class PrimedClientRegistry { + + private final Set primed = ConcurrentHashMap.newKeySet(); + + /** + * Returns the not-yet-primed subset of {@code clientClassNames}, in encounter order, ignoring nulls. Warm the + * returned names, then pass them to {@link #markPrimed(Collection)}. + */ + public Set selectUnprimed(Collection clientClassNames) { + Set unprimed = new LinkedHashSet<>(); + for (String name : clientClassNames) { + if (name != null && !primed.contains(name)) { + unprimed.add(name); + } + } + return unprimed; + } + + /** + * Records the given names as primed. Call only after warming completes, so a failed run is retried by a later call. + */ + public void markPrimed(Collection clientClassNames) { + primed.addAll(clientClassNames); + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java index 0e886e64e2fe..493c77011d1e 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java @@ -41,7 +41,6 @@ void setup() { // Dummy region so prime()'s HTTP warm-up resolves a non-existent STS host and fails DNS immediately, keeping the test offline. savedRegionProperty = System.getProperty("aws.region"); System.setProperty("aws.region", "warmup-unit-test"); - SdkWarmUp.resetTargetedPrimeStateForTesting(); RegisteredWarmUpProvider.INVOCATIONS.set(0); RegisteredWarmUpProvider.WARMED_CLIENTS.clear(); } @@ -90,6 +89,7 @@ void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws Interrup void prime_withMatchingSyncClient_warmsSyncTransportThroughWarmUpClient() { SdkWarmUp.prime(RegisteredSyncClient.class); + // Targeted prime warms via warmUpClient() only; the full warmUp() path (counted by INVOCATIONS) must not run. assertThat(RegisteredWarmUpProvider.INVOCATIONS.get()).isEqualTo(0); assertThat(RegisteredWarmUpProvider.WARMED_CLIENTS).containsExactly(ClientType.SYNC); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/PrimedClientRegistryTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/PrimedClientRegistryTest.java new file mode 100644 index 000000000000..0c3170800cec --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/PrimedClientRegistryTest.java @@ -0,0 +1,91 @@ +/* + * 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.core.internal.crac; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link PrimedClientRegistry}. Each test uses a fresh instance, so the dedup state is isolated without + * any static reset hook or reflection. + */ +class PrimedClientRegistryTest { + + private static final String SERVICE1_SYNC = "software.amazon.awssdk.services.service1.Service1Client"; + private static final String SERVICE1_ASYNC = "software.amazon.awssdk.services.service1.Service1AsyncClient"; + + @Test + void selectUnprimed_freshRegistry_returnsAllRequested() { + PrimedClientRegistry registry = new PrimedClientRegistry(); + + assertThat(registry.selectUnprimed(Arrays.asList(SERVICE1_SYNC, SERVICE1_ASYNC))) + .containsExactly(SERVICE1_SYNC, SERVICE1_ASYNC); + } + + @Test + void selectUnprimed_afterMarkPrimed_skipsAlreadyPrimed() { + PrimedClientRegistry registry = new PrimedClientRegistry(); + registry.markPrimed(Collections.singletonList(SERVICE1_SYNC)); + + assertThat(registry.selectUnprimed(Arrays.asList(SERVICE1_SYNC, SERVICE1_ASYNC))) + .containsExactly(SERVICE1_ASYNC); + } + + @Test + void selectUnprimed_allAlreadyPrimed_returnsEmpty() { + PrimedClientRegistry registry = new PrimedClientRegistry(); + registry.markPrimed(Arrays.asList(SERVICE1_SYNC, SERVICE1_ASYNC)); + + assertThat(registry.selectUnprimed(Arrays.asList(SERVICE1_SYNC, SERVICE1_ASYNC))).isEmpty(); + } + + @Test + void selectUnprimed_preservesEncounterOrderAndDeduplicates() { + PrimedClientRegistry registry = new PrimedClientRegistry(); + + assertThat(registry.selectUnprimed(Arrays.asList(SERVICE1_ASYNC, SERVICE1_SYNC, SERVICE1_ASYNC))) + .containsExactly(SERVICE1_ASYNC, SERVICE1_SYNC); + } + + @Test + void selectUnprimed_ignoresNullNames() { + PrimedClientRegistry registry = new PrimedClientRegistry(); + + assertThat(registry.selectUnprimed(Arrays.asList(SERVICE1_SYNC, null, SERVICE1_ASYNC))) + .containsExactly(SERVICE1_SYNC, SERVICE1_ASYNC); + } + + @Test + void selectUnprimed_emptyInput_returnsEmpty() { + PrimedClientRegistry registry = new PrimedClientRegistry(); + + assertThat(registry.selectUnprimed(Collections.emptyList())).isEmpty(); + } + + @Test + void separateInstances_doNotShareState() { + PrimedClientRegistry first = new PrimedClientRegistry(); + first.markPrimed(Collections.singletonList(SERVICE1_SYNC)); + + PrimedClientRegistry second = new PrimedClientRegistry(); + + assertThat(second.selectUnprimed(Collections.singletonList(SERVICE1_SYNC))) + .containsExactly(SERVICE1_SYNC); + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java index e0a172d195f9..9ebd63739f8a 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java @@ -16,7 +16,6 @@ package software.amazon.awssdk.core.internal.crac; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import java.util.Arrays; import java.util.Collections; @@ -31,60 +30,60 @@ class TargetedWarmUpInvokerTest { - private static final String S3_SYNC = "software.amazon.awssdk.services.s3.S3Client"; - private static final String S3_ASYNC = "software.amazon.awssdk.services.s3.S3AsyncClient"; - private static final String DDB_SYNC = "software.amazon.awssdk.services.dynamodb.DynamoDbClient"; + private static final String SERVICE1_SYNC = "software.amazon.awssdk.services.service1.Service1Client"; + private static final String SERVICE1_ASYNC = "software.amazon.awssdk.services.service1.Service1AsyncClient"; + private static final String SERVICE2_SYNC = "software.amazon.awssdk.services.service2.Service2Client"; @Test void invoke_syncClassRequested_warmsSyncTransportOnly() { - RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); - Set matched = invokerLoading(s3).invoke(Arrays.asList(S3_SYNC)); + Set matched = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_SYNC)); - assertThat(s3.warmedTypes()).containsExactly(ClientType.SYNC); + assertThat(service1.warmedTypes()).containsExactly(ClientType.SYNC); assertThat(matched).containsExactly(ClientType.SYNC); } @Test void invoke_asyncClassRequested_warmsAsyncTransportOnly() { - RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); - Set matched = invokerLoading(s3).invoke(Arrays.asList(S3_ASYNC)); + Set matched = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_ASYNC)); - assertThat(s3.warmedTypes()).containsExactly(ClientType.ASYNC); + assertThat(service1.warmedTypes()).containsExactly(ClientType.ASYNC); assertThat(matched).containsExactly(ClientType.ASYNC); } @Test void invoke_bothClassesRequested_warmsBothTransports() { - RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); - Set matched = invokerLoading(s3).invoke(Arrays.asList(S3_SYNC, S3_ASYNC)); + Set matched = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_SYNC, SERVICE1_ASYNC)); - assertThat(s3.warmedTypes()).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); + assertThat(service1.warmedTypes()).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); assertThat(matched).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); } @Test void invoke_classAcrossTwoProviders_warmsOnlyTheMatchingProvider() { - RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); - RecordingProvider ddb = new RecordingProvider(DDB_SYNC, null); + RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); + RecordingProvider service2 = new RecordingProvider(SERVICE2_SYNC, null); - invokerLoading(s3, ddb).invoke(Arrays.asList(DDB_SYNC)); + invokerLoading(service1, service2).invoke(Arrays.asList(SERVICE2_SYNC)); - assertThat(s3.warmedTypes()).isEmpty(); - assertThat(ddb.warmedTypes()).containsExactly(ClientType.SYNC); + assertThat(service1.warmedTypes()).isEmpty(); + assertThat(service2.warmedTypes()).containsExactly(ClientType.SYNC); } @Test void invoke_unmatchedClass_logsWarnAndDoesNotThrow() { - RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { - Set matched = invokerLoading(s3).invoke(Arrays.asList("com.example.NotAClient")); + Set matched = invokerLoading(service1).invoke(Arrays.asList("com.example.NotAClient")); assertThat(matched).isEmpty(); - assertThat(s3.warmedTypes()).isEmpty(); + assertThat(service1.warmedTypes()).isEmpty(); assertThat(logCaptor.loggedEvents()) .anyMatch(event -> event.getLevel() == Level.WARN && event.getMessage().getFormattedMessage().contains("com.example.NotAClient")); @@ -93,7 +92,7 @@ void invoke_unmatchedClass_logsWarnAndDoesNotThrow() { @Test void invoke_whenProviderWarmUpThrows_stillReturnsMatchedTransport() { - SdkWarmUpProvider throwing = new TestProvider(S3_SYNC, S3_ASYNC) { + SdkWarmUpProvider throwing = new TestProvider(SERVICE1_SYNC, SERVICE1_ASYNC) { @Override public void warmUpClient(ClientType clientType) { throw new RuntimeException("boom"); @@ -102,19 +101,19 @@ public void warmUpClient(ClientType clientType) { // A throwing warmUpClient must not stop the follow-on HTTP warm-up: the matched transport is still returned so // the caller can narrow the HTTP warmers. - Set matched = invokerLoading(throwing).invoke(Arrays.asList(S3_SYNC)); + Set matched = invokerLoading(throwing).invoke(Arrays.asList(SERVICE1_SYNC)); assertThat(matched).containsExactly(ClientType.SYNC); } @Test void invoke_emptyRequest_isNoOp() { - RecordingProvider s3 = new RecordingProvider(S3_SYNC, S3_ASYNC); + RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); - Set matched = invokerLoading(s3).invoke(Collections.emptyList()); + Set matched = invokerLoading(service1).invoke(Collections.emptyList()); assertThat(matched).isEmpty(); - assertThat(s3.warmedTypes()).isEmpty(); + assertThat(service1.warmedTypes()).isEmpty(); } private TargetedWarmUpInvoker invokerLoading(SdkWarmUpProvider... providers) { diff --git a/test/warmup-tests/pom.xml b/test/warmup-tests/pom.xml index b4c10b292038..d292904c998f 100644 --- a/test/warmup-tests/pom.xml +++ b/test/warmup-tests/pom.xml @@ -48,7 +48,6 @@ software.amazon.awssdk sdk-core ${awsjavasdk.version} - test software.amazon.awssdk @@ -80,6 +79,18 @@ ${awsjavasdk.version} test + + software.amazon.awssdk + sts + ${awsjavasdk.version} + test + + + software.amazon.awssdk + dynamodb + ${awsjavasdk.version} + test + org.junit.jupiter junit-jupiter diff --git a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/IdempotenceSyncClient.java b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/IdempotenceSyncClient.java new file mode 100644 index 000000000000..79f37f03015e --- /dev/null +++ b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/IdempotenceSyncClient.java @@ -0,0 +1,25 @@ +/* + * 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.http.warmup; + +import software.amazon.awssdk.core.SdkClient; + +/** + * Test-only client matching {@link IdempotenceWarmUpProvider#syncClientClassName()}, primed only by + * {@link SdkWarmUpPrimeIdempotenceTest}. + */ +public interface IdempotenceSyncClient extends SdkClient { +} diff --git a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/IdempotenceWarmUpProvider.java b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/IdempotenceWarmUpProvider.java new file mode 100644 index 000000000000..e3295a3aa413 --- /dev/null +++ b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/IdempotenceWarmUpProvider.java @@ -0,0 +1,54 @@ +/* + * 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.http.warmup; + +import java.util.concurrent.atomic.AtomicInteger; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; + +/** + * Test-only {@link SdkWarmUpProvider} for {@link SdkWarmUpPrimeIdempotenceTest}. Matches {@link IdempotenceSyncClient} + * only and counts sync warms; performs no I/O. + */ +public final class IdempotenceWarmUpProvider implements SdkWarmUpProvider { + + private static final AtomicInteger SYNC_WARMS = new AtomicInteger(); + + public static int syncWarmCount() { + return SYNC_WARMS.get(); + } + + @Override + public void warmUp() { + } + + @Override + public String syncClientClassName() { + return "software.amazon.awssdk.http.warmup.IdempotenceSyncClient"; + } + + @Override + public String asyncClientClassName() { + return "software.amazon.awssdk.http.warmup.IdempotenceAsyncClient"; + } + + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + SYNC_WARMS.incrementAndGet(); + } + } +} diff --git a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveAsyncClient.java b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveAsyncClient.java new file mode 100644 index 000000000000..b7937496bdac --- /dev/null +++ b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveAsyncClient.java @@ -0,0 +1,25 @@ +/* + * 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.http.warmup; + +import software.amazon.awssdk.core.SdkClient; + +/** + * Test-only client matching {@link SelectiveWarmUpProvider#asyncClientClassName()}, primed only by + * {@link SdkWarmUpPrimeSelectiveTest}. + */ +public interface SelectiveAsyncClient extends SdkClient { +} diff --git a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveSyncClient.java b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveSyncClient.java new file mode 100644 index 000000000000..e04839df47a1 --- /dev/null +++ b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveSyncClient.java @@ -0,0 +1,25 @@ +/* + * 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.http.warmup; + +import software.amazon.awssdk.core.SdkClient; + +/** + * Test-only client matching {@link SelectiveWarmUpProvider#syncClientClassName()}, primed only by + * {@link SdkWarmUpPrimeSelectiveTest}. + */ +public interface SelectiveSyncClient extends SdkClient { +} diff --git a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveWarmUpProvider.java b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveWarmUpProvider.java new file mode 100644 index 000000000000..7f213819dff8 --- /dev/null +++ b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveWarmUpProvider.java @@ -0,0 +1,61 @@ +/* + * 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.http.warmup; + +import java.util.concurrent.atomic.AtomicInteger; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; + +/** + * Test-only {@link SdkWarmUpProvider} for {@link SdkWarmUpPrimeSelectiveTest}. Matches {@link SelectiveSyncClient} and + * {@link SelectiveAsyncClient} and counts warms per transport; performs no I/O. + */ +public final class SelectiveWarmUpProvider implements SdkWarmUpProvider { + + private static final AtomicInteger SYNC_WARMS = new AtomicInteger(); + private static final AtomicInteger ASYNC_WARMS = new AtomicInteger(); + + public static int syncWarmCount() { + return SYNC_WARMS.get(); + } + + public static int asyncWarmCount() { + return ASYNC_WARMS.get(); + } + + @Override + public void warmUp() { + } + + @Override + public String syncClientClassName() { + return "software.amazon.awssdk.http.warmup.SelectiveSyncClient"; + } + + @Override + public String asyncClientClassName() { + return "software.amazon.awssdk.http.warmup.SelectiveAsyncClient"; + } + + @Override + public void warmUpClient(ClientType clientType) { + if (clientType == ClientType.SYNC) { + SYNC_WARMS.incrementAndGet(); + } else if (clientType == ClientType.ASYNC) { + ASYNC_WARMS.incrementAndGet(); + } + } +} diff --git a/test/warmup-tests/src/main/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider b/test/warmup-tests/src/main/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider new file mode 100644 index 000000000000..5222028771e5 --- /dev/null +++ b/test/warmup-tests/src/main/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider @@ -0,0 +1,2 @@ +software.amazon.awssdk.http.warmup.IdempotenceWarmUpProvider +software.amazon.awssdk.http.warmup.SelectiveWarmUpProvider diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeIdempotenceTest.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeIdempotenceTest.java new file mode 100644 index 000000000000..588a5f672ba5 --- /dev/null +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeIdempotenceTest.java @@ -0,0 +1,62 @@ +/* + * 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.http.warmup; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.crac.SdkWarmUp; + +/** + * Verifies {@link SdkWarmUp#prime(Class...)} warms a client at most once per JVM. Uses a dedicated + * {@link IdempotenceSyncClient} that no other test primes, so the "first warms, repeat no-ops" transition is observable + * regardless of execution order or JVM sharing. + */ +class SdkWarmUpPrimeIdempotenceTest { + + private String savedRegionProperty; + + @BeforeEach + void setUp() { + savedRegionProperty = System.getProperty("aws.region"); + System.setProperty("aws.region", "warmup-idempotence-test"); + } + + @AfterEach + void tearDown() { + if (savedRegionProperty != null) { + System.setProperty("aws.region", savedRegionProperty); + } else { + System.clearProperty("aws.region"); + } + } + + @Test + void prime_sameClientTwice_warmsProviderExactlyOnce() { + SdkWarmUp.prime(IdempotenceSyncClient.class); + assertThat(IdempotenceWarmUpProvider.syncWarmCount()) + .as("first prime warms the sync transport once") + .isEqualTo(1); + + // Second call for the same client must be a no-op: it is already recorded as primed. + SdkWarmUp.prime(IdempotenceSyncClient.class); + assertThat(IdempotenceWarmUpProvider.syncWarmCount()) + .as("second prime of the same client does not warm again") + .isEqualTo(1); + } +} diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeSelectiveTest.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeSelectiveTest.java new file mode 100644 index 000000000000..359532c44158 --- /dev/null +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeSelectiveTest.java @@ -0,0 +1,65 @@ +/* + * 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.http.warmup; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.crac.SdkWarmUp; + +/** + * Verifies that when {@link SdkWarmUp#prime(Class...)} is called with a batch mixing an already-primed client and a new + * one, only the new client is warmed. Uses dedicated clients that no other test primes. + */ +class SdkWarmUpPrimeSelectiveTest { + + private String savedRegionProperty; + + @BeforeEach + void setUp() { + savedRegionProperty = System.getProperty("aws.region"); + System.setProperty("aws.region", "warmup-selective-test"); + } + + @AfterEach + void tearDown() { + if (savedRegionProperty != null) { + System.setProperty("aws.region", savedRegionProperty); + } else { + System.clearProperty("aws.region"); + } + } + + @Test + void prime_previouslyPrimedClientInNewBatch_warmsOnlyTheNewClient() { + // Prime the sync client first. + SdkWarmUp.prime(SelectiveSyncClient.class); + assertThat(SelectiveWarmUpProvider.syncWarmCount()).isEqualTo(1); + assertThat(SelectiveWarmUpProvider.asyncWarmCount()).isEqualTo(0); + + // Now prime a batch containing the already-primed sync client plus a new async client. + SdkWarmUp.prime(SelectiveSyncClient.class, SelectiveAsyncClient.class); + + assertThat(SelectiveWarmUpProvider.syncWarmCount()) + .as("already-primed sync client is not warmed again") + .isEqualTo(1); + assertThat(SelectiveWarmUpProvider.asyncWarmCount()) + .as("the new async client is warmed once") + .isEqualTo(1); + } +} diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeTargetedTest.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeTargetedTest.java index ed0a04eebf43..cf37b5b1910d 100644 --- a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeTargetedTest.java +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeTargetedTest.java @@ -19,20 +19,25 @@ import static com.github.tomakehurst.wiremock.client.WireMock.any; import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.params.provider.Arguments.arguments; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.ServiceLoader; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +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; @@ -43,19 +48,7 @@ /** * End-to-end integration test of {@link SdkWarmUp#prime(Class...)} with real service clients (STS, DynamoDB) whose - * generated {@link SdkWarmUpProvider} implementations are on the classpath via ServiceLoader. Verifies that: - *

    - *
  • Passing a sync client class routes through {@code warmUpClient(SYNC)} and triggers the sync HTTP warmer.
  • - *
  • Passing an async client class routes through {@code warmUpClient(ASYNC)} and triggers the async HTTP warmer.
  • - *
  • Idempotency: a second call with the same client is a no-op.
  • - *
  • Multiple services can be primed in one call.
  • - *
  • Concurrent callers do not throw or corrupt state.
  • - *
  • An unmatched client is a safe no-op.
  • - *
- * - *

The HTTP warmers resolve their endpoint via {@code RegionEndpointResolver}, which reads {@code aws.region}. Tests - * set a dummy region so DNS fails fast and no real network traffic leaves the host. The generated providers use - * {@code CannedResponseHttpClient}, so their {@code warmUpClient} does not need the network either. + * generated {@link SdkWarmUpProvider} implementations are on the classpath via ServiceLoader. */ class SdkWarmUpPrimeTargetedTest { @@ -70,12 +63,8 @@ void setUp() { .withStatus(302) .withHeader("Location", "https://aws.amazon.com/iam"))); - // Use a non-routable region so the HTTP warmer's STS endpoint fails DNS immediately; the warm-up is best-effort - // and swallows the failure, keeping the test offline. savedRegionProperty = System.getProperty("aws.region"); System.setProperty("aws.region", "warmup-integration-test"); - - SdkWarmUp.resetTargetedPrimeStateForTesting(); } @AfterEach @@ -101,24 +90,19 @@ void serviceLoader_discoversExpectedProviders() { assertThat(count).isGreaterThanOrEqualTo(3); } - @Test - void prime_withStsSyncClient_completesWithoutError() { - assertThatCode(() -> SdkWarmUp.prime(StsClient.class)).doesNotThrowAnyException(); - } - - @Test - void prime_withStsAsyncClient_completesWithoutError() { - assertThatCode(() -> SdkWarmUp.prime(StsAsyncClient.class)).doesNotThrowAnyException(); + @ParameterizedTest(name = "{0}") + @MethodSource("singleClientCases") + void prime_withSingleClient_completesWithoutError(String description, Class client) { + assertThatCode(() -> SdkWarmUp.prime(client)).doesNotThrowAnyException(); } - @Test - void prime_withDynamoDbSyncClient_completesWithoutError() { - assertThatCode(() -> SdkWarmUp.prime(DynamoDbClient.class)).doesNotThrowAnyException(); - } - - @Test - void prime_withDynamoDbAsyncClient_completesWithoutError() { - assertThatCode(() -> SdkWarmUp.prime(DynamoDbAsyncClient.class)).doesNotThrowAnyException(); + private static Stream singleClientCases() { + return Stream.of( + arguments("STS sync", StsClient.class), + arguments("STS async", StsAsyncClient.class), + arguments("DynamoDB sync", DynamoDbClient.class), + arguments("DynamoDB async", DynamoDbAsyncClient.class) + ); } @Test @@ -144,11 +128,13 @@ void prime_calledWithNewClientAfterPrevious_primesOnlyNewClient() { } @Test - void prime_concurrentCalls_doNotThrow() throws InterruptedException { + void prime_concurrentCalls_allCompleteSuccessfully() throws InterruptedException { int threadCount = 8; CountDownLatch start = new CountDownLatch(1); CountDownLatch done = new CountDownLatch(threadCount); List threads = new ArrayList<>(); + List failures = Collections.synchronizedList(new ArrayList<>()); + AtomicInteger completed = new AtomicInteger(); Class[] clients = {StsClient.class, StsAsyncClient.class, DynamoDbClient.class, DynamoDbAsyncClient.class}; for (int i = 0; i < threadCount; i++) { @@ -158,8 +144,11 @@ void prime_concurrentCalls_doNotThrow() throws InterruptedException { try { start.await(); SdkWarmUp.prime(client); + completed.incrementAndGet(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + } catch (Throwable t) { + failures.add(t); } finally { done.countDown(); } @@ -173,7 +162,9 @@ void prime_concurrentCalls_doNotThrow() throws InterruptedException { for (Thread thread : threads) { thread.join(); } - // If we get here without exceptions or hangs, concurrency is safe. + + assertThat(failures).as("no prime(Class...) call threw").isEmpty(); + assertThat(completed.get()).as("every prime(Class...) call completed").isEqualTo(threadCount); } @Test @@ -186,16 +177,20 @@ void prime_withUnmatchedClient_isNoOpAndSendsNoRequest() { mockServer.verify(0, anyRequestedFor(anyUrl())); } - @Test - void prime_withNullArray_isNoOp() { - assertThatCode(() -> SdkWarmUp.prime((Class[]) null)).doesNotThrowAnyException(); + @ParameterizedTest(name = "{0}") + @MethodSource("noOpInputCases") + void prime_withNoOpInput_doesNotThrow(String description, Class[] clients) { + assertThatCode(() -> SdkWarmUp.prime(clients)).doesNotThrowAnyException(); } - @Test - void prime_withEmptyArray_isNoOp() { - assertThatCode(() -> SdkWarmUp.prime(new Class[0])).doesNotThrowAnyException(); + private static Stream noOpInputCases() { + return Stream.of( + arguments("null array", (Class[]) null), + arguments("empty array", new Class[0]) + ); } interface UnregisteredClient extends SdkClient { } } + From 13d3a7cd7a5a8b0c36d87dc321c0f64d28eb47b3 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 10 Jul 2026 12:57:35 -0700 Subject: [PATCH 3/4] Handled cases when one warmUp provider fails in overloaded method ,so that we can retry for next prime call --- .../amazon/awssdk/core/crac/SdkWarmUp.java | 16 +++-- .../internal/crac/TargetedWarmUpInvoker.java | 45 ++++++------- .../internal/crac/TargetedWarmUpResult.java | 45 +++++++++++++ .../awssdk/core/crac/SdkWarmUpTest.java | 21 ++++++ .../crac/TargetedWarmUpInvokerTest.java | 62 ++++++++++++++---- .../awssdk/http/warmup/RetrySyncClient.java | 24 +++++++ .../http/warmup/RetryWarmUpProvider.java | 62 ++++++++++++++++++ ....amazon.awssdk.core.crac.SdkWarmUpProvider | 1 + .../http/warmup/SdkWarmUpPrimeRetryTest.java | 64 +++++++++++++++++++ 9 files changed, 299 insertions(+), 41 deletions(-) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResult.java create mode 100644 test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/RetrySyncClient.java create mode 100644 test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/RetryWarmUpProvider.java create mode 100644 test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeRetryTest.java diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java index ca1a280cce04..ed64149e26a3 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java @@ -25,6 +25,7 @@ import software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker; import software.amazon.awssdk.core.internal.crac.PrimedClientRegistry; import software.amazon.awssdk.core.internal.crac.TargetedWarmUpInvoker; +import software.amazon.awssdk.core.internal.crac.TargetedWarmUpResult; import software.amazon.awssdk.core.internal.http.loader.AsyncHttpClientWarmer; import software.amazon.awssdk.core.internal.http.loader.ClasspathHttpWarmupInvoker; import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; @@ -87,8 +88,9 @@ public static void prime() { /** * Primes only the given service clients, warming the sync path for a sync client and the async path for an async - * client. A class matched by no provider is logged at warn and skipped. Best-effort and safe to call concurrently; - * a client already primed by a previous call is skipped. + * client. Best-effort and safe to call concurrently; a client already primed by a previous call is skipped. + * + *

A class matched by no provider, or whose warm-up fails, is logged at warn and retried by a later call. * * @param clients the service client classes to prime, for example {@code S3Client.class} or * {@code S3AsyncClient.class}. @@ -112,15 +114,15 @@ public static void prime(Class... clients) { } // Racing calls may double-warm the same client; warming is idempotent, so that is harmless. - Set matched = TargetedWarmUpInvoker.create().invoke(toPrime); - if (matched.contains(ClientType.SYNC)) { + TargetedWarmUpResult result = TargetedWarmUpInvoker.create().invoke(toPrime); + if (result.matchedTransports().contains(ClientType.SYNC)) { SyncHttpClientWarmer.create().warmAll(); } - if (matched.contains(ClientType.ASYNC)) { + if (result.matchedTransports().contains(ClientType.ASYNC)) { AsyncHttpClientWarmer.create().warmAll(); } - // Record names only after warming completes, so a run that throws is retried by a later call. - PRIMED_CLIENTS.markPrimed(toPrime); + // Only successfully warmed names are recorded; unmatched or failed ones are retried on a later call. + PRIMED_CLIENTS.markPrimed(result.warmedClientNames()); } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java index b36614657d91..e5eeea7f4e88 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -51,40 +52,40 @@ public static TargetedWarmUpInvoker create() { * at warn and skipped, and a provider that fails does not stop the others. * * @param requestedClassNames the fully qualified sync or async client class names to warm. - * @return the transports that were matched, for narrowing the follow-on HTTP-client warm-up. + * @return the matched transports and the names that warmed successfully. */ - public Set invoke(Collection requestedClassNames) { + public TargetedWarmUpResult invoke(Collection requestedClassNames) { Set matchedTransports = EnumSet.noneOf(ClientType.class); + Set warmedClientNames = new LinkedHashSet<>(); if (requestedClassNames.isEmpty()) { - return matchedTransports; + return new TargetedWarmUpResult(matchedTransports, warmedClientNames); } List providers = loadProviders(); for (String requested : requestedClassNames) { - Set matched = warmMatching(requested, providers); + boolean warmFailed = false; + Set matched = EnumSet.noneOf(ClientType.class); + for (SdkWarmUpProvider provider : providers) { + ClientType transport = transportFor(requested, provider); + if (transport == null) { + continue; + } + matched.add(transport); + try { + provider.warmUpClient(transport); + } catch (RuntimeException | LinkageError e) { + warmFailed = true; + log.warn(() -> "Warm-up failed for " + provider.getClass().getName() + " and was skipped.", e); + } + } if (matched.isEmpty()) { log.warn(() -> "No warm-up provider matched client class " + requested + "; skipping."); + } else if (!warmFailed) { + warmedClientNames.add(requested); } matchedTransports.addAll(matched); } - return matchedTransports; - } - - private Set warmMatching(String requested, List providers) { - Set matched = EnumSet.noneOf(ClientType.class); - for (SdkWarmUpProvider provider : providers) { - ClientType transport = transportFor(requested, provider); - if (transport == null) { - continue; - } - matched.add(transport); - try { - provider.warmUpClient(transport); - } catch (RuntimeException | LinkageError e) { - log.warn(() -> "Warm-up failed for " + provider.getClass().getName() + " and was skipped.", e); - } - } - return matched; + return new TargetedWarmUpResult(matchedTransports, warmedClientNames); } private ClientType transportFor(String requested, SdkWarmUpProvider provider) { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResult.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResult.java new file mode 100644 index 000000000000..96d07936b843 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResult.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.core.internal.crac; + +import java.util.Collections; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.ClientType; + +/** + * Outcome of a {@link TargetedWarmUpInvoker} run: the transports that matched and the client names that warmed + * successfully. + */ +@SdkInternalApi +public final class TargetedWarmUpResult { + + private final Set matchedTransports; + private final Set warmedClientNames; + + TargetedWarmUpResult(Set matchedTransports, Set warmedClientNames) { + this.matchedTransports = Collections.unmodifiableSet(matchedTransports); + this.warmedClientNames = Collections.unmodifiableSet(warmedClientNames); + } + + public Set matchedTransports() { + return matchedTransports; + } + + public Set warmedClientNames() { + return warmedClientNames; + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java index 493c77011d1e..95193363caf4 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java @@ -21,11 +21,13 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; +import org.apache.logging.log4j.Level; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.SdkClient; +import software.amazon.awssdk.testutils.LogCaptor; /** * Tests the static {@link SdkWarmUp#prime()} entry point end to end through {@link java.util.ServiceLoader}, @@ -107,6 +109,22 @@ void prime_withUnmatchedClient_doesNotThrow() { assertThatCode(() -> SdkWarmUp.prime(UnmatchedClient.class)).doesNotThrowAnyException(); } + @Test + void prime_withUnmatchedClient_warnsOnEveryCallAndIsRetried() { + // An unmatched client is not recorded as primed, so every call warns and retries. + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + SdkWarmUp.prime(RetriedUnmatchedClient.class); + SdkWarmUp.prime(RetriedUnmatchedClient.class); + + long unmatchedWarns = logCaptor.loggedEvents().stream() + .filter(event -> event.getLevel() == Level.WARN + && event.getMessage().getFormattedMessage() + .contains(RetriedUnmatchedClient.class.getName())) + .count(); + assertThat(unmatchedWarns).isEqualTo(2); + } + } + @Test void prime_withEmptyArray_isNoOp() { assertThatCode(() -> SdkWarmUp.prime(new Class[0])).doesNotThrowAnyException(); @@ -114,4 +132,7 @@ void prime_withEmptyArray_isNoOp() { interface UnmatchedClient extends SdkClient { } + + interface RetriedUnmatchedClient extends SdkClient { + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java index 9ebd63739f8a..8c50151a6b3e 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java @@ -38,30 +38,34 @@ class TargetedWarmUpInvokerTest { void invoke_syncClassRequested_warmsSyncTransportOnly() { RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); - Set matched = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_SYNC)); + TargetedWarmUpResult result = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_SYNC)); assertThat(service1.warmedTypes()).containsExactly(ClientType.SYNC); - assertThat(matched).containsExactly(ClientType.SYNC); + assertThat(result.matchedTransports()).containsExactly(ClientType.SYNC); + assertThat(result.warmedClientNames()).containsExactly(SERVICE1_SYNC); } @Test void invoke_asyncClassRequested_warmsAsyncTransportOnly() { RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); - Set matched = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_ASYNC)); + TargetedWarmUpResult result = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_ASYNC)); assertThat(service1.warmedTypes()).containsExactly(ClientType.ASYNC); - assertThat(matched).containsExactly(ClientType.ASYNC); + assertThat(result.matchedTransports()).containsExactly(ClientType.ASYNC); + assertThat(result.warmedClientNames()).containsExactly(SERVICE1_ASYNC); } @Test void invoke_bothClassesRequested_warmsBothTransports() { RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); - Set matched = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_SYNC, SERVICE1_ASYNC)); + TargetedWarmUpResult result = + invokerLoading(service1).invoke(Arrays.asList(SERVICE1_SYNC, SERVICE1_ASYNC)); assertThat(service1.warmedTypes()).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); - assertThat(matched).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); + assertThat(result.matchedTransports()).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); + assertThat(result.warmedClientNames()).containsExactly(SERVICE1_SYNC, SERVICE1_ASYNC); } @Test @@ -80,9 +84,11 @@ void invoke_unmatchedClass_logsWarnAndDoesNotThrow() { RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { - Set matched = invokerLoading(service1).invoke(Arrays.asList("com.example.NotAClient")); + TargetedWarmUpResult result = + invokerLoading(service1).invoke(Arrays.asList("com.example.NotAClient")); - assertThat(matched).isEmpty(); + assertThat(result.matchedTransports()).isEmpty(); + assertThat(result.warmedClientNames()).isEmpty(); assertThat(service1.warmedTypes()).isEmpty(); assertThat(logCaptor.loggedEvents()) .anyMatch(event -> event.getLevel() == Level.WARN @@ -101,18 +107,50 @@ public void warmUpClient(ClientType clientType) { // A throwing warmUpClient must not stop the follow-on HTTP warm-up: the matched transport is still returned so // the caller can narrow the HTTP warmers. - Set matched = invokerLoading(throwing).invoke(Arrays.asList(SERVICE1_SYNC)); + TargetedWarmUpResult result = invokerLoading(throwing).invoke(Arrays.asList(SERVICE1_SYNC)); - assertThat(matched).containsExactly(ClientType.SYNC); + assertThat(result.matchedTransports()).containsExactly(ClientType.SYNC); + } + + @Test + void invoke_whenProviderWarmUpThrows_doesNotReportClientWarmed() { + SdkWarmUpProvider throwing = new TestProvider(SERVICE1_SYNC, SERVICE1_ASYNC) { + @Override + public void warmUpClient(ClientType clientType) { + throw new RuntimeException("boom"); + } + }; + + TargetedWarmUpResult result = invokerLoading(throwing).invoke(Arrays.asList(SERVICE1_SYNC)); + + assertThat(result.warmedClientNames()).isEmpty(); + } + + @Test + void invoke_mixedSuccessAndFailure_reportsOnlySuccessfulClientWarmed() { + RecordingProvider healthy = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); + SdkWarmUpProvider throwing = new TestProvider(SERVICE2_SYNC, null) { + @Override + public void warmUpClient(ClientType clientType) { + throw new RuntimeException("boom"); + } + }; + + TargetedWarmUpResult result = + invokerLoading(healthy, throwing).invoke(Arrays.asList(SERVICE1_SYNC, SERVICE2_SYNC)); + + assertThat(result.warmedClientNames()).containsExactly(SERVICE1_SYNC); + assertThat(result.matchedTransports()).containsExactly(ClientType.SYNC); } @Test void invoke_emptyRequest_isNoOp() { RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); - Set matched = invokerLoading(service1).invoke(Collections.emptyList()); + TargetedWarmUpResult result = invokerLoading(service1).invoke(Collections.emptyList()); - assertThat(matched).isEmpty(); + assertThat(result.matchedTransports()).isEmpty(); + assertThat(result.warmedClientNames()).isEmpty(); assertThat(service1.warmedTypes()).isEmpty(); } diff --git a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/RetrySyncClient.java b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/RetrySyncClient.java new file mode 100644 index 000000000000..13f8cea0e151 --- /dev/null +++ b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/RetrySyncClient.java @@ -0,0 +1,24 @@ +/* + * 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.http.warmup; + +import software.amazon.awssdk.core.SdkClient; + +/** + * Test-only client matching {@link RetryWarmUpProvider#syncClientClassName()}. + */ +public interface RetrySyncClient extends SdkClient { +} diff --git a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/RetryWarmUpProvider.java b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/RetryWarmUpProvider.java new file mode 100644 index 000000000000..0793a8df35b1 --- /dev/null +++ b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/RetryWarmUpProvider.java @@ -0,0 +1,62 @@ +/* + * 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.http.warmup; + +import java.util.concurrent.atomic.AtomicInteger; +import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; + +/** + * Test-only provider whose first {@code warmUpClient(SYNC)} call throws; later calls succeed. + */ +public final class RetryWarmUpProvider implements SdkWarmUpProvider { + + private static final AtomicInteger SYNC_ATTEMPTS = new AtomicInteger(); + private static final AtomicInteger SYNC_SUCCESSES = new AtomicInteger(); + + public static int syncAttemptCount() { + return SYNC_ATTEMPTS.get(); + } + + public static int syncSuccessCount() { + return SYNC_SUCCESSES.get(); + } + + @Override + public void warmUp() { + } + + @Override + public String syncClientClassName() { + return "software.amazon.awssdk.http.warmup.RetrySyncClient"; + } + + @Override + public String asyncClientClassName() { + return null; + } + + @Override + public void warmUpClient(ClientType clientType) { + if (clientType != ClientType.SYNC) { + return; + } + if (SYNC_ATTEMPTS.incrementAndGet() == 1) { + throw new RuntimeException("Simulated transient warm-up failure (test)"); + } + SYNC_SUCCESSES.incrementAndGet(); + } +} diff --git a/test/warmup-tests/src/main/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider b/test/warmup-tests/src/main/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider index 5222028771e5..3e6b812379bc 100644 --- a/test/warmup-tests/src/main/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider +++ b/test/warmup-tests/src/main/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider @@ -1,2 +1,3 @@ software.amazon.awssdk.http.warmup.IdempotenceWarmUpProvider software.amazon.awssdk.http.warmup.SelectiveWarmUpProvider +software.amazon.awssdk.http.warmup.RetryWarmUpProvider diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeRetryTest.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeRetryTest.java new file mode 100644 index 000000000000..93f0423b860f --- /dev/null +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeRetryTest.java @@ -0,0 +1,64 @@ +/* + * 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.http.warmup; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.crac.SdkWarmUp; + +/** + * Verifies a failed {@link SdkWarmUp#prime(Class...)} warm-up is retried on the next call. + */ +class SdkWarmUpPrimeRetryTest { + + private String savedRegionProperty; + + @BeforeEach + void setUp() { + savedRegionProperty = System.getProperty("aws.region"); + System.setProperty("aws.region", "warmup-retry-test"); + } + + @AfterEach + void tearDown() { + if (savedRegionProperty != null) { + System.setProperty("aws.region", savedRegionProperty); + } else { + System.clearProperty("aws.region"); + } + } + + @Test + void prime_afterWarmUpFailure_retriesOnNextCallThenStops() { + // First call: the provider throws, so the client is not recorded as primed. + assertThatCode(() -> SdkWarmUp.prime(RetrySyncClient.class)).doesNotThrowAnyException(); + assertThat(RetryWarmUpProvider.syncAttemptCount()).as("first call attempts the warm").isEqualTo(1); + assertThat(RetryWarmUpProvider.syncSuccessCount()).as("first call fails").isEqualTo(0); + + // Second call: retried; this time it succeeds. + SdkWarmUp.prime(RetrySyncClient.class); + assertThat(RetryWarmUpProvider.syncAttemptCount()).as("failure is retried").isEqualTo(2); + assertThat(RetryWarmUpProvider.syncSuccessCount()).as("retry succeeds").isEqualTo(1); + + // Third call: already primed, no further attempt. + SdkWarmUp.prime(RetrySyncClient.class); + assertThat(RetryWarmUpProvider.syncAttemptCount()).as("successful warm is not repeated").isEqualTo(2); + } +} From 34259a2a686ade4f78b461c7e89f7c517a9c98a0 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Mon, 13 Jul 2026 08:49:22 -0700 Subject: [PATCH 4/4] Handle Review comments --- .../amazon/awssdk/core/crac/SdkWarmUp.java | 17 ++++--- .../internal/crac/TargetedWarmUpInvoker.java | 24 +++++----- .../internal/crac/TargetedWarmUpResult.java | 45 ++++++++++++++++--- .../core/crac/RegisteredSyncClient.java | 2 +- .../core/crac/RegisteredWarmUpProvider.java | 2 +- .../awssdk/core/crac/SdkWarmUpTest.java | 4 +- .../crac/TargetedWarmUpInvokerTest.java | 24 +++++----- .../crac/TargetedWarmUpResultTest.java | 29 ++++++++++++ .../http/warmup/SelectiveWarmUpProvider.java | 2 +- .../warmup/SdkWarmUpPrimeIdempotenceTest.java | 2 +- 10 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResultTest.java diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java index ed64149e26a3..00e83cd19e8d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java @@ -70,6 +70,9 @@ private SdkWarmUp() { * Discovers every {@link SdkWarmUpProvider} on the classpath and invokes {@link SdkWarmUpProvider#warmUp()} * on each, honoring the idempotency, per-provider resilience, and empty-classpath behavior described on * this class. Safe to call concurrently. + * + *

This method and {@link #prime(Class[])} track primed state independently: this method warms + * every provider and does not skip clients that were warmed by a targeted {@link #prime(Class[])} call. */ public static void prime() { if (primed) { @@ -88,12 +91,14 @@ public static void prime() { /** * Primes only the given service clients, warming the sync path for a sync client and the async path for an async - * client. Best-effort and safe to call concurrently; a client already primed by a previous call is skipped. + * client. A client already primed by this method is skipped; a class matched by no provider, or whose warm-up + * fails, is logged at warn and retried on the next call.Best-effort and safe to call concurrently. * - *

A class matched by no provider, or whose warm-up fails, is logged at warn and retried by a later call. + *

This method and {@link #prime()} track primed state independently: this method does not skip + * clients that {@link #prime()} already warmed. * - * @param clients the service client classes to prime, for example {@code S3Client.class} or - * {@code S3AsyncClient.class}. + * @param clients the service client classes to prime, for example {@code ServiceClient.class} or + * {@code ServiceAsyncClient.class}. */ @SafeVarargs public static void prime(Class... clients) { @@ -115,10 +120,10 @@ public static void prime(Class... clients) { // Racing calls may double-warm the same client; warming is idempotent, so that is harmless. TargetedWarmUpResult result = TargetedWarmUpInvoker.create().invoke(toPrime); - if (result.matchedTransports().contains(ClientType.SYNC)) { + if (result.matchedClientTypes().contains(ClientType.SYNC)) { SyncHttpClientWarmer.create().warmAll(); } - if (result.matchedTransports().contains(ClientType.ASYNC)) { + if (result.matchedClientTypes().contains(ClientType.ASYNC)) { AsyncHttpClientWarmer.create().warmAll(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java index e5eeea7f4e88..d45cef6514b3 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvoker.java @@ -29,7 +29,7 @@ /** * Warms only the service clients named by {@code SdkWarmUp.prime(Class...)}. Matches each requested client class name - * against the discovered {@link SdkWarmUpProvider}s' sync and async class names, then warms the matched transport only. + * against the discovered {@link SdkWarmUpProvider}s' sync and async class names, then warms the matched client type only. */ @SdkInternalApi public final class TargetedWarmUpInvoker { @@ -48,17 +48,17 @@ public static TargetedWarmUpInvoker create() { } /** - * Warms the provider transport matching each requested client class name. Best-effort: an unmatched name is logged + * Warms the provider client type matching each requested client class name. An unmatched name is logged * at warn and skipped, and a provider that fails does not stop the others. * * @param requestedClassNames the fully qualified sync or async client class names to warm. - * @return the matched transports and the names that warmed successfully. + * @return the matched client types and the names that warmed successfully. */ public TargetedWarmUpResult invoke(Collection requestedClassNames) { - Set matchedTransports = EnumSet.noneOf(ClientType.class); + Set matchedClientTypes = EnumSet.noneOf(ClientType.class); Set warmedClientNames = new LinkedHashSet<>(); if (requestedClassNames.isEmpty()) { - return new TargetedWarmUpResult(matchedTransports, warmedClientNames); + return new TargetedWarmUpResult(matchedClientTypes, warmedClientNames); } List providers = loadProviders(); @@ -66,13 +66,13 @@ public TargetedWarmUpResult invoke(Collection requestedClassNames) { boolean warmFailed = false; Set matched = EnumSet.noneOf(ClientType.class); for (SdkWarmUpProvider provider : providers) { - ClientType transport = transportFor(requested, provider); - if (transport == null) { + ClientType clientType = clientTypeFor(requested, provider); + if (clientType == null) { continue; } - matched.add(transport); + matched.add(clientType); try { - provider.warmUpClient(transport); + provider.warmUpClient(clientType); } catch (RuntimeException | LinkageError e) { warmFailed = true; log.warn(() -> "Warm-up failed for " + provider.getClass().getName() + " and was skipped.", e); @@ -83,12 +83,12 @@ public TargetedWarmUpResult invoke(Collection requestedClassNames) { } else if (!warmFailed) { warmedClientNames.add(requested); } - matchedTransports.addAll(matched); + matchedClientTypes.addAll(matched); } - return new TargetedWarmUpResult(matchedTransports, warmedClientNames); + return new TargetedWarmUpResult(matchedClientTypes, warmedClientNames); } - private ClientType transportFor(String requested, SdkWarmUpProvider provider) { + private ClientType clientTypeFor(String requested, SdkWarmUpProvider provider) { if (requested.equals(provider.syncClientClassName())) { return ClientType.SYNC; } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResult.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResult.java index 96d07936b843..1ca19c0e1712 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResult.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResult.java @@ -19,27 +19,60 @@ import java.util.Set; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.utils.ToString; /** - * Outcome of a {@link TargetedWarmUpInvoker} run: the transports that matched and the client names that warmed + * Outcome of a {@link TargetedWarmUpInvoker} run: the client types that matched and the client names that warmed * successfully. */ @SdkInternalApi public final class TargetedWarmUpResult { - private final Set matchedTransports; + private final Set matchedClientTypes; private final Set warmedClientNames; - TargetedWarmUpResult(Set matchedTransports, Set warmedClientNames) { - this.matchedTransports = Collections.unmodifiableSet(matchedTransports); + TargetedWarmUpResult(Set matchedClientTypes, Set warmedClientNames) { + this.matchedClientTypes = Collections.unmodifiableSet(matchedClientTypes); this.warmedClientNames = Collections.unmodifiableSet(warmedClientNames); } - public Set matchedTransports() { - return matchedTransports; + public Set matchedClientTypes() { + return matchedClientTypes; } public Set warmedClientNames() { return warmedClientNames; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TargetedWarmUpResult that = (TargetedWarmUpResult) o; + + if (!matchedClientTypes.equals(that.matchedClientTypes)) { + return false; + } + return warmedClientNames.equals(that.warmedClientNames); + } + + @Override + public int hashCode() { + int result = matchedClientTypes.hashCode(); + result = 31 * result + warmedClientNames.hashCode(); + return result; + } + + @Override + public String toString() { + return ToString.builder("TargetedWarmUpResult") + .add("matchedClientTypes", matchedClientTypes) + .add("warmedClientNames", warmedClientNames) + .build(); + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredSyncClient.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredSyncClient.java index 39ea86c34876..f84155ecf9ec 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredSyncClient.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredSyncClient.java @@ -19,7 +19,7 @@ /** * Test-only client whose fully qualified name matches {@link RegisteredWarmUpProvider#syncClientClassName()}, so - * {@code SdkWarmUp.prime(RegisteredSyncClient.class)} matches that provider's sync transport. + * {@code SdkWarmUp.prime(RegisteredSyncClient.class)} matches that provider's sync client type. */ public interface RegisteredSyncClient extends SdkClient { } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java index 270d3e91af85..c3e94968556f 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java @@ -26,7 +26,7 @@ * java.util.ServiceLoader} discovers and instantiates it by name. Both static fields are static because the loader * builds its own instance. Must be public with a no-arg constructor for ServiceLoader. * - *

{@code INVOCATIONS} counts full {@link #warmUp()} calls; {@code WARMED_CLIENTS} records the transports passed to + *

{@code INVOCATIONS} counts full {@link #warmUp()} calls; {@code WARMED_CLIENTS} records the client types passed to * {@link #warmUpClient(ClientType)}, so a test can tell the targeted path from the full one. */ public final class RegisteredWarmUpProvider implements SdkWarmUpProvider { diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java index 95193363caf4..53bc6880ed6c 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java @@ -88,7 +88,7 @@ void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws Interrup } @Test - void prime_withMatchingSyncClient_warmsSyncTransportThroughWarmUpClient() { + void prime_withMatchingSyncClient_warmsSyncClientTypeThroughWarmUpClient() { SdkWarmUp.prime(RegisteredSyncClient.class); // Targeted prime warms via warmUpClient() only; the full warmUp() path (counted by INVOCATIONS) must not run. @@ -97,7 +97,7 @@ void prime_withMatchingSyncClient_warmsSyncTransportThroughWarmUpClient() { } @Test - void prime_withMatchingAsyncClient_warmsAsyncTransportThroughWarmUpClient() { + void prime_withMatchingAsyncClient_warmsAsyncClientTypeThroughWarmUpClient() { SdkWarmUp.prime(RegisteredAsyncClient.class); assertThat(RegisteredWarmUpProvider.INVOCATIONS.get()).isEqualTo(0); diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java index 8c50151a6b3e..bcffe3b1d571 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpInvokerTest.java @@ -35,36 +35,36 @@ class TargetedWarmUpInvokerTest { private static final String SERVICE2_SYNC = "software.amazon.awssdk.services.service2.Service2Client"; @Test - void invoke_syncClassRequested_warmsSyncTransportOnly() { + void invoke_syncClassRequested_warmsSyncClientTypeOnly() { RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); TargetedWarmUpResult result = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_SYNC)); assertThat(service1.warmedTypes()).containsExactly(ClientType.SYNC); - assertThat(result.matchedTransports()).containsExactly(ClientType.SYNC); + assertThat(result.matchedClientTypes()).containsExactly(ClientType.SYNC); assertThat(result.warmedClientNames()).containsExactly(SERVICE1_SYNC); } @Test - void invoke_asyncClassRequested_warmsAsyncTransportOnly() { + void invoke_asyncClassRequested_warmsAsyncClientTypeOnly() { RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); TargetedWarmUpResult result = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_ASYNC)); assertThat(service1.warmedTypes()).containsExactly(ClientType.ASYNC); - assertThat(result.matchedTransports()).containsExactly(ClientType.ASYNC); + assertThat(result.matchedClientTypes()).containsExactly(ClientType.ASYNC); assertThat(result.warmedClientNames()).containsExactly(SERVICE1_ASYNC); } @Test - void invoke_bothClassesRequested_warmsBothTransports() { + void invoke_bothClassesRequested_warmsBothClientTypes() { RecordingProvider service1 = new RecordingProvider(SERVICE1_SYNC, SERVICE1_ASYNC); TargetedWarmUpResult result = invokerLoading(service1).invoke(Arrays.asList(SERVICE1_SYNC, SERVICE1_ASYNC)); assertThat(service1.warmedTypes()).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); - assertThat(result.matchedTransports()).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); + assertThat(result.matchedClientTypes()).containsExactlyInAnyOrder(ClientType.SYNC, ClientType.ASYNC); assertThat(result.warmedClientNames()).containsExactly(SERVICE1_SYNC, SERVICE1_ASYNC); } @@ -87,7 +87,7 @@ void invoke_unmatchedClass_logsWarnAndDoesNotThrow() { TargetedWarmUpResult result = invokerLoading(service1).invoke(Arrays.asList("com.example.NotAClient")); - assertThat(result.matchedTransports()).isEmpty(); + assertThat(result.matchedClientTypes()).isEmpty(); assertThat(result.warmedClientNames()).isEmpty(); assertThat(service1.warmedTypes()).isEmpty(); assertThat(logCaptor.loggedEvents()) @@ -97,7 +97,7 @@ void invoke_unmatchedClass_logsWarnAndDoesNotThrow() { } @Test - void invoke_whenProviderWarmUpThrows_stillReturnsMatchedTransport() { + void invoke_whenProviderWarmUpThrows_stillReturnsMatchedClientType() { SdkWarmUpProvider throwing = new TestProvider(SERVICE1_SYNC, SERVICE1_ASYNC) { @Override public void warmUpClient(ClientType clientType) { @@ -105,11 +105,11 @@ public void warmUpClient(ClientType clientType) { } }; - // A throwing warmUpClient must not stop the follow-on HTTP warm-up: the matched transport is still returned so + // A throwing warmUpClient must not stop the follow-on HTTP warm-up: the matched client type is still returned so // the caller can narrow the HTTP warmers. TargetedWarmUpResult result = invokerLoading(throwing).invoke(Arrays.asList(SERVICE1_SYNC)); - assertThat(result.matchedTransports()).containsExactly(ClientType.SYNC); + assertThat(result.matchedClientTypes()).containsExactly(ClientType.SYNC); } @Test @@ -140,7 +140,7 @@ public void warmUpClient(ClientType clientType) { invokerLoading(healthy, throwing).invoke(Arrays.asList(SERVICE1_SYNC, SERVICE2_SYNC)); assertThat(result.warmedClientNames()).containsExactly(SERVICE1_SYNC); - assertThat(result.matchedTransports()).containsExactly(ClientType.SYNC); + assertThat(result.matchedClientTypes()).containsExactly(ClientType.SYNC); } @Test @@ -149,7 +149,7 @@ void invoke_emptyRequest_isNoOp() { TargetedWarmUpResult result = invokerLoading(service1).invoke(Collections.emptyList()); - assertThat(result.matchedTransports()).isEmpty(); + assertThat(result.matchedClientTypes()).isEmpty(); assertThat(result.warmedClientNames()).isEmpty(); assertThat(service1.warmedTypes()).isEmpty(); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResultTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResultTest.java new file mode 100644 index 000000000000..9e88b276b689 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/TargetedWarmUpResultTest.java @@ -0,0 +1,29 @@ +/* + * 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.core.internal.crac; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; + +public class TargetedWarmUpResultTest { + + @Test + public void equalsHashCode() { + EqualsVerifier.forClass(TargetedWarmUpResult.class) + .withNonnullFields("matchedClientTypes", "warmedClientNames") + .verify(); + } +} diff --git a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveWarmUpProvider.java b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveWarmUpProvider.java index 7f213819dff8..21b038807fcd 100644 --- a/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveWarmUpProvider.java +++ b/test/warmup-tests/src/main/java/software/amazon/awssdk/http/warmup/SelectiveWarmUpProvider.java @@ -21,7 +21,7 @@ /** * Test-only {@link SdkWarmUpProvider} for {@link SdkWarmUpPrimeSelectiveTest}. Matches {@link SelectiveSyncClient} and - * {@link SelectiveAsyncClient} and counts warms per transport; performs no I/O. + * {@link SelectiveAsyncClient} and counts warms per client type; performs no I/O. */ public final class SelectiveWarmUpProvider implements SdkWarmUpProvider { diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeIdempotenceTest.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeIdempotenceTest.java index 588a5f672ba5..2fa65bf2187b 100644 --- a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeIdempotenceTest.java +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SdkWarmUpPrimeIdempotenceTest.java @@ -50,7 +50,7 @@ void tearDown() { void prime_sameClientTwice_warmsProviderExactlyOnce() { SdkWarmUp.prime(IdempotenceSyncClient.class); assertThat(IdempotenceWarmUpProvider.syncWarmCount()) - .as("first prime warms the sync transport once") + .as("first prime warms the sync client type once") .isEqualTo(1); // Second call for the same client must be a no-op: it is already recorded as primed.