From adaf1a0a8fbb1eb6425f4f8843b4f2f88c3cefa5 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Thu, 25 Jun 2026 13:21:15 -0700 Subject: [PATCH 1/7] feat(sdk-core): add sync HTTP-client warm-up to SdkWarmUp.prime() for CRaC priming --- core/sdk-core/pom.xml | 6 + .../amazon/awssdk/core/crac/SdkWarmUp.java | 27 ++- .../internal/crac/ClasspathWarmUpInvoker.java | 30 +-- .../internal/crac/RegionEndpointResolver.java | 89 +++++++++ .../core/internal/crac/WarmUpDiscovery.java | 66 +++++++ .../core/internal/crac/WarmUpRequest.java | 73 +++++++ .../http/loader/HttpClientWarmer.java | 31 +++ .../http/loader/SyncHttpClientWarmer.java | 106 +++++++++++ .../awssdk/core/crac/SdkWarmUpTest.java | 23 +++ .../crac/ClasspathWarmUpInvokerTest.java | 6 +- .../crac/RegionEndpointResolverTest.java | 140 ++++++++++++++ .../SyncHttpClientWarmerIntegrationTest.java | 132 +++++++++++++ .../http/loader/SyncHttpClientWarmerTest.java | 180 ++++++++++++++++++ .../http/crt/AwsCrtHttpClientWarmUpTest.java | 52 +++++ .../UrlConnectionHttpClientWarmUpTest.java | 52 +++++ .../CodingConventionWithSuppressionTest.java | 2 +- 16 files changed, 974 insertions(+), 41 deletions(-) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpDiscovery.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpClientWarmer.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java create mode 100644 http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWarmUpTest.java create mode 100644 http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientWarmUpTest.java diff --git a/core/sdk-core/pom.xml b/core/sdk-core/pom.xml index 88682ba51c93..af5df62ca03b 100644 --- a/core/sdk-core/pom.xml +++ b/core/sdk-core/pom.xml @@ -112,6 +112,12 @@ ${awsjavasdk.version} test + + software.amazon.awssdk + apache5-client + ${awsjavasdk.version} + test + org.junit.jupiter junit-jupiter 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 3ab09cfcf437..73861377ae8e 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,10 +15,14 @@ package software.amazon.awssdk.core.crac; +import java.util.Arrays; +import java.util.List; import java.util.ServiceLoader; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker; +import software.amazon.awssdk.core.internal.http.loader.HttpClientWarmer; +import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; /** * Entry point for warming up SDK service request paths before a Coordinated Restore at Checkpoint (CRaC) @@ -30,15 +34,16 @@ * *

Behavior contract: *

* - *

Call this once during application initialization, before a CRaC checkpoint is taken. + *

{@code prime()} also fires a best-effort {@code GET} per sync HTTP client at a regional AWS endpoint to JIT-compile the + * HTTP, DNS, TLS, and cert-chain paths. This needs network connectivity during init; if unavailable, the failure is swallowed. + * + *

Call this once during initialization, before a CRaC checkpoint is taken. */ @ThreadSafe @SdkPublicApi @@ -46,6 +51,9 @@ public final class SdkWarmUp { private static final Object PRIME_LOCK = new Object(); + // The HTTP-client warmers invoked by prime(), one per transport kind. The async warmer is added here when implemented. + private static final List HTTP_CLIENT_WARMERS = Arrays.asList(SyncHttpClientWarmer.create()); + private static volatile boolean primed = false; private SdkWarmUp() { @@ -64,8 +72,11 @@ public static void prime() { if (primed) { return; } - // Set primed only after invokeAll() succeeds, so a failed run leaves primed false and a later call retries. + // Set primed only after warm-up succeeds, so a failed run leaves primed false and a later call retries. ClasspathWarmUpInvoker.create().invokeAll(); + for (HttpClientWarmer warmer : HTTP_CLIENT_WARMERS) { + warmer.warmAll(); + } primed = true; } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvoker.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvoker.java index 4624b4fdeb14..9f215505b8d9 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvoker.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvoker.java @@ -15,13 +15,10 @@ package software.amazon.awssdk.core.internal.crac; -import java.util.Iterator; -import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.core.crac.SdkWarmUpProvider; -import software.amazon.awssdk.utils.Logger; /** * {@link WarmUpInvoker} implementation that uses {@link ServiceLoader} to find {@link SdkWarmUpProvider} @@ -30,8 +27,6 @@ @SdkInternalApi public final class ClasspathWarmUpInvoker implements WarmUpInvoker { - private static final Logger log = Logger.loggerFor(ClasspathWarmUpInvoker.class); - private final WarmUpServiceLoader serviceLoader; @SdkTestInternalApi @@ -41,30 +36,7 @@ public final class ClasspathWarmUpInvoker implements WarmUpInvoker { @Override public void invokeAll() { - Iterator iterator = serviceLoader.loadProviders(); - boolean invokedAny = false; - - while (iterator.hasNext()) { - SdkWarmUpProvider provider; - try { - provider = iterator.next(); - } catch (ServiceConfigurationError e) { - // next() has already advanced past the bad provider, so it is safe to continue to the next one. - log.warn(() -> "Skipping an SdkWarmUpProvider that could not be loaded.", e); - continue; - } - - invokedAny = true; - try { - provider.warmUp(); - } catch (RuntimeException e) { - log.warn(() -> "An SdkWarmUpProvider failed during warmUp() and was skipped.", e); - } - } - - if (!invokedAny) { - log.debug(() -> "No SdkWarmUpProvider implementations were discovered on the classpath."); - } + WarmUpDiscovery.forEachDiscovered(serviceLoader.loadProviders(), SdkWarmUpProvider::warmUp); } /** diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java new file mode 100644 index 000000000000..deb8f406c90c --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java @@ -0,0 +1,89 @@ +/* + * 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.net.URI; +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.SystemSetting; + +/** + * Resolves the regional STS endpoint that the CRaC HTTP-client warm-up sends its GET to. + * + *

Region precedence: {@code aws.region} system property, {@code AWS_REGION} env, {@code AWS_DEFAULT_REGION} env, then + * {@value #DEFAULT_REGION}. Tiers are resolved independently so a blank value falls through; the combined + * {@code SdkSystemSetting.AWS_REGION.getStringValue()} cannot be used because a blank {@code aws.region} would shadow the env + * vars. + * + *

Reads only system properties and env vars: no IMDS, profile-file, or credential-provider lookups, which would slow + * priming. The host is partition-naive ({@code sts..amazonaws.com}); the warm-up only JIT-compiles DNS/TLS/cert-chain + * against any reachable AWS host and is best-effort, so a wrong host in cn/gov/iso partitions fails and is swallowed. + */ +@SdkInternalApi +public final class RegionEndpointResolver { + + static final String DEFAULT_REGION = "us-east-1"; + + private static final String AWS_REGION_PROPERTY = "aws.region"; + private static final String AWS_REGION_ENV_VAR = "AWS_REGION"; + private static final String AWS_DEFAULT_REGION_ENV_VAR = "AWS_DEFAULT_REGION"; + + // Property-only SystemSetting (null env var) so aws.region is read as a tier distinct from the AWS_REGION env var, + // without a direct System.getProperty call (Checkstyle-banned outside the system-setting utilities). + private static final SystemSetting AWS_REGION_PROPERTY_SETTING = new SystemSetting() { + @Override + public String property() { + return AWS_REGION_PROPERTY; + } + + @Override + public String environmentVariable() { + return null; + } + + @Override + public String defaultValue() { + return null; + } + }; + + private RegionEndpointResolver() { + } + + public static RegionEndpointResolver create() { + return new RegionEndpointResolver(); + } + + /** + * @return the regional STS endpoint URI for the resolved region; never null. + */ + public URI stsEndpoint() { + return URI.create("https://sts." + resolveRegion() + ".amazonaws.com/"); + } + + private String resolveRegion() { + return trimmed(AWS_REGION_PROPERTY_SETTING.getStringValue()) + .orElseGet(() -> trimmed(SystemSetting.getStringValueFromEnvironmentVariable(AWS_REGION_ENV_VAR)) + .orElseGet(() -> trimmed(SystemSetting.getStringValueFromEnvironmentVariable(AWS_DEFAULT_REGION_ENV_VAR)) + .orElse(DEFAULT_REGION))); + } + + private static Optional trimmed(Optional value) { + // trimToNull returns null for blank/empty input, so Optional.map collapses those to an empty Optional. + return value.map(StringUtils::trimToNull); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpDiscovery.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpDiscovery.java new file mode 100644 index 000000000000..f97a13dcd613 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpDiscovery.java @@ -0,0 +1,66 @@ +/* + * 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.Iterator; +import java.util.ServiceConfigurationError; +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.Logger; + +/** + * Shared best-effort {@link java.util.ServiceLoader} iteration for the CRaC warm-up paths. + */ +@SdkInternalApi +public final class WarmUpDiscovery { + + private static final Logger log = Logger.loggerFor(WarmUpDiscovery.class); + + private WarmUpDiscovery() { + } + + /** + * Applies {@code action} to every discovered element, skipping (and logging at warn) any element that fails to load or + * whose action throws, so the rest still run. Logs at debug when nothing is discovered. + */ + public static void forEachDiscovered(Iterator iterator, Consumer action) { + boolean discoveredAny = false; + while (iterator.hasNext()) { + T element; + try { + element = iterator.next(); + } catch (ServiceConfigurationError e) { + // next() has already advanced past the bad element, so it is safe to continue to the next one. + log.warn(() -> "Skipping a warm-up task that could not be loaded.", e); + continue; + } + + discoveredAny = true; + T discovered = element; + try { + action.accept(discovered); + } catch (RuntimeException | LinkageError e) { + // LinkageError because a discovered element can fail to link (missing deps/native lib, failed static init), + // which is an Error, not an Exception. Skip it to keep warm-up best-effort; fatal Errors still propagate. + log.warn(() -> "Warm-up failed for " + discovered.getClass().getName() + " and was skipped.", e); + } + } + + if (!discoveredAny) { + log.debug(() -> "No warm-up tasks were discovered on the classpath."); + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java new file mode 100644 index 000000000000..6b6b1a49609e --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java @@ -0,0 +1,73 @@ +/* + * 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.net.URI; +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; + +/** + * Describes the request the CRaC warm-up sends to the resolved endpoint, and converts it to an {@link SdkHttpRequest} bound to + * that endpoint. The request is never signed and needs no credentials; only its execution (DNS, TLS, request/response I/O) + * matters for JIT priming. + * + *

Add a new warm-up shape (e.g. a request with a body for a different protocol) by adding a factory method here. + */ +@SdkInternalApi +public final class WarmUpRequest { + + private final SdkHttpMethod method; + private final String body; + private final String contentType; + + private WarmUpRequest(SdkHttpMethod method, String body, String contentType) { + this.method = method; + this.body = body; + this.contentType = contentType; + } + + /** + * A bare {@code GET} to the endpoint, with no body. Warms DNS, the TLS handshake, and certificate-chain validation, which + * are the dominant cold-start costs and are shared across all services and operations. + */ + public static WarmUpRequest get() { + return new WarmUpRequest(SdkHttpMethod.GET, null, null); + } + + /** + * @retusrn this request as an {@link SdkHttpRequest} targeting {@code endpoint}. + */ + public SdkHttpRequest toHttpRequest(URI endpoint) { + SdkHttpRequest.Builder builder = SdkHttpRequest.builder() + .method(method) + .uri(endpoint); + if (body != null) { + builder.putHeader("Content-Type", contentType) + .putHeader("Content-Length", String.valueOf(body.getBytes(java.nio.charset.StandardCharsets.UTF_8).length)); + } + return builder.build(); + } + + /** + * @return the request body stream provider, or empty if this request has no body. + */ + public Optional contentStreamProvider() { + return body == null ? Optional.empty() : Optional.of(ContentStreamProvider.fromUtf8String(body)); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpClientWarmer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpClientWarmer.java new file mode 100644 index 000000000000..80be8d0deb79 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpClientWarmer.java @@ -0,0 +1,31 @@ +/* + * 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.http.loader; + +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * Warms the HTTP clients of one transport kind (sync or async) on the classpath for CRaC priming. {@code SdkWarmUp.prime()} + * invokes the sync and async implementations uniformly as peers. + */ +@SdkInternalApi +public interface HttpClientWarmer { + + /** + * Discovers and warms every HTTP client of this transport kind on the classpath. Best-effort; never throws. + */ + void warmAll(); +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java new file mode 100644 index 000000000000..8a5b98f26621 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java @@ -0,0 +1,106 @@ +/* + * 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.http.loader; + +import java.net.URI; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.core.internal.crac.RegionEndpointResolver; +import software.amazon.awssdk.core.internal.crac.WarmUpDiscovery; +import software.amazon.awssdk.core.internal.crac.WarmUpRequest; +import software.amazon.awssdk.http.ExecutableHttpRequest; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpService; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.awssdk.utils.Logger; + +/** + * Warms every sync {@link SdkHttpService} on the classpath for CRaC priming: builds each client and sends a best-effort + * {@link WarmUpRequest} to the resolved STS endpoint, draining the response body, so the HTTP/DNS/TLS/cert-chain code is + * JIT-compiled into the snapshot. + * + *

Lives in this package because the {@link SdkServiceLoader} it reuses is package-private. + */ +@SdkInternalApi +public final class SyncHttpClientWarmer implements HttpClientWarmer { + + private static final Logger log = Logger.loggerFor(SyncHttpClientWarmer.class); + + private final SdkServiceLoader serviceLoader; + private final Supplier endpointProvider; + private final WarmUpRequest warmUpRequest; + + @SdkTestInternalApi + SyncHttpClientWarmer(SdkServiceLoader serviceLoader, Supplier endpointProvider, WarmUpRequest warmUpRequest) { + this.serviceLoader = serviceLoader; + this.endpointProvider = endpointProvider; + this.warmUpRequest = warmUpRequest; + } + + /** + * Discovers sync HTTP clients from the classpath as usual, but warms them against {@code endpointProvider} instead of the + * resolved STS endpoint. Lets a caller redirect the warm-up request (e.g. to a local mock server). + */ + public SyncHttpClientWarmer(Supplier endpointProvider) { + this(SdkServiceLoader.INSTANCE, endpointProvider, WarmUpRequest.get()); + } + + public static SyncHttpClientWarmer create() { + return new SyncHttpClientWarmer(SdkServiceLoader.INSTANCE, + () -> RegionEndpointResolver.create().stsEndpoint(), + WarmUpRequest.get()); + } + + @Override + public void warmAll() { + URI endpoint = endpointProvider.get(); + WarmUpDiscovery.forEachDiscovered(serviceLoader.loadServices(SdkHttpService.class), service -> { + SdkHttpClient client = service.createHttpClientBuilder().buildWithDefaults(AttributeMap.empty()); + warmClient(client, endpoint); + }); + } + + /** + * Sends the {@link WarmUpRequest} to {@code endpoint}, drains the response body (which warms the read and decode path), + * and closes the client. Never throws; the goal is JIT compilation, not a successful request. + */ + private void warmClient(SdkHttpClient client, URI endpoint) { + try { + HttpExecuteRequest request = HttpExecuteRequest.builder() + .request(warmUpRequest.toHttpRequest(endpoint)) + .contentStreamProvider(warmUpRequest.contentStreamProvider() + .orElse(null)) + .build(); + ExecutableHttpRequest executableRequest = client.prepareRequest(request); + HttpExecuteResponse response = executableRequest.call(); + response.responseBody().ifPresent(body -> { + try { + IoUtils.drainInputStream(body); + } finally { + IoUtils.closeQuietlyV2(body, log); + } + }); + } catch (Exception e) { + log.debug(() -> "Sync HTTP client warm-up call failed (ignored).", e); + } finally { + IoUtils.closeQuietlyV2(client, log); + } + } +} 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 76d0dfbb2c3e..69a9b8cc2cca 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 @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** @@ -29,6 +31,27 @@ */ class SdkWarmUpTest { + private String savedRegionProperty; + + @BeforeEach + void setup() { + // prime() now also warms every sync HTTP client on the classpath (apache-client is on this module's test + // classpath) by firing a real GET at the resolved STS endpoint. Pin the region to a bogus value so the host + // (sts.warmup-unit-test.amazonaws.com) never routes to real STS: it fails fast with UnknownHostException, which + // the warmer swallows, keeping this unit test hermetic and offline. + savedRegionProperty = System.getProperty("aws.region"); + System.setProperty("aws.region", "warmup-unit-test"); + } + + @AfterEach + void teardown() { + if (savedRegionProperty != null) { + System.setProperty("aws.region", savedRegionProperty); + } else { + System.clearProperty("aws.region"); + } + } + @Test void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws InterruptedException { RegisteredWarmUpProvider.INVOCATIONS.set(0); diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvokerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvokerTest.java index c7d05fb7740c..284aa28cd582 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvokerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvokerTest.java @@ -101,7 +101,7 @@ void invokeAll_whenProviderThrows_logsAtWarn() { assertThat(logCaptor.loggedEvents()) .filteredOn(loggedFromInvoker()) .anyMatch(event -> event.getLevel() == Level.WARN - && event.getMessage().getFormattedMessage().contains("failed during warmUp()")); + && event.getMessage().getFormattedMessage().contains("Warm-up failed for")); } } @@ -127,12 +127,12 @@ void invokeAll_whenNoProviders_logsAtDebug() { assertThat(logCaptor.loggedEvents()) .filteredOn(loggedFromInvoker()) .anyMatch(event -> event.getLevel() == Level.DEBUG - && event.getMessage().getFormattedMessage().contains("No SdkWarmUpProvider")); + && event.getMessage().getFormattedMessage().contains("No warm-up tasks")); } } private static Predicate loggedFromInvoker() { - return event -> ClasspathWarmUpInvoker.class.getName().equals(event.getLoggerName()); + return event -> WarmUpDiscovery.class.getName().equals(event.getLoggerName()); } private WarmUpInvoker invokerLoading(SdkWarmUpProvider... providers) { diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java new file mode 100644 index 000000000000..8fc6e0d77c33 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java @@ -0,0 +1,140 @@ +/* + * 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.net.URI; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; + +/** + * Unit tests for {@link RegionEndpointResolver}. + * + *

Region resolution precedence under test (system property first, per the SDK's {@code SystemSetting} convention): + * {@code aws.region} system property, then {@code AWS_REGION} env, then {@code AWS_DEFAULT_REGION} env, then the + * {@code us-east-1} default. + */ +class RegionEndpointResolverTest { + + private static final String REGION_PROPERTY = "aws.region"; + private static final String AWS_REGION_ENV = "AWS_REGION"; + private static final String AWS_DEFAULT_REGION_ENV = "AWS_DEFAULT_REGION"; + + private final EnvironmentVariableHelper env = new EnvironmentVariableHelper(); + private String savedRegionProperty; + + @BeforeEach + void setup() { + savedRegionProperty = System.getProperty(REGION_PROPERTY); + System.clearProperty(REGION_PROPERTY); + env.remove(AWS_REGION_ENV); + env.remove(AWS_DEFAULT_REGION_ENV); + } + + @AfterEach + void teardown() { + env.reset(); + if (savedRegionProperty != null) { + System.setProperty(REGION_PROPERTY, savedRegionProperty); + } else { + System.clearProperty(REGION_PROPERTY); + } + } + + @Test + void stsEndpoint_noConfiguration_usesDefaultRegion() { + assertThat(RegionEndpointResolver.create().stsEndpoint()) + .isEqualTo(URI.create("https://sts.us-east-1.amazonaws.com/")); + } + + @Test + void stsEndpoint_systemProperty_takesPrecedenceOverEnvVars() { + System.setProperty(REGION_PROPERTY, "eu-west-1"); + env.set(AWS_REGION_ENV, "ap-south-1"); + env.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); + + assertThat(RegionEndpointResolver.create().stsEndpoint()) + .isEqualTo(URI.create("https://sts.eu-west-1.amazonaws.com/")); + } + + @Test + void stsEndpoint_awsRegionEnv_takesPrecedenceOverDefaultRegionEnv() { + env.set(AWS_REGION_ENV, "ap-south-1"); + env.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); + + assertThat(RegionEndpointResolver.create().stsEndpoint()) + .isEqualTo(URI.create("https://sts.ap-south-1.amazonaws.com/")); + } + + @Test + void stsEndpoint_awsDefaultRegionEnv_usedWhenNothingElseSet() { + env.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); + + assertThat(RegionEndpointResolver.create().stsEndpoint()) + .isEqualTo(URI.create("https://sts.us-west-2.amazonaws.com/")); + } + + @Test + void stsEndpoint_blankSystemProperty_fallsThroughToNextTier() { + System.setProperty(REGION_PROPERTY, " "); + env.set(AWS_REGION_ENV, "ap-south-1"); + + assertThat(RegionEndpointResolver.create().stsEndpoint()) + .isEqualTo(URI.create("https://sts.ap-south-1.amazonaws.com/")); + } + + @Test + void stsEndpoint_blankAwsRegionEnv_fallsThroughToDefaultRegionEnv() { + env.set(AWS_REGION_ENV, " "); + env.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); + + assertThat(RegionEndpointResolver.create().stsEndpoint()) + .isEqualTo(URI.create("https://sts.us-west-2.amazonaws.com/")); + } + + @Test + void stsEndpoint_configuredRegionIsTrimmed() { + System.setProperty(REGION_PROPERTY, " eu-central-1 "); + + assertThat(RegionEndpointResolver.create().stsEndpoint()) + .isEqualTo(URI.create("https://sts.eu-central-1.amazonaws.com/")); + } + + @Test + void stsEndpoint_doesNotCallImds() { + // Point IMDS at a non-routable address. A resolver that (incorrectly) called IMDS would block here; + // the correct resolver ignores IMDS entirely and returns the default region immediately. + String savedImdsEndpoint = System.getProperty("aws.ec2MetadataServiceEndpoint"); + System.setProperty("aws.ec2MetadataServiceEndpoint", "http://10.255.255.1"); + try { + long startNanos = System.nanoTime(); + URI endpoint = RegionEndpointResolver.create().stsEndpoint(); + long elapsedMillis = (System.nanoTime() - startNanos) / 1_000_000; + + assertThat(endpoint).isEqualTo(URI.create("https://sts.us-east-1.amazonaws.com/")); + assertThat(elapsedMillis).isLessThan(1_000L); + } finally { + if (savedImdsEndpoint != null) { + System.setProperty("aws.ec2MetadataServiceEndpoint", savedImdsEndpoint); + } else { + System.clearProperty("aws.ec2MetadataServiceEndpoint"); + } + } + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java new file mode 100644 index 000000000000..d27a0f328882 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java @@ -0,0 +1,132 @@ +/* + * 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.http.loader; + +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.junit.WireMockRule; +import java.io.File; +import java.net.URI; +import java.util.Collections; +import java.util.Iterator; +import java.util.function.Supplier; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.core.internal.crac.WarmUpRequest; +import software.amazon.awssdk.http.SdkHttpService; +import software.amazon.awssdk.http.apache.ApacheSdkHttpService; +import software.amazon.awssdk.http.apache5.Apache5SdkHttpService; + +/** + * Integration tests for {@link SyncHttpClientWarmer} against the real sync HTTP clients on this module's test classpath + * (Apache and Apache5) and a WireMock server. Each test pins discovery to one real client so the warm-up exercises that + * client implementation specifically. + * + *

The stub mirrors what a real {@code GET https://sts..amazonaws.com/} returns: a {@code 302} redirect with an + * empty body. The warm-up must not follow the redirect, so exactly one GET is expected. + */ +public class SyncHttpClientWarmerIntegrationTest { + + private static final int WARM_UP_CYCLES = 20; + private static final int STS_REDIRECT_STATUS = 302; + private static final String STS_REDIRECT_LOCATION = "https://aws.amazon.com/iam"; + + @Rule + public WireMockRule mockServer = new WireMockRule(0); + + @Test + public void warmAll_sendsWarmUpRequestThroughApache() { + assertWarmUpRequestIssued(new ApacheSdkHttpService()); + } + + @Test + public void warmAll_sendsWarmUpRequestThroughApache5() { + assertWarmUpRequestIssued(new Apache5SdkHttpService()); + } + + @Test + public void warmAll_repeatedCycles_doNotLeakFileDescriptors() { + org.junit.Assume.assumeTrue("FD check only runs where /proc/self/fd is available", openFdCountAvailable()); + stubStsRedirect(); + URI endpoint = endpoint(); + + long before = openFdCount(); + for (int i = 0; i < WARM_UP_CYCLES; i++) { + warmer(new ApacheSdkHttpService(), endpoint).warmAll(); + } + long after = openFdCount(); + + assertThat(after - before).isLessThan(WARM_UP_CYCLES); + } + + @Test + public void warmAll_whenServerUnreachable_swallowsAndDoesNotThrow() { + int unusedPort = mockServer.port(); + mockServer.stop(); + URI endpoint = URI.create("http://localhost:" + unusedPort + "/"); + + assertThatCode(() -> warmer(new ApacheSdkHttpService(), endpoint).warmAll()).doesNotThrowAnyException(); + } + + private void assertWarmUpRequestIssued(SdkHttpService service) { + stubStsRedirect(); + + warmer(service, endpoint()).warmAll(); + + // One GET to "/", and nothing else: the warm-up issued the GET and did not follow the redirect. + mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); + mockServer.verify(1, anyRequestedFor(anyUrl())); + } + + // Warmer pinned to a single real service, warmed against the given (WireMock) endpoint. + private static SyncHttpClientWarmer warmer(SdkHttpService service, URI endpoint) { + SdkServiceLoader loader = new SdkServiceLoader() { + @Override + @SuppressWarnings("unchecked") + Iterator loadServices(Class clazz) { + return (Iterator) Collections.singletonList(service).iterator(); + } + }; + Supplier endpointProvider = () -> endpoint; + return new SyncHttpClientWarmer(loader, endpointProvider, WarmUpRequest.get()); + } + + private URI endpoint() { + return URI.create("http://localhost:" + mockServer.port() + "/"); + } + + private void stubStsRedirect() { + mockServer.stubFor(any(anyUrl()).willReturn(aResponse() + .withStatus(STS_REDIRECT_STATUS) + .withHeader("Location", STS_REDIRECT_LOCATION))); + } + + private static boolean openFdCountAvailable() { + return new File("/proc/self/fd").isDirectory(); + } + + private static long openFdCount() { + File[] fds = new File("/proc/self/fd").listFiles(); + return fds == null ? 0 : fds.length; + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java new file mode 100644 index 000000000000..6b592aae70a7 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java @@ -0,0 +1,180 @@ +/* + * 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.http.loader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import software.amazon.awssdk.core.internal.crac.WarmUpRequest; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.ExecutableHttpRequest; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.SdkHttpService; + +/** + * Unit tests for {@link SyncHttpClientWarmer}. Every test drives the real {@link SyncHttpClientWarmer#warmAll()} with an + * injected fake {@link SdkServiceLoader} (supplying stub {@link SdkHttpService}s) and a fixed endpoint supplier (standing in + * for the resolved STS host). + */ +class SyncHttpClientWarmerTest { + + private static final URI ENDPOINT = URI.create("https://sts.us-east-1.amazonaws.com/"); + + // ---- per-client recipe (driven through warmAll) ---- + + @Test + void warmAll_drainsAndClosesResponseBody() throws IOException { + InputStream body = spy(new ByteArrayInputStream("denied".getBytes())); + SdkHttpClient client = stubClient(respondingWith(403, body)); + + warmer(serviceFor(client)).warmAll(); + + verify(body, atLeastOnce()).read(); // drained to EOF + verify(body).close(); + verify(client).close(); + } + + @Test + void warmAll_issuesGetToResolvedEndpoint() { + SdkHttpClient client = stubClient(respondingWith(403, emptyBody())); + ArgumentCaptor request = ArgumentCaptor.forClass(HttpExecuteRequest.class); + + warmer(serviceFor(client)).warmAll(); + + verify(client).prepareRequest(request.capture()); + assertThat(request.getValue().httpRequest().method()).isEqualTo(SdkHttpMethod.GET); + assertThat(request.getValue().httpRequest().getUri()).isEqualTo(ENDPOINT); + } + + @Test + void warmAll_whenRequestFails_swallowsAndStillClosesClient() throws IOException { + SdkHttpClient client = mock(SdkHttpClient.class); + ExecutableHttpRequest request = mock(ExecutableHttpRequest.class); + when(client.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(request); + when(request.call()).thenThrow(new IOException("offline")); + + assertThatCode(() -> warmer(serviceFor(client)).warmAll()).doesNotThrowAnyException(); + verify(client).close(); + } + + @Test + void warmAll_whenNoResponseBody_stillClosesClient() { + SdkHttpClient client = stubClient(respondingWith(403, null)); + + assertThatCode(() -> warmer(serviceFor(client)).warmAll()).doesNotThrowAnyException(); + verify(client).close(); + } + + // ---- discovery loop ---- + + @Test + void warmAll_warmsEveryDiscoveredService() { + SdkHttpClient first = stubClient(respondingWith(403, emptyBody())); + SdkHttpClient second = stubClient(respondingWith(403, emptyBody())); + + warmer(serviceFor(first), serviceFor(second)).warmAll(); + + verify(first).prepareRequest(any(HttpExecuteRequest.class)); + verify(second).prepareRequest(any(HttpExecuteRequest.class)); + } + + @Test + void warmAll_whenOneServiceFailsToBuild_stillWarmsOthers() { + SdkHttpService failing = mock(SdkHttpService.class); + when(failing.createHttpClientBuilder()).thenThrow(new RuntimeException("bad service")); + SdkHttpClient healthy = stubClient(respondingWith(403, emptyBody())); + + warmer(failing, serviceFor(healthy)).warmAll(); + + verify(healthy).prepareRequest(any(HttpExecuteRequest.class)); + } + + @Test + void warmAll_whenNoServices_isNoOp() { + assertThatCode(() -> warmer(Collections.emptyIterator()).warmAll()).doesNotThrowAnyException(); + } + + // ---- helpers ---- + + private static SyncHttpClientWarmer warmer(SdkHttpService... services) { + return warmer(Arrays.asList(services).iterator()); + } + + private static SyncHttpClientWarmer warmer(Iterator services) { + SdkServiceLoader loader = new SdkServiceLoader() { + @Override + @SuppressWarnings("unchecked") + Iterator loadServices(Class clazz) { + return (Iterator) services; + } + }; + return new SyncHttpClientWarmer(loader, () -> ENDPOINT, WarmUpRequest.get()); + } + + /** A service whose builder yields the given client. */ + private static SdkHttpService serviceFor(SdkHttpClient client) { + SdkHttpClient.Builder builder = mock(SdkHttpClient.Builder.class); + when(builder.buildWithDefaults(any())).thenReturn(client); + + SdkHttpService service = mock(SdkHttpService.class); + when(service.createHttpClientBuilder()).thenReturn(builder); + return service; + } + + /** A client whose single request returns the given response. */ + private static SdkHttpClient stubClient(HttpExecuteResponse response) { + try { + ExecutableHttpRequest request = mock(ExecutableHttpRequest.class); + when(request.call()).thenReturn(response); + + SdkHttpClient client = mock(SdkHttpClient.class); + when(client.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(request); + return client; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static HttpExecuteResponse respondingWith(int statusCode, InputStream body) { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(statusCode).build()) + .responseBody(body == null ? null : AbortableInputStream.create(body)) + .build(); + } + + private static InputStream emptyBody() { + return new ByteArrayInputStream(new byte[0]); + } +} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWarmUpTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWarmUpTest.java new file mode 100644 index 000000000000..a5065fe6a724 --- /dev/null +++ b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWarmUpTest.java @@ -0,0 +1,52 @@ +/* + * 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.crt; + +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 com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; + +/** + * Verifies the CRaC sync warm-up sends its GET through the real CRT sync client discovered on the classpath. The stub mirrors + * a real STS {@code GET} (302 redirect, empty body); the warm-up must not follow the redirect. + */ +public class AwsCrtHttpClientWarmUpTest { + + @Rule + public WireMockRule mockServer = new WireMockRule(0); + + @Test + public void warmAll_sendsWarmUpGetThroughCrtClient() { + mockServer.stubFor(any(anyUrl()).willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "https://aws.amazon.com/iam"))); + + URI endpoint = URI.create("http://localhost:" + mockServer.port() + "/"); + new SyncHttpClientWarmer(() -> endpoint).warmAll(); + + mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); + mockServer.verify(1, anyRequestedFor(anyUrl())); + } +} diff --git a/http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientWarmUpTest.java b/http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientWarmUpTest.java new file mode 100644 index 000000000000..3ef2f27b0923 --- /dev/null +++ b/http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientWarmUpTest.java @@ -0,0 +1,52 @@ +/* + * 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.urlconnection; + +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 com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; + +/** + * Verifies the CRaC sync warm-up sends its GET through the real {@code UrlConnectionHttpClient} discovered on the classpath. + * The stub mirrors a real STS {@code GET} (302 redirect, empty body); the warm-up must not follow the redirect. + */ +public class UrlConnectionHttpClientWarmUpTest { + + @Rule + public WireMockRule mockServer = new WireMockRule(0); + + @Test + public void warmAll_sendsWarmUpGetThroughUrlConnectionClient() { + mockServer.stubFor(any(anyUrl()).willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "https://aws.amazon.com/iam"))); + + URI endpoint = URI.create("http://localhost:" + mockServer.port() + "/"); + new SyncHttpClientWarmer(() -> endpoint).warmAll(); + + mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); + mockServer.verify(1, anyRequestedFor(anyUrl())); + } +} 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 c942eaf9c8b8..edfff3c08bf5 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,7 @@ public class CodingConventionWithSuppressionTest { ArchUtils.classNameToPattern(KnownContentLengthAsyncRequestBodySubscriber.class), ArchUtils.classNameToPattern(UnknownContentLengthAsyncRequestBodySubscriber.class), ArchUtils.classNameToPattern(CopyObjectHelper.class), - ArchUtils.classNameToPattern("software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker"))); + ArchUtils.classNameToPattern("software.amazon.awssdk.core.internal.crac.WarmUpDiscovery"))); private static final Set ALLOWED_ERROR_LOG_SUPPRESSION = new HashSet<>( Arrays.asList( From 82c723bda619e837db6eadeff220bdbaf23486bd Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 26 Jun 2026 08:12:39 -0700 Subject: [PATCH 2/7] Updated tests --- .../amazon/awssdk/core/crac/SdkWarmUp.java | 29 ++--- .../internal/crac/RegionEndpointResolver.java | 16 +-- .../loader/ClasspathHttpWarmupInvoker.java | 49 ++++++++ .../http/loader/HttpClientWarmer.java | 5 +- .../http/loader/HttpWarmupInvoker.java | 30 +++++ .../crac/RegionEndpointResolverTest.java | 108 +++++++----------- .../ClasspathHttpWarmupInvokerTest.java | 57 +++++++++ .../http/loader/SyncHttpClientWarmerTest.java | 6 +- 8 files changed, 200 insertions(+), 100 deletions(-) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/ClasspathHttpWarmupInvoker.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpWarmupInvoker.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/ClasspathHttpWarmupInvokerTest.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 73861377ae8e..9bd34cab3122 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,14 +15,11 @@ package software.amazon.awssdk.core.crac; -import java.util.Arrays; -import java.util.List; import java.util.ServiceLoader; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker; -import software.amazon.awssdk.core.internal.http.loader.HttpClientWarmer; -import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; +import software.amazon.awssdk.core.internal.http.loader.ClasspathHttpWarmupInvoker; /** * Entry point for warming up SDK service request paths before a Coordinated Restore at Checkpoint (CRaC) @@ -34,16 +31,15 @@ * *

Behavior contract: *

* - *

{@code prime()} also fires a best-effort {@code GET} per sync HTTP client at a regional AWS endpoint to JIT-compile the - * HTTP, DNS, TLS, and cert-chain paths. This needs network connectivity during init; if unavailable, the failure is swallowed. - * - *

Call this once during initialization, before a CRaC checkpoint is taken. + *

Call this once during application initialization, before a CRaC checkpoint is taken. */ @ThreadSafe @SdkPublicApi @@ -51,9 +47,6 @@ public final class SdkWarmUp { private static final Object PRIME_LOCK = new Object(); - // The HTTP-client warmers invoked by prime(), one per transport kind. The async warmer is added here when implemented. - private static final List HTTP_CLIENT_WARMERS = Arrays.asList(SyncHttpClientWarmer.create()); - private static volatile boolean primed = false; private SdkWarmUp() { @@ -72,11 +65,9 @@ public static void prime() { if (primed) { return; } - // Set primed only after warm-up succeeds, so a failed run leaves primed false and a later call retries. + // Set primed only after invokeAll() succeeds, so a failed run leaves primed false and a later call retries. ClasspathWarmUpInvoker.create().invokeAll(); - for (HttpClientWarmer warmer : HTTP_CLIENT_WARMERS) { - warmer.warmAll(); - } + ClasspathHttpWarmupInvoker.create().invokeAll(); primed = true; } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java index deb8f406c90c..d1ee8617c081 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java @@ -22,16 +22,16 @@ import software.amazon.awssdk.utils.SystemSetting; /** - * Resolves the regional STS endpoint that the CRaC HTTP-client warm-up sends its GET to. + * Resolves the regional STS endpoint ({@code https://sts..amazonaws.com/}) used by the CRaC HTTP-client warm-up. * - *

Region precedence: {@code aws.region} system property, {@code AWS_REGION} env, {@code AWS_DEFAULT_REGION} env, then - * {@value #DEFAULT_REGION}. Tiers are resolved independently so a blank value falls through; the combined - * {@code SdkSystemSetting.AWS_REGION.getStringValue()} cannot be used because a blank {@code aws.region} would shadow the env - * vars. + *

The region is taken from the first of: {@code aws.region} system property, {@code AWS_REGION} environment variable, + * {@code AWS_DEFAULT_REGION} environment variable, or {@value #DEFAULT_REGION}. A blank value at one source is ignored and the + * next is tried. * - *

Reads only system properties and env vars: no IMDS, profile-file, or credential-provider lookups, which would slow - * priming. The host is partition-naive ({@code sts..amazonaws.com}); the warm-up only JIT-compiles DNS/TLS/cert-chain - * against any reachable AWS host and is best-effort, so a wrong host in cn/gov/iso partitions fails and is swallowed. + *

Only system properties and environment variables are read. The full SDK region-resolution chain (IMDS, profile file) is + * avoided during priming because those add network or filesystem calls that may fail or time out. The endpoint host always + * uses the {@code amazonaws.com} suffix, which is incorrect for the China, GovCloud, and ISO partitions; in those partitions + * the warm-up request simply fails and is ignored, since it is best-effort. */ @SdkInternalApi public final class RegionEndpointResolver { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/ClasspathHttpWarmupInvoker.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/ClasspathHttpWarmupInvoker.java new file mode 100644 index 000000000000..3cc15194e7c3 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/ClasspathHttpWarmupInvoker.java @@ -0,0 +1,49 @@ +/* + * 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.http.loader; + +import java.util.Collections; +import java.util.List; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; + +/** + * {@link HttpWarmupInvoker} that owns the set of {@link HttpClientWarmer}s and invokes each. + */ +@SdkInternalApi +public final class ClasspathHttpWarmupInvoker implements HttpWarmupInvoker { + + private final List warmers; + + @SdkTestInternalApi + ClasspathHttpWarmupInvoker(List warmers) { + this.warmers = warmers; + } + + /** + * @return an invoker over the HTTP-client warmers on the classpath. + */ + public static HttpWarmupInvoker create() { + return new ClasspathHttpWarmupInvoker(Collections.singletonList(SyncHttpClientWarmer.create())); + } + + @Override + public void invokeAll() { + for (HttpClientWarmer warmer : warmers) { + warmer.warmAll(); + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpClientWarmer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpClientWarmer.java index 80be8d0deb79..e63b7d62dec2 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpClientWarmer.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpClientWarmer.java @@ -18,14 +18,13 @@ import software.amazon.awssdk.annotations.SdkInternalApi; /** - * Warms the HTTP clients of one transport kind (sync or async) on the classpath for CRaC priming. {@code SdkWarmUp.prime()} - * invokes the sync and async implementations uniformly as peers. + * Warms the sync or async HTTP clients on the classpath for CRaC priming. */ @SdkInternalApi public interface HttpClientWarmer { /** - * Discovers and warms every HTTP client of this transport kind on the classpath. Best-effort; never throws. + * Warms every HTTP client found on the classpath. Best-effort; never throws. */ void warmAll(); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpWarmupInvoker.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpWarmupInvoker.java new file mode 100644 index 000000000000..c7fc72c19fd1 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/HttpWarmupInvoker.java @@ -0,0 +1,30 @@ +/* + * 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.http.loader; + +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * Warms every HTTP client on the classpath for CRaC priming by invoking each {@link HttpClientWarmer}. + */ +@SdkInternalApi +public interface HttpWarmupInvoker { + + /** + * Invokes {@link HttpClientWarmer#warmAll()} on every warmer. Best-effort; never throws. + */ + void invokeAll(); +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java index 8fc6e0d77c33..5826fbffdc58 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java @@ -16,11 +16,16 @@ package software.amazon.awssdk.core.internal.crac; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; import java.net.URI; +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.testutils.EnvironmentVariableHelper; /** @@ -36,88 +41,45 @@ class RegionEndpointResolverTest { private static final String AWS_REGION_ENV = "AWS_REGION"; private static final String AWS_DEFAULT_REGION_ENV = "AWS_DEFAULT_REGION"; - private final EnvironmentVariableHelper env = new EnvironmentVariableHelper(); - private String savedRegionProperty; + private static final EnvironmentVariableHelper ENV = new EnvironmentVariableHelper(); @BeforeEach - void setup() { - savedRegionProperty = System.getProperty(REGION_PROPERTY); + void clearSettings() { + ENV.reset(); System.clearProperty(REGION_PROPERTY); - env.remove(AWS_REGION_ENV); - env.remove(AWS_DEFAULT_REGION_ENV); } @AfterEach - void teardown() { - env.reset(); - if (savedRegionProperty != null) { - System.setProperty(REGION_PROPERTY, savedRegionProperty); - } else { - System.clearProperty(REGION_PROPERTY); - } - } - - @Test - void stsEndpoint_noConfiguration_usesDefaultRegion() { - assertThat(RegionEndpointResolver.create().stsEndpoint()) - .isEqualTo(URI.create("https://sts.us-east-1.amazonaws.com/")); - } - - @Test - void stsEndpoint_systemProperty_takesPrecedenceOverEnvVars() { - System.setProperty(REGION_PROPERTY, "eu-west-1"); - env.set(AWS_REGION_ENV, "ap-south-1"); - env.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); - - assertThat(RegionEndpointResolver.create().stsEndpoint()) - .isEqualTo(URI.create("https://sts.eu-west-1.amazonaws.com/")); - } - - @Test - void stsEndpoint_awsRegionEnv_takesPrecedenceOverDefaultRegionEnv() { - env.set(AWS_REGION_ENV, "ap-south-1"); - env.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); - - assertThat(RegionEndpointResolver.create().stsEndpoint()) - .isEqualTo(URI.create("https://sts.ap-south-1.amazonaws.com/")); - } - - @Test - void stsEndpoint_awsDefaultRegionEnv_usedWhenNothingElseSet() { - env.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); - - assertThat(RegionEndpointResolver.create().stsEndpoint()) - .isEqualTo(URI.create("https://sts.us-west-2.amazonaws.com/")); + void restoreSettings() { + ENV.reset(); + System.clearProperty(REGION_PROPERTY); } - @Test - void stsEndpoint_blankSystemProperty_fallsThroughToNextTier() { - System.setProperty(REGION_PROPERTY, " "); - env.set(AWS_REGION_ENV, "ap-south-1"); + @ParameterizedTest(name = "{0}") + @MethodSource("regionResolutionCases") + void stsEndpoint_whenRegionConfigured_resolvesExpectedEndpoint(String description, String sysprop, String awsRegion, + String awsDefaultRegion, String expectedRegion) { + applyRegionSettings(sysprop, awsRegion, awsDefaultRegion); assertThat(RegionEndpointResolver.create().stsEndpoint()) - .isEqualTo(URI.create("https://sts.ap-south-1.amazonaws.com/")); + .isEqualTo(URI.create("https://sts." + expectedRegion + ".amazonaws.com/")); } - @Test - void stsEndpoint_blankAwsRegionEnv_fallsThroughToDefaultRegionEnv() { - env.set(AWS_REGION_ENV, " "); - env.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); - - assertThat(RegionEndpointResolver.create().stsEndpoint()) - .isEqualTo(URI.create("https://sts.us-west-2.amazonaws.com/")); + private static Stream regionResolutionCases() { + return Stream.of( + // description aws.region AWS_REGION AWS_DEFAULT_REGION expected + arguments("nothing set -> default region", null, null, null, "us-east-1"), + arguments("system property wins over both env vars", "eu-west-1", "ap-south-1", "us-west-2", "eu-west-1"), + arguments("AWS_REGION wins over AWS_DEFAULT_REGION", null, "ap-south-1", "us-west-2", "ap-south-1"), + arguments("AWS_DEFAULT_REGION used when nothing else set", null, null, "us-west-2", "us-west-2"), + arguments("blank system property falls through to AWS_REGION", " ", "ap-south-1", null, "ap-south-1"), + arguments("blank AWS_REGION falls through to AWS_DEFAULT_REGION", null, " ", "us-west-2", "us-west-2"), + arguments("chosen value is trimmed", " eu-central-1 ", null, null, "eu-central-1") + ); } @Test - void stsEndpoint_configuredRegionIsTrimmed() { - System.setProperty(REGION_PROPERTY, " eu-central-1 "); - - assertThat(RegionEndpointResolver.create().stsEndpoint()) - .isEqualTo(URI.create("https://sts.eu-central-1.amazonaws.com/")); - } - - @Test - void stsEndpoint_doesNotCallImds() { + void stsEndpoint_whenImdsUnreachable_resolvesWithoutCallingImds() { // Point IMDS at a non-routable address. A resolver that (incorrectly) called IMDS would block here; // the correct resolver ignores IMDS entirely and returns the default region immediately. String savedImdsEndpoint = System.getProperty("aws.ec2MetadataServiceEndpoint"); @@ -137,4 +99,16 @@ void stsEndpoint_doesNotCallImds() { } } } + + private static void applyRegionSettings(String sysprop, String awsRegion, String awsDefaultRegion) { + if (sysprop != null) { + System.setProperty(REGION_PROPERTY, sysprop); + } + if (awsRegion != null) { + ENV.set(AWS_REGION_ENV, awsRegion); + } + if (awsDefaultRegion != null) { + ENV.set(AWS_DEFAULT_REGION_ENV, awsDefaultRegion); + } + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/ClasspathHttpWarmupInvokerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/ClasspathHttpWarmupInvokerTest.java new file mode 100644 index 000000000000..7fd1240b8b73 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/ClasspathHttpWarmupInvokerTest.java @@ -0,0 +1,57 @@ +/* + * 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.http.loader; + +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.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class ClasspathHttpWarmupInvokerTest { + + @Test + void invokeAll_whenMultipleWarmers_invokesEachOnce() { + CountingWarmer first = new CountingWarmer(); + CountingWarmer second = new CountingWarmer(); + + new ClasspathHttpWarmupInvoker(Arrays.asList(first, second)).invokeAll(); + + assertThat(first.invocations()).isEqualTo(1); + assertThat(second.invocations()).isEqualTo(1); + } + + @Test + void invokeAll_whenNoWarmers_isNoOp() { + assertThatCode(() -> new ClasspathHttpWarmupInvoker(Collections.emptyList()).invokeAll()) + .doesNotThrowAnyException(); + } + + private static final class CountingWarmer implements HttpClientWarmer { + private final AtomicInteger invocations = new AtomicInteger(); + + @Override + public void warmAll() { + invocations.incrementAndGet(); + } + + int invocations() { + return invocations.get(); + } + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java index 6b592aae70a7..bb119debc3a3 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java @@ -55,7 +55,7 @@ class SyncHttpClientWarmerTest { // ---- per-client recipe (driven through warmAll) ---- @Test - void warmAll_drainsAndClosesResponseBody() throws IOException { + void warmAll_whenResponseHasBody_drainsAndClosesIt() throws IOException { InputStream body = spy(new ByteArrayInputStream("denied".getBytes())); SdkHttpClient client = stubClient(respondingWith(403, body)); @@ -67,7 +67,7 @@ void warmAll_drainsAndClosesResponseBody() throws IOException { } @Test - void warmAll_issuesGetToResolvedEndpoint() { + void warmAll_whenInvoked_issuesGetToResolvedEndpoint() { SdkHttpClient client = stubClient(respondingWith(403, emptyBody())); ArgumentCaptor request = ArgumentCaptor.forClass(HttpExecuteRequest.class); @@ -100,7 +100,7 @@ void warmAll_whenNoResponseBody_stillClosesClient() { // ---- discovery loop ---- @Test - void warmAll_warmsEveryDiscoveredService() { + void warmAll_whenMultipleServicesDiscovered_warmsEach() { SdkHttpClient first = stubClient(respondingWith(403, emptyBody())); SdkHttpClient second = stubClient(respondingWith(403, emptyBody())); From 311aff8fc7589de9cdfdb6f66a6df3c9122500ce Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 26 Jun 2026 08:53:19 -0700 Subject: [PATCH 3/7] restructure test and comments --- core/sdk-core/pom.xml | 6 --- .../core/internal/crac/WarmUpRequest.java | 2 - .../http/loader/SyncHttpClientWarmer.java | 3 +- .../awssdk/core/crac/SdkWarmUpTest.java | 5 +- .../crac/RegionEndpointResolverTest.java | 7 --- .../SyncHttpClientWarmerIntegrationTest.java | 17 ++---- http-clients/apache5-client/pom.xml | 6 +++ .../apache5/Apache5HttpClientWarmUpTest.java | 52 +++++++++++++++++++ 8 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWarmUpTest.java diff --git a/core/sdk-core/pom.xml b/core/sdk-core/pom.xml index af5df62ca03b..88682ba51c93 100644 --- a/core/sdk-core/pom.xml +++ b/core/sdk-core/pom.xml @@ -112,12 +112,6 @@ ${awsjavasdk.version} test - - software.amazon.awssdk - apache5-client - ${awsjavasdk.version} - test - org.junit.jupiter junit-jupiter diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java index 6b6b1a49609e..42aed4b83627 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java @@ -26,8 +26,6 @@ * Describes the request the CRaC warm-up sends to the resolved endpoint, and converts it to an {@link SdkHttpRequest} bound to * that endpoint. The request is never signed and needs no credentials; only its execution (DNS, TLS, request/response I/O) * matters for JIT priming. - * - *

Add a new warm-up shape (e.g. a request with a body for a different protocol) by adding a factory method here. */ @SdkInternalApi public final class WarmUpRequest { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java index 8a5b98f26621..cf98ee40231e 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java @@ -56,8 +56,9 @@ public final class SyncHttpClientWarmer implements HttpClientWarmer { /** * Discovers sync HTTP clients from the classpath as usual, but warms them against {@code endpointProvider} instead of the - * resolved STS endpoint. Lets a caller redirect the warm-up request (e.g. to a local mock server). + * resolved STS endpoint, so a test can redirect the warm-up request to a local mock server. */ + @SdkTestInternalApi public SyncHttpClientWarmer(Supplier endpointProvider) { this(SdkServiceLoader.INSTANCE, endpointProvider, WarmUpRequest.get()); } 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 69a9b8cc2cca..e21cdf3dd657 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 @@ -35,10 +35,7 @@ class SdkWarmUpTest { @BeforeEach void setup() { - // prime() now also warms every sync HTTP client on the classpath (apache-client is on this module's test - // classpath) by firing a real GET at the resolved STS endpoint. Pin the region to a bogus value so the host - // (sts.warmup-unit-test.amazonaws.com) never routes to real STS: it fails fast with UnknownHostException, which - // the warmer swallows, keeping this unit test hermetic and offline. + // 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"); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java index 5826fbffdc58..3b8a348697a5 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java @@ -28,13 +28,6 @@ import org.junit.jupiter.params.provider.MethodSource; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; -/** - * Unit tests for {@link RegionEndpointResolver}. - * - *

Region resolution precedence under test (system property first, per the SDK's {@code SystemSetting} convention): - * {@code aws.region} system property, then {@code AWS_REGION} env, then {@code AWS_DEFAULT_REGION} env, then the - * {@code us-east-1} default. - */ class RegionEndpointResolverTest { private static final String REGION_PROPERTY = "aws.region"; diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java index d27a0f328882..85ece31bd78c 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java @@ -23,6 +23,7 @@ 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.Assume.assumeTrue; import com.github.tomakehurst.wiremock.junit.WireMockRule; import java.io.File; @@ -35,15 +36,12 @@ import software.amazon.awssdk.core.internal.crac.WarmUpRequest; import software.amazon.awssdk.http.SdkHttpService; import software.amazon.awssdk.http.apache.ApacheSdkHttpService; -import software.amazon.awssdk.http.apache5.Apache5SdkHttpService; /** - * Integration tests for {@link SyncHttpClientWarmer} against the real sync HTTP clients on this module's test classpath - * (Apache and Apache5) and a WireMock server. Each test pins discovery to one real client so the warm-up exercises that - * client implementation specifically. + * Integration tests that run {@link SyncHttpClientWarmer} against a real HTTP client and a WireMock server. * - *

The stub mirrors what a real {@code GET https://sts..amazonaws.com/} returns: a {@code 302} redirect with an - * empty body. The warm-up must not follow the redirect, so exactly one GET is expected. + *

WireMock returns a {@code 302} redirect with an empty body, like a real {@code GET} to an STS endpoint. The warm-up + * must not follow the redirect, so exactly one request is expected. */ public class SyncHttpClientWarmerIntegrationTest { @@ -59,14 +57,9 @@ public void warmAll_sendsWarmUpRequestThroughApache() { assertWarmUpRequestIssued(new ApacheSdkHttpService()); } - @Test - public void warmAll_sendsWarmUpRequestThroughApache5() { - assertWarmUpRequestIssued(new Apache5SdkHttpService()); - } - @Test public void warmAll_repeatedCycles_doNotLeakFileDescriptors() { - org.junit.Assume.assumeTrue("FD check only runs where /proc/self/fd is available", openFdCountAvailable()); + assumeTrue("FD check only runs where /proc/self/fd is available", openFdCountAvailable()); stubStsRedirect(); URI endpoint = endpoint(); diff --git a/http-clients/apache5-client/pom.xml b/http-clients/apache5-client/pom.xml index d95b9cf7f6c6..1d01c51b4fb8 100644 --- a/http-clients/apache5-client/pom.xml +++ b/http-clients/apache5-client/pom.xml @@ -70,6 +70,12 @@ ${awsjavasdk.version} test + + software.amazon.awssdk + sdk-core + ${awsjavasdk.version} + test + org.apache.logging.log4j diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWarmUpTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWarmUpTest.java new file mode 100644 index 000000000000..ed21065948e2 --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWarmUpTest.java @@ -0,0 +1,52 @@ +/* + * 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.apache5; + +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 com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; + +/** + * Verifies the CRaC sync warm-up sends its GET through the real {@code Apache5HttpClient} discovered on the classpath. + * The stub mirrors a real STS {@code GET} (302 redirect, empty body); the warm-up must not follow the redirect. + */ +public class Apache5HttpClientWarmUpTest { + + @Rule + public WireMockRule mockServer = new WireMockRule(0); + + @Test + public void warmAll_sendsWarmUpGetThroughApache5Client() { + mockServer.stubFor(any(anyUrl()).willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "https://aws.amazon.com/iam"))); + + URI endpoint = URI.create("http://localhost:" + mockServer.port() + "/"); + new SyncHttpClientWarmer(() -> endpoint).warmAll(); + + mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); + mockServer.verify(1, anyRequestedFor(anyUrl())); + } +} From 2f5b8640fe7835274abcbc02d4635cd9316c229d Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 26 Jun 2026 09:16:57 -0700 Subject: [PATCH 4/7] Fixed checkstyle issues --- .../internal/crac/RegionEndpointResolver.java | 64 ++++++++++--------- .../crac/RegionEndpointResolverTest.java | 22 +++++++ .../SyncHttpClientWarmerIntegrationTest.java | 28 -------- .../http/loader/SyncHttpClientWarmerTest.java | 6 -- 4 files changed, 55 insertions(+), 65 deletions(-) diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java index d1ee8617c081..f3b5462582fa 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java @@ -18,15 +18,18 @@ import java.net.URI; import java.util.Optional; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.utils.OptionalUtils; import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.SystemSetting; +import software.amazon.awssdk.utils.http.SdkHttpUtils; +import software.amazon.awssdk.utils.internal.SystemSettingUtils; /** * Resolves the regional STS endpoint ({@code https://sts..amazonaws.com/}) used by the CRaC HTTP-client warm-up. * - *

The region is taken from the first of: {@code aws.region} system property, {@code AWS_REGION} environment variable, - * {@code AWS_DEFAULT_REGION} environment variable, or {@value #DEFAULT_REGION}. A blank value at one source is ignored and the - * next is tried. + *

The region is taken from the first of: {@link SdkSystemSetting#AWS_REGION} (the {@code aws.region} system property or + * {@code AWS_REGION} environment variable), the {@code AWS_DEFAULT_REGION} environment variable, or {@value #DEFAULT_REGION}. * *

Only system properties and environment variables are read. The full SDK region-resolution chain (IMDS, profile file) is * avoided during priming because those add network or filesystem calls that may fail or time out. The endpoint host always @@ -38,29 +41,6 @@ public final class RegionEndpointResolver { static final String DEFAULT_REGION = "us-east-1"; - private static final String AWS_REGION_PROPERTY = "aws.region"; - private static final String AWS_REGION_ENV_VAR = "AWS_REGION"; - private static final String AWS_DEFAULT_REGION_ENV_VAR = "AWS_DEFAULT_REGION"; - - // Property-only SystemSetting (null env var) so aws.region is read as a tier distinct from the AWS_REGION env var, - // without a direct System.getProperty call (Checkstyle-banned outside the system-setting utilities). - private static final SystemSetting AWS_REGION_PROPERTY_SETTING = new SystemSetting() { - @Override - public String property() { - return AWS_REGION_PROPERTY; - } - - @Override - public String environmentVariable() { - return null; - } - - @Override - public String defaultValue() { - return null; - } - }; - private RegionEndpointResolver() { } @@ -72,18 +52,40 @@ public static RegionEndpointResolver create() { * @return the regional STS endpoint URI for the resolved region; never null. */ public URI stsEndpoint() { - return URI.create("https://sts." + resolveRegion() + ".amazonaws.com/"); + // URL-encode the region before putting it in the host, same as Region.of(String). + return URI.create("https://sts." + SdkHttpUtils.urlEncode(resolveRegion()) + ".amazonaws.com/"); } private String resolveRegion() { - return trimmed(AWS_REGION_PROPERTY_SETTING.getStringValue()) - .orElseGet(() -> trimmed(SystemSetting.getStringValueFromEnvironmentVariable(AWS_REGION_ENV_VAR)) - .orElseGet(() -> trimmed(SystemSetting.getStringValueFromEnvironmentVariable(AWS_DEFAULT_REGION_ENV_VAR)) - .orElse(DEFAULT_REGION))); + Optional awsRegion = trimmed(SdkSystemSetting.AWS_REGION.getStringValue()); + return OptionalUtils.firstPresent(awsRegion, RegionEndpointResolver::awsDefaultRegion) + .orElse(DEFAULT_REGION); + } + + private static Optional awsDefaultRegion() { + return trimmed(SystemSettingUtils.resolveEnvironmentVariable(new AwsDefaultRegionEnvVar())); } private static Optional trimmed(Optional value) { // trimToNull returns null for blank/empty input, so Optional.map collapses those to an empty Optional. return value.map(StringUtils::trimToNull); } + + // AWS_DEFAULT_REGION is an environment-variable-only fallback with no system-property equivalent. + private static final class AwsDefaultRegionEnvVar implements SystemSetting { + @Override + public String property() { + return null; + } + + @Override + public String environmentVariable() { + return "AWS_DEFAULT_REGION"; + } + + @Override + public String defaultValue() { + return null; + } + } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java index 3b8a348697a5..b002b82bb3cf 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java @@ -71,6 +71,28 @@ private static Stream regionResolutionCases() { ); } + @ParameterizedTest(name = "{0}") + @MethodSource("untrustedRegionCases") + void stsEndpoint_whenRegionHasSpecialCharacters_encodesThemIntoHost(String description, String region) { + System.setProperty(REGION_PROPERTY, region); + + URI endpoint = RegionEndpointResolver.create().stsEndpoint(); + + assertThat(endpoint.toString()).startsWith("https://sts.").endsWith(".amazonaws.com/"); + assertThat(endpoint.toString()).doesNotContain(region); + } + + private static Stream untrustedRegionCases() { + return Stream.of( + arguments("newline is encoded", "us-east-1\nfoo"), + arguments("carriage return is encoded", "us-east-1\rfoo"), + arguments("forward slash is encoded", "us-east-1/foo"), + arguments("space is encoded", "us-east-1 foo"), + arguments("at sign is encoded", "evil@host"), + arguments("hash is encoded", "host#fragment") + ); + } + @Test void stsEndpoint_whenImdsUnreachable_resolvesWithoutCallingImds() { // Point IMDS at a non-routable address. A resolver that (incorrectly) called IMDS would block here; diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java index 85ece31bd78c..cec55ec18d3a 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java @@ -21,12 +21,9 @@ 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.Assume.assumeTrue; import com.github.tomakehurst.wiremock.junit.WireMockRule; -import java.io.File; import java.net.URI; import java.util.Collections; import java.util.Iterator; @@ -45,7 +42,6 @@ */ public class SyncHttpClientWarmerIntegrationTest { - private static final int WARM_UP_CYCLES = 20; private static final int STS_REDIRECT_STATUS = 302; private static final String STS_REDIRECT_LOCATION = "https://aws.amazon.com/iam"; @@ -57,21 +53,6 @@ public void warmAll_sendsWarmUpRequestThroughApache() { assertWarmUpRequestIssued(new ApacheSdkHttpService()); } - @Test - public void warmAll_repeatedCycles_doNotLeakFileDescriptors() { - assumeTrue("FD check only runs where /proc/self/fd is available", openFdCountAvailable()); - stubStsRedirect(); - URI endpoint = endpoint(); - - long before = openFdCount(); - for (int i = 0; i < WARM_UP_CYCLES; i++) { - warmer(new ApacheSdkHttpService(), endpoint).warmAll(); - } - long after = openFdCount(); - - assertThat(after - before).isLessThan(WARM_UP_CYCLES); - } - @Test public void warmAll_whenServerUnreachable_swallowsAndDoesNotThrow() { int unusedPort = mockServer.port(); @@ -113,13 +94,4 @@ private void stubStsRedirect() { .withStatus(STS_REDIRECT_STATUS) .withHeader("Location", STS_REDIRECT_LOCATION))); } - - private static boolean openFdCountAvailable() { - return new File("/proc/self/fd").isDirectory(); - } - - private static long openFdCount() { - File[] fds = new File("/proc/self/fd").listFiles(); - return fds == null ? 0 : fds.length; - } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java index bb119debc3a3..d5600f9db1c1 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java @@ -52,8 +52,6 @@ class SyncHttpClientWarmerTest { private static final URI ENDPOINT = URI.create("https://sts.us-east-1.amazonaws.com/"); - // ---- per-client recipe (driven through warmAll) ---- - @Test void warmAll_whenResponseHasBody_drainsAndClosesIt() throws IOException { InputStream body = spy(new ByteArrayInputStream("denied".getBytes())); @@ -97,8 +95,6 @@ void warmAll_whenNoResponseBody_stillClosesClient() { verify(client).close(); } - // ---- discovery loop ---- - @Test void warmAll_whenMultipleServicesDiscovered_warmsEach() { SdkHttpClient first = stubClient(respondingWith(403, emptyBody())); @@ -126,8 +122,6 @@ void warmAll_whenNoServices_isNoOp() { assertThatCode(() -> warmer(Collections.emptyIterator()).warmAll()).doesNotThrowAnyException(); } - // ---- helpers ---- - private static SyncHttpClientWarmer warmer(SdkHttpService... services) { return warmer(Arrays.asList(services).iterator()); } From 072736dc1eec48e2bbd7f980d9dd65548fd63987 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Mon, 29 Jun 2026 12:22:14 -0700 Subject: [PATCH 5/7] Handled PR comments --- .brazil.json | 3 +- .github/workflows/dependency-cve-monitor.yml | 2 +- .../internal/crac/RegionEndpointResolver.java | 64 ++++------ .../core/internal/crac/WarmUpRequest.java | 71 ----------- .../http/loader/SyncHttpClientWarmer.java | 55 +++++---- .../crac/RegionEndpointResolverTest.java | 61 +++++----- .../SyncHttpClientWarmerIntegrationTest.java | 97 --------------- .../http/loader/SyncHttpClientWarmerTest.java | 21 +--- http-clients/apache5-client/pom.xml | 6 - .../apache5/Apache5HttpClientWarmUpTest.java | 52 -------- .../http/crt/AwsCrtHttpClientWarmUpTest.java | 52 -------- .../UrlConnectionHttpClientWarmUpTest.java | 52 -------- pom.xml | 1 + test/warmup-tests/pom.xml | 113 ++++++++++++++++++ .../http/warmup/SyncHttpClientWarmUpTest.java | 113 ++++++++++++++++++ 15 files changed, 323 insertions(+), 440 deletions(-) delete mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java delete mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java delete mode 100644 http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWarmUpTest.java delete mode 100644 http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWarmUpTest.java delete mode 100644 http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientWarmUpTest.java create mode 100644 test/warmup-tests/pom.xml create mode 100644 test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SyncHttpClientWarmUpTest.java diff --git a/.brazil.json b/.brazil.json index 69638fc11a0a..ad498cefb483 100644 --- a/.brazil.json +++ b/.brazil.json @@ -117,7 +117,8 @@ "crt-unavailable-tests": { "skipImport": true }, "bundle-shading-tests": { "skipImport": true }, "architecture-tests": {"skipImport": true}, - "s3-tests": {"skipImport": true} + "s3-tests": {"skipImport": true}, + "warmup-tests": { "skipImport": true } }, "dependencies": { diff --git a/.github/workflows/dependency-cve-monitor.yml b/.github/workflows/dependency-cve-monitor.yml index 22868167c539..69b95ec86c9a 100644 --- a/.github/workflows/dependency-cve-monitor.yml +++ b/.github/workflows/dependency-cve-monitor.yml @@ -21,7 +21,7 @@ jobs: maven-args: >- -DtransitiveExcludes=*:* -DclasspathScope=runtime - -pl !build-tools,!release-scripts,!archetypes,!test/test-utils,!test/sdk-benchmarks,!test/http-client-tests,!test/http-client-benchmarks,!test/s3-benchmarks,!test/protocol-tests-core,!test/ruleset-testing-core,!test/protocol-tests,!test/service-test-utils,!test/codegen-generated-classes-test,!test/sdk-standard-benchmarks,!test/module-path-tests,!test/tests-coverage-reporting,!test/stability-tests,!test/sdk-native-image-test,!test/auth-tests,!test/region-testing,!test/old-client-version-compatibility-test,!test/bundle-logging-bridge-binding-test,!test/v2-migration-tests,!test/bundle-shading-tests,!test/crt-unavailable-tests,!test/architecture-tests,!test/s3-tests + -pl !build-tools,!release-scripts,!archetypes,!test/test-utils,!test/sdk-benchmarks,!test/http-client-tests,!test/http-client-benchmarks,!test/s3-benchmarks,!test/protocol-tests-core,!test/ruleset-testing-core,!test/protocol-tests,!test/service-test-utils,!test/codegen-generated-classes-test,!test/sdk-standard-benchmarks,!test/module-path-tests,!test/tests-coverage-reporting,!test/stability-tests,!test/sdk-native-image-test,!test/auth-tests,!test/region-testing,!test/old-client-version-compatibility-test,!test/bundle-logging-bridge-binding-test,!test/v2-migration-tests,!test/bundle-shading-tests,!test/crt-unavailable-tests,!test/architecture-tests,!test/s3-tests,!test/warmup-tests notify-alerts: if: github.repository == 'aws/aws-sdk-java-v2' diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java index f3b5462582fa..fe7a71f5c3a8 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolver.java @@ -16,20 +16,19 @@ package software.amazon.awssdk.core.internal.crac; import java.net.URI; -import java.util.Optional; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.SdkSystemSetting; -import software.amazon.awssdk.utils.OptionalUtils; +import software.amazon.awssdk.utils.HostnameValidator; +import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.StringUtils; -import software.amazon.awssdk.utils.SystemSetting; -import software.amazon.awssdk.utils.http.SdkHttpUtils; -import software.amazon.awssdk.utils.internal.SystemSettingUtils; /** - * Resolves the regional STS endpoint ({@code https://sts..amazonaws.com/}) used by the CRaC HTTP-client warm-up. + * Resolves the endpoint used by the CRaC HTTP-client warm-up. The warm-up targets a single predetermined service; that service + * is currently STS, so this builds the regional STS endpoint ({@code https://sts..amazonaws.com/}). * - *

The region is taken from the first of: {@link SdkSystemSetting#AWS_REGION} (the {@code aws.region} system property or - * {@code AWS_REGION} environment variable), the {@code AWS_DEFAULT_REGION} environment variable, or {@value #DEFAULT_REGION}. + *

The region is taken from {@link SdkSystemSetting#AWS_REGION} (the {@code aws.region} system property or {@code AWS_REGION} + * environment variable), falling back to {@value #DEFAULT_REGION} when it is unset or not a valid hostname component. This + * matches how the SDK resolves a region from system settings. * *

Only system properties and environment variables are read. The full SDK region-resolution chain (IMDS, profile file) is * avoided during priming because those add network or filesystem calls that may fail or time out. The endpoint host always @@ -41,6 +40,8 @@ public final class RegionEndpointResolver { static final String DEFAULT_REGION = "us-east-1"; + private static final Logger log = Logger.loggerFor(RegionEndpointResolver.class); + private RegionEndpointResolver() { } @@ -51,41 +52,26 @@ public static RegionEndpointResolver create() { /** * @return the regional STS endpoint URI for the resolved region; never null. */ - public URI stsEndpoint() { - // URL-encode the region before putting it in the host, same as Region.of(String). - return URI.create("https://sts." + SdkHttpUtils.urlEncode(resolveRegion()) + ".amazonaws.com/"); + public URI endpoint() { + return URI.create("https://sts." + resolveRegion() + ".amazonaws.com/"); } private String resolveRegion() { - Optional awsRegion = trimmed(SdkSystemSetting.AWS_REGION.getStringValue()); - return OptionalUtils.firstPresent(awsRegion, RegionEndpointResolver::awsDefaultRegion) - .orElse(DEFAULT_REGION); - } - - private static Optional awsDefaultRegion() { - return trimmed(SystemSettingUtils.resolveEnvironmentVariable(new AwsDefaultRegionEnvVar())); - } - - private static Optional trimmed(Optional value) { - // trimToNull returns null for blank/empty input, so Optional.map collapses those to an empty Optional. - return value.map(StringUtils::trimToNull); - } - - // AWS_DEFAULT_REGION is an environment-variable-only fallback with no system-property equivalent. - private static final class AwsDefaultRegionEnvVar implements SystemSetting { - @Override - public String property() { - return null; + // trimToNull turns blank/empty into null so a blank AWS_REGION falls through to the default. + String awsRegion = SdkSystemSetting.AWS_REGION.getStringValue() + .map(StringUtils::trimToNull) + .orElse(null); + if (awsRegion == null) { + return DEFAULT_REGION; } - - @Override - public String environmentVariable() { - return "AWS_DEFAULT_REGION"; - } - - @Override - public String defaultValue() { - return null; + // A real region is a hostname-compliant token. Reject anything else so it cannot alter the endpoint host, and fall + // back to the default so the best-effort warm-up still runs. + try { + HostnameValidator.validateHostnameCompliant(awsRegion, "region", "AWS_REGION"); + return awsRegion; + } catch (IllegalArgumentException e) { + log.debug(() -> "Configured region is not a valid hostname component; using " + DEFAULT_REGION + " for warm-up.", e); + return DEFAULT_REGION; } } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java deleted file mode 100644 index 42aed4b83627..000000000000 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpRequest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.net.URI; -import java.util.Optional; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.ContentStreamProvider; -import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.http.SdkHttpRequest; - -/** - * Describes the request the CRaC warm-up sends to the resolved endpoint, and converts it to an {@link SdkHttpRequest} bound to - * that endpoint. The request is never signed and needs no credentials; only its execution (DNS, TLS, request/response I/O) - * matters for JIT priming. - */ -@SdkInternalApi -public final class WarmUpRequest { - - private final SdkHttpMethod method; - private final String body; - private final String contentType; - - private WarmUpRequest(SdkHttpMethod method, String body, String contentType) { - this.method = method; - this.body = body; - this.contentType = contentType; - } - - /** - * A bare {@code GET} to the endpoint, with no body. Warms DNS, the TLS handshake, and certificate-chain validation, which - * are the dominant cold-start costs and are shared across all services and operations. - */ - public static WarmUpRequest get() { - return new WarmUpRequest(SdkHttpMethod.GET, null, null); - } - - /** - * @retusrn this request as an {@link SdkHttpRequest} targeting {@code endpoint}. - */ - public SdkHttpRequest toHttpRequest(URI endpoint) { - SdkHttpRequest.Builder builder = SdkHttpRequest.builder() - .method(method) - .uri(endpoint); - if (body != null) { - builder.putHeader("Content-Type", contentType) - .putHeader("Content-Length", String.valueOf(body.getBytes(java.nio.charset.StandardCharsets.UTF_8).length)); - } - return builder.build(); - } - - /** - * @return the request body stream provider, or empty if this request has no body. - */ - public Optional contentStreamProvider() { - return body == null ? Optional.empty() : Optional.of(ContentStreamProvider.fromUtf8String(body)); - } -} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java index cf98ee40231e..9cac7044cb8b 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmer.java @@ -16,16 +16,18 @@ package software.amazon.awssdk.core.internal.http.loader; import java.net.URI; +import java.util.Collections; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.core.internal.crac.RegionEndpointResolver; import software.amazon.awssdk.core.internal.crac.WarmUpDiscovery; -import software.amazon.awssdk.core.internal.crac.WarmUpRequest; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.HttpExecuteResponse; import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.SdkHttpService; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.IoUtils; @@ -33,61 +35,68 @@ /** * Warms every sync {@link SdkHttpService} on the classpath for CRaC priming: builds each client and sends a best-effort - * {@link WarmUpRequest} to the resolved STS endpoint, draining the response body, so the HTTP/DNS/TLS/cert-chain code is - * JIT-compiled into the snapshot. - * - *

Lives in this package because the {@link SdkServiceLoader} it reuses is package-private. + * {@code GET} to the resolved STS endpoint, draining the response body, so the HTTP/DNS/TLS/cert-chain code is JIT-compiled + * into the snapshot. */ @SdkInternalApi public final class SyncHttpClientWarmer implements HttpClientWarmer { private static final Logger log = Logger.loggerFor(SyncHttpClientWarmer.class); - private final SdkServiceLoader serviceLoader; + private final Iterable services; private final Supplier endpointProvider; - private final WarmUpRequest warmUpRequest; @SdkTestInternalApi - SyncHttpClientWarmer(SdkServiceLoader serviceLoader, Supplier endpointProvider, WarmUpRequest warmUpRequest) { - this.serviceLoader = serviceLoader; + SyncHttpClientWarmer(Iterable services, Supplier endpointProvider) { + this.services = services; this.endpointProvider = endpointProvider; - this.warmUpRequest = warmUpRequest; } /** - * Discovers sync HTTP clients from the classpath as usual, but warms them against {@code endpointProvider} instead of the - * resolved STS endpoint, so a test can redirect the warm-up request to a local mock server. + * Warms a single {@code service} against {@code endpointProvider}. */ @SdkTestInternalApi - public SyncHttpClientWarmer(Supplier endpointProvider) { - this(SdkServiceLoader.INSTANCE, endpointProvider, WarmUpRequest.get()); + public static SyncHttpClientWarmer forService(Supplier endpointProvider, SdkHttpService service) { + return new SyncHttpClientWarmer(Collections.singletonList(service), endpointProvider); } public static SyncHttpClientWarmer create() { - return new SyncHttpClientWarmer(SdkServiceLoader.INSTANCE, - () -> RegionEndpointResolver.create().stsEndpoint(), - WarmUpRequest.get()); + return new SyncHttpClientWarmer(discoverServices(), () -> RegionEndpointResolver.create().endpoint()); + } + + /** + * Like {@link #create()}, but warms against {@code endpointProvider}. + */ + @SdkTestInternalApi + public static SyncHttpClientWarmer create(Supplier endpointProvider) { + return new SyncHttpClientWarmer(discoverServices(), endpointProvider); + } + + private static Iterable discoverServices() { + return () -> SdkServiceLoader.INSTANCE.loadServices(SdkHttpService.class); } @Override public void warmAll() { URI endpoint = endpointProvider.get(); - WarmUpDiscovery.forEachDiscovered(serviceLoader.loadServices(SdkHttpService.class), service -> { + WarmUpDiscovery.forEachDiscovered(services.iterator(), service -> { SdkHttpClient client = service.createHttpClientBuilder().buildWithDefaults(AttributeMap.empty()); warmClient(client, endpoint); }); } /** - * Sends the {@link WarmUpRequest} to {@code endpoint}, drains the response body (which warms the read and decode path), - * and closes the client. Never throws; the goal is JIT compilation, not a successful request. + * Sends the warm-up {@code GET} to {@code endpoint}, drains the response body, and closes the client. Best-effort: the + * goal is JIT compilation, not a successful request, so any failure is logged and swallowed. */ private void warmClient(SdkHttpClient client, URI endpoint) { try { + SdkHttpRequest httpRequest = SdkHttpRequest.builder() + .method(SdkHttpMethod.GET) + .uri(endpoint) + .build(); HttpExecuteRequest request = HttpExecuteRequest.builder() - .request(warmUpRequest.toHttpRequest(endpoint)) - .contentStreamProvider(warmUpRequest.contentStreamProvider() - .orElse(null)) + .request(httpRequest) .build(); ExecutableHttpRequest executableRequest = client.prepareRequest(request); HttpExecuteResponse response = executableRequest.call(); diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java index b002b82bb3cf..625093474a9c 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java @@ -50,58 +50,66 @@ void restoreSettings() { @ParameterizedTest(name = "{0}") @MethodSource("regionResolutionCases") - void stsEndpoint_whenRegionConfigured_resolvesExpectedEndpoint(String description, String sysprop, String awsRegion, - String awsDefaultRegion, String expectedRegion) { - applyRegionSettings(sysprop, awsRegion, awsDefaultRegion); + void endpoint_whenRegionConfigured_resolvesExpectedEndpoint(String description, String sysprop, String awsRegion, + String expectedRegion) { + applyRegionSettings(sysprop, awsRegion); - assertThat(RegionEndpointResolver.create().stsEndpoint()) + assertThat(RegionEndpointResolver.create().endpoint()) .isEqualTo(URI.create("https://sts." + expectedRegion + ".amazonaws.com/")); } private static Stream regionResolutionCases() { return Stream.of( - // description aws.region AWS_REGION AWS_DEFAULT_REGION expected - arguments("nothing set -> default region", null, null, null, "us-east-1"), - arguments("system property wins over both env vars", "eu-west-1", "ap-south-1", "us-west-2", "eu-west-1"), - arguments("AWS_REGION wins over AWS_DEFAULT_REGION", null, "ap-south-1", "us-west-2", "ap-south-1"), - arguments("AWS_DEFAULT_REGION used when nothing else set", null, null, "us-west-2", "us-west-2"), - arguments("blank system property falls through to AWS_REGION", " ", "ap-south-1", null, "ap-south-1"), - arguments("blank AWS_REGION falls through to AWS_DEFAULT_REGION", null, " ", "us-west-2", "us-west-2"), - arguments("chosen value is trimmed", " eu-central-1 ", null, null, "eu-central-1") + // description aws.region AWS_REGION expected + arguments("nothing set -> default region", null, null, "us-east-1"), + arguments("system property wins over AWS_REGION", "eu-west-1", "ap-south-1", "eu-west-1"), + arguments("AWS_REGION used when no system property", null, "ap-south-1", "ap-south-1"), + arguments("blank system property falls through to AWS_REGION", " ", "ap-south-1", "ap-south-1"), + arguments("blank AWS_REGION falls through to default", null, " ", "us-east-1"), + arguments("chosen value is trimmed", " eu-central-1 ", null, "eu-central-1") ); } + @Test + void endpoint_whenOnlyAwsDefaultRegionSet_ignoresItAndUsesDefault() { + ENV.set(AWS_DEFAULT_REGION_ENV, "us-west-2"); + + assertThat(RegionEndpointResolver.create().endpoint()) + .isEqualTo(URI.create("https://sts.us-east-1.amazonaws.com/")); + } + @ParameterizedTest(name = "{0}") @MethodSource("untrustedRegionCases") - void stsEndpoint_whenRegionHasSpecialCharacters_encodesThemIntoHost(String description, String region) { + void endpoint_whenRegionNotHostnameCompliant_fallsBackToDefault(String description, String region) { System.setProperty(REGION_PROPERTY, region); - URI endpoint = RegionEndpointResolver.create().stsEndpoint(); + URI endpoint = RegionEndpointResolver.create().endpoint(); - assertThat(endpoint.toString()).startsWith("https://sts.").endsWith(".amazonaws.com/"); - assertThat(endpoint.toString()).doesNotContain(region); + // A non-compliant region cannot reach the host; it is rejected and the default region is used instead. + assertThat(endpoint).isEqualTo(URI.create("https://sts.us-east-1.amazonaws.com/")); } private static Stream untrustedRegionCases() { return Stream.of( - arguments("newline is encoded", "us-east-1\nfoo"), - arguments("carriage return is encoded", "us-east-1\rfoo"), - arguments("forward slash is encoded", "us-east-1/foo"), - arguments("space is encoded", "us-east-1 foo"), - arguments("at sign is encoded", "evil@host"), - arguments("hash is encoded", "host#fragment") + arguments("newline is rejected", "us-east-1\nfoo"), + arguments("carriage return is rejected", "us-east-1\rfoo"), + arguments("forward slash is rejected", "us-east-1/foo"), + arguments("space is rejected", "us-east-1 foo"), + arguments("at sign is rejected", "evil@host"), + arguments("hash is rejected", "host#fragment"), + arguments("dot is rejected", "sts.evil.com") ); } @Test - void stsEndpoint_whenImdsUnreachable_resolvesWithoutCallingImds() { + void endpoint_whenImdsUnreachable_resolvesWithoutCallingImds() { // Point IMDS at a non-routable address. A resolver that (incorrectly) called IMDS would block here; // the correct resolver ignores IMDS entirely and returns the default region immediately. String savedImdsEndpoint = System.getProperty("aws.ec2MetadataServiceEndpoint"); System.setProperty("aws.ec2MetadataServiceEndpoint", "http://10.255.255.1"); try { long startNanos = System.nanoTime(); - URI endpoint = RegionEndpointResolver.create().stsEndpoint(); + URI endpoint = RegionEndpointResolver.create().endpoint(); long elapsedMillis = (System.nanoTime() - startNanos) / 1_000_000; assertThat(endpoint).isEqualTo(URI.create("https://sts.us-east-1.amazonaws.com/")); @@ -115,15 +123,12 @@ void stsEndpoint_whenImdsUnreachable_resolvesWithoutCallingImds() { } } - private static void applyRegionSettings(String sysprop, String awsRegion, String awsDefaultRegion) { + private static void applyRegionSettings(String sysprop, String awsRegion) { if (sysprop != null) { System.setProperty(REGION_PROPERTY, sysprop); } if (awsRegion != null) { ENV.set(AWS_REGION_ENV, awsRegion); } - if (awsDefaultRegion != null) { - ENV.set(AWS_DEFAULT_REGION_ENV, awsDefaultRegion); - } } } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java deleted file mode 100644 index cec55ec18d3a..000000000000 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerIntegrationTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.http.loader; - -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.assertThatCode; - -import com.github.tomakehurst.wiremock.junit.WireMockRule; -import java.net.URI; -import java.util.Collections; -import java.util.Iterator; -import java.util.function.Supplier; -import org.junit.Rule; -import org.junit.Test; -import software.amazon.awssdk.core.internal.crac.WarmUpRequest; -import software.amazon.awssdk.http.SdkHttpService; -import software.amazon.awssdk.http.apache.ApacheSdkHttpService; - -/** - * Integration tests that run {@link SyncHttpClientWarmer} against a real HTTP client and a WireMock server. - * - *

WireMock returns a {@code 302} redirect with an empty body, like a real {@code GET} to an STS endpoint. The warm-up - * must not follow the redirect, so exactly one request is expected. - */ -public class SyncHttpClientWarmerIntegrationTest { - - private static final int STS_REDIRECT_STATUS = 302; - private static final String STS_REDIRECT_LOCATION = "https://aws.amazon.com/iam"; - - @Rule - public WireMockRule mockServer = new WireMockRule(0); - - @Test - public void warmAll_sendsWarmUpRequestThroughApache() { - assertWarmUpRequestIssued(new ApacheSdkHttpService()); - } - - @Test - public void warmAll_whenServerUnreachable_swallowsAndDoesNotThrow() { - int unusedPort = mockServer.port(); - mockServer.stop(); - URI endpoint = URI.create("http://localhost:" + unusedPort + "/"); - - assertThatCode(() -> warmer(new ApacheSdkHttpService(), endpoint).warmAll()).doesNotThrowAnyException(); - } - - private void assertWarmUpRequestIssued(SdkHttpService service) { - stubStsRedirect(); - - warmer(service, endpoint()).warmAll(); - - // One GET to "/", and nothing else: the warm-up issued the GET and did not follow the redirect. - mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); - mockServer.verify(1, anyRequestedFor(anyUrl())); - } - - // Warmer pinned to a single real service, warmed against the given (WireMock) endpoint. - private static SyncHttpClientWarmer warmer(SdkHttpService service, URI endpoint) { - SdkServiceLoader loader = new SdkServiceLoader() { - @Override - @SuppressWarnings("unchecked") - Iterator loadServices(Class clazz) { - return (Iterator) Collections.singletonList(service).iterator(); - } - }; - Supplier endpointProvider = () -> endpoint; - return new SyncHttpClientWarmer(loader, endpointProvider, WarmUpRequest.get()); - } - - private URI endpoint() { - return URI.create("http://localhost:" + mockServer.port() + "/"); - } - - private void stubStsRedirect() { - mockServer.stubFor(any(anyUrl()).willReturn(aResponse() - .withStatus(STS_REDIRECT_STATUS) - .withHeader("Location", STS_REDIRECT_LOCATION))); - } -} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java index d5600f9db1c1..438d5beb9760 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/SyncHttpClientWarmerTest.java @@ -29,11 +29,8 @@ import java.io.InputStream; import java.net.URI; import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.core.internal.crac.WarmUpRequest; import software.amazon.awssdk.http.AbortableInputStream; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; @@ -45,8 +42,7 @@ /** * Unit tests for {@link SyncHttpClientWarmer}. Every test drives the real {@link SyncHttpClientWarmer#warmAll()} with an - * injected fake {@link SdkServiceLoader} (supplying stub {@link SdkHttpService}s) and a fixed endpoint supplier (standing in - * for the resolved STS host). + * injected list of stub {@link SdkHttpService}s and a fixed endpoint. */ class SyncHttpClientWarmerTest { @@ -119,22 +115,11 @@ void warmAll_whenOneServiceFailsToBuild_stillWarmsOthers() { @Test void warmAll_whenNoServices_isNoOp() { - assertThatCode(() -> warmer(Collections.emptyIterator()).warmAll()).doesNotThrowAnyException(); + assertThatCode(() -> warmer().warmAll()).doesNotThrowAnyException(); } private static SyncHttpClientWarmer warmer(SdkHttpService... services) { - return warmer(Arrays.asList(services).iterator()); - } - - private static SyncHttpClientWarmer warmer(Iterator services) { - SdkServiceLoader loader = new SdkServiceLoader() { - @Override - @SuppressWarnings("unchecked") - Iterator loadServices(Class clazz) { - return (Iterator) services; - } - }; - return new SyncHttpClientWarmer(loader, () -> ENDPOINT, WarmUpRequest.get()); + return new SyncHttpClientWarmer(Arrays.asList(services), () -> ENDPOINT); } /** A service whose builder yields the given client. */ diff --git a/http-clients/apache5-client/pom.xml b/http-clients/apache5-client/pom.xml index 1d01c51b4fb8..d95b9cf7f6c6 100644 --- a/http-clients/apache5-client/pom.xml +++ b/http-clients/apache5-client/pom.xml @@ -70,12 +70,6 @@ ${awsjavasdk.version} test - - software.amazon.awssdk - sdk-core - ${awsjavasdk.version} - test - org.apache.logging.log4j diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWarmUpTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWarmUpTest.java deleted file mode 100644 index ed21065948e2..000000000000 --- a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientWarmUpTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.apache5; - -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 com.github.tomakehurst.wiremock.junit.WireMockRule; -import java.net.URI; -import org.junit.Rule; -import org.junit.Test; -import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; - -/** - * Verifies the CRaC sync warm-up sends its GET through the real {@code Apache5HttpClient} discovered on the classpath. - * The stub mirrors a real STS {@code GET} (302 redirect, empty body); the warm-up must not follow the redirect. - */ -public class Apache5HttpClientWarmUpTest { - - @Rule - public WireMockRule mockServer = new WireMockRule(0); - - @Test - public void warmAll_sendsWarmUpGetThroughApache5Client() { - mockServer.stubFor(any(anyUrl()).willReturn(aResponse() - .withStatus(302) - .withHeader("Location", "https://aws.amazon.com/iam"))); - - URI endpoint = URI.create("http://localhost:" + mockServer.port() + "/"); - new SyncHttpClientWarmer(() -> endpoint).warmAll(); - - mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); - mockServer.verify(1, anyRequestedFor(anyUrl())); - } -} diff --git a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWarmUpTest.java b/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWarmUpTest.java deleted file mode 100644 index a5065fe6a724..000000000000 --- a/http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWarmUpTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.crt; - -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 com.github.tomakehurst.wiremock.junit.WireMockRule; -import java.net.URI; -import org.junit.Rule; -import org.junit.Test; -import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; - -/** - * Verifies the CRaC sync warm-up sends its GET through the real CRT sync client discovered on the classpath. The stub mirrors - * a real STS {@code GET} (302 redirect, empty body); the warm-up must not follow the redirect. - */ -public class AwsCrtHttpClientWarmUpTest { - - @Rule - public WireMockRule mockServer = new WireMockRule(0); - - @Test - public void warmAll_sendsWarmUpGetThroughCrtClient() { - mockServer.stubFor(any(anyUrl()).willReturn(aResponse() - .withStatus(302) - .withHeader("Location", "https://aws.amazon.com/iam"))); - - URI endpoint = URI.create("http://localhost:" + mockServer.port() + "/"); - new SyncHttpClientWarmer(() -> endpoint).warmAll(); - - mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); - mockServer.verify(1, anyRequestedFor(anyUrl())); - } -} diff --git a/http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientWarmUpTest.java b/http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientWarmUpTest.java deleted file mode 100644 index 3ef2f27b0923..000000000000 --- a/http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientWarmUpTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.urlconnection; - -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 com.github.tomakehurst.wiremock.junit.WireMockRule; -import java.net.URI; -import org.junit.Rule; -import org.junit.Test; -import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; - -/** - * Verifies the CRaC sync warm-up sends its GET through the real {@code UrlConnectionHttpClient} discovered on the classpath. - * The stub mirrors a real STS {@code GET} (302 redirect, empty body); the warm-up must not follow the redirect. - */ -public class UrlConnectionHttpClientWarmUpTest { - - @Rule - public WireMockRule mockServer = new WireMockRule(0); - - @Test - public void warmAll_sendsWarmUpGetThroughUrlConnectionClient() { - mockServer.stubFor(any(anyUrl()).willReturn(aResponse() - .withStatus(302) - .withHeader("Location", "https://aws.amazon.com/iam"))); - - URI endpoint = URI.create("http://localhost:" + mockServer.port() + "/"); - new SyncHttpClientWarmer(() -> endpoint).warmAll(); - - mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); - mockServer.verify(1, anyRequestedFor(anyUrl())); - } -} diff --git a/pom.xml b/pom.xml index f93fdc49fe26..69b309991d10 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ third-party v2-migration test/http-client-tests + test/warmup-tests test/protocol-tests test/protocol-tests-core test/service-test-utils diff --git a/test/warmup-tests/pom.xml b/test/warmup-tests/pom.xml new file mode 100644 index 000000000000..1f898dd20ae4 --- /dev/null +++ b/test/warmup-tests/pom.xml @@ -0,0 +1,113 @@ + + + + + 4.0.0 + + aws-sdk-java-pom + software.amazon.awssdk + 2.46.18-SNAPSHOT + ../../pom.xml + + warmup-tests + AWS Java SDK :: Test :: Warm-up Tests + Centralized tests for the CRaC HTTP-client warm-up, run against every sync HTTP client so each client does not + need its own warm-up test. This is a leaf test module: nothing depends on it, so depending on the HTTP clients here + does not create a dependency cycle. + https://aws.amazon.com/sdkforjava + + + + + software.amazon.awssdk + bom-internal + ${project.version} + pom + import + + + + + + + software.amazon.awssdk + sdk-core + ${awsjavasdk.version} + test + + + software.amazon.awssdk + apache-client + ${awsjavasdk.version} + test + + + software.amazon.awssdk + apache5-client + ${awsjavasdk.version} + test + + + software.amazon.awssdk + aws-crt-client + ${awsjavasdk.version} + test + + + software.amazon.awssdk + url-connection-client + ${awsjavasdk.version} + test + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + com.github.tomakehurst + wiremock-jre8-standalone + test + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + + true + + + + org.apache.maven.plugins + maven-dependency-plugin + + true + + + + + diff --git a/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SyncHttpClientWarmUpTest.java b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SyncHttpClientWarmUpTest.java new file mode 100644 index 000000000000..2f62733bad83 --- /dev/null +++ b/test/warmup-tests/src/test/java/software/amazon/awssdk/http/warmup/SyncHttpClientWarmUpTest.java @@ -0,0 +1,113 @@ +/* + * 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 com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.URI; +import java.util.ServiceLoader; +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.MethodSource; +import software.amazon.awssdk.core.internal.http.loader.SyncHttpClientWarmer; +import software.amazon.awssdk.http.SdkHttpService; +import software.amazon.awssdk.http.apache.ApacheSdkHttpService; +import software.amazon.awssdk.http.apache5.Apache5SdkHttpService; +import software.amazon.awssdk.http.crt.AwsCrtSdkHttpService; +import software.amazon.awssdk.http.urlconnection.UrlConnectionSdkHttpService; + +/** + * Verifies the CRaC sync warm-up sends its GET through each real sync HTTP client. Every client is exercised here, in one + * place, instead of a separate test in each client module. The stub mirrors a real STS {@code GET} (302 redirect, empty + * body); the warm-up must not follow the redirect, so exactly one request is expected. + */ +class SyncHttpClientWarmUpTest { + + // The sync HTTP clients on this module's classpath: apache, apache5, aws-crt, url-connection. Hardcoded so a broken + // ServiceLoader that discovers nothing fails the test instead of passing a trivial verify(0). + private static final int SYNC_CLIENT_COUNT = 4; + + private WireMockServer mockServer; + + static SdkHttpService[] syncClients() { + return new SdkHttpService[] { + new Apache5SdkHttpService(), + new ApacheSdkHttpService(), + new AwsCrtSdkHttpService(), + new UrlConnectionSdkHttpService() + }; + } + + @BeforeEach + void setUp() { + mockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + mockServer.start(); + } + + @AfterEach + void tearDown() { + mockServer.stop(); + } + + @ParameterizedTest + @MethodSource("syncClients") + void warmAll_sendsWarmUpGetThroughClient(SdkHttpService service) { + mockServer.stubFor(any(anyUrl()).willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "https://aws.amazon.com/iam"))); + + URI endpoint = URI.create("http://localhost:" + mockServer.port() + "/"); + SyncHttpClientWarmer.forService(() -> endpoint, service).warmAll(); + + mockServer.verify(1, getRequestedFor(urlPathEqualTo("/"))); + mockServer.verify(1, anyRequestedFor(anyUrl())); + } + + /** + * Exercises the real classpath-discovery path used by {@code prime()}: {@code warmAll()} discovers every sync + * {@link SdkHttpService} on the classpath via {@link ServiceLoader} and warms each. Confirms discovery finds all + * {@value #SYNC_CLIENT_COUNT} clients and that each receives exactly one warm-up GET. + */ + @Test + void warmAll_whenDiscoveringFromClasspath_warmsEverySyncClient() { + mockServer.stubFor(any(anyUrl()).willReturn(aResponse() + .withStatus(302) + .withHeader("Location", "https://aws.amazon.com/iam"))); + + int discoveredClients = 0; + for (SdkHttpService ignored : ServiceLoader.load(SdkHttpService.class)) { + discoveredClients++; + } + // Guard against a broken ServiceLoader: if discovery silently finds 0, verify(0) below would pass trivially. + assertThat(discoveredClients).isEqualTo(SYNC_CLIENT_COUNT); + + URI endpoint = URI.create("http://localhost:" + mockServer.port() + "/"); + SyncHttpClientWarmer.create(() -> endpoint).warmAll(); + + mockServer.verify(SYNC_CLIENT_COUNT, getRequestedFor(urlPathEqualTo("/"))); + mockServer.verify(SYNC_CLIENT_COUNT, anyRequestedFor(anyUrl())); + } +} From 9d6a9d55d4dea72a007c5c65cd7f3b9ac05434c1 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Mon, 29 Jun 2026 15:13:17 -0700 Subject: [PATCH 6/7] Update test for blank input --- .../awssdk/core/internal/crac/RegionEndpointResolverTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java index 625093474a9c..f1c5a5bcc46a 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java @@ -64,7 +64,7 @@ private static Stream regionResolutionCases() { arguments("nothing set -> default region", null, null, "us-east-1"), arguments("system property wins over AWS_REGION", "eu-west-1", "ap-south-1", "eu-west-1"), arguments("AWS_REGION used when no system property", null, "ap-south-1", "ap-south-1"), - arguments("blank system property falls through to AWS_REGION", " ", "ap-south-1", "ap-south-1"), + arguments("blank system property short-circuits AWS_REGION", " ", "ap-south-1", "us-east-1"), arguments("blank AWS_REGION falls through to default", null, " ", "us-east-1"), arguments("chosen value is trimmed", " eu-central-1 ", null, "eu-central-1") ); From 131e1570fb177acd765840e2d7752d74e5c5fced Mon Sep 17 00:00:00 2001 From: John Viegas Date: Mon, 29 Jun 2026 16:59:57 -0700 Subject: [PATCH 7/7] Update before and after conditions in test --- .../core/internal/crac/RegionEndpointResolverTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java index f1c5a5bcc46a..19e2a5e4c04b 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/RegionEndpointResolverTest.java @@ -38,13 +38,18 @@ class RegionEndpointResolverTest { @BeforeEach void clearSettings() { - ENV.reset(); - System.clearProperty(REGION_PROPERTY); + clearRegionSettings(); } @AfterEach void restoreSettings() { + clearRegionSettings(); + } + + private void clearRegionSettings() { ENV.reset(); + ENV.remove(AWS_REGION_ENV); + ENV.remove(AWS_DEFAULT_REGION_ENV); System.clearProperty(REGION_PROPERTY); }