diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/AsyncHttpClientWarmer.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/AsyncHttpClientWarmer.java index 226024495e58..ae88b61a8010 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/AsyncHttpClientWarmer.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/AsyncHttpClientWarmer.java @@ -86,9 +86,10 @@ private static Iterable discoverServices() { @Override public void warmAll() { URI endpoint = endpointProvider.get(); + String userAgent = HttpClientWarmer.warmUpUserAgent(); WarmUpDiscovery.forEachDiscovered(services.iterator(), service -> { SdkAsyncHttpClient client = service.createAsyncHttpClientFactory().buildWithDefaults(AttributeMap.empty()); - warmClient(client, endpoint); + warmClient(client, endpoint, userAgent); }); } @@ -97,11 +98,12 @@ public void warmAll() { * failure or timeout is logged and swallowed. We block on the execute future (bounded) because the bundled async * clients complete it only after the body is drained, so its completion implies the full path was exercised. */ - private void warmClient(SdkAsyncHttpClient client, URI endpoint) { + private void warmClient(SdkAsyncHttpClient client, URI endpoint, String userAgent) { try { SdkHttpFullRequest httpRequest = SdkHttpFullRequest.builder() .method(SdkHttpMethod.GET) .uri(endpoint) + .putHeader(HEADER_USER_AGENT, userAgent) .build(); AsyncExecuteRequest request = AsyncExecuteRequest.builder() .request(httpRequest) 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 e63b7d62dec2..04ad65f3b97b 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 @@ -15,7 +15,12 @@ package software.amazon.awssdk.core.internal.http.loader; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.FEATURE_METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.appendSpaceAndField; + import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder; +import software.amazon.awssdk.core.util.SystemUserAgent; /** * Warms the sync or async HTTP clients on the classpath for CRaC priming. @@ -23,8 +28,23 @@ @SdkInternalApi public interface HttpClientWarmer { + String HEADER_USER_AGENT = "User-Agent"; + + String WARM_UP_FEATURE_ID = "warmup"; + /** * Warms every HTTP client found on the classpath. Best-effort; never throws. */ void warmAll(); + + /** + * Builds the {@code User-Agent} header value for warm-up requests: the system user agent (SDK version, Java version, OS, + * etc.) plus the {@link #WARM_UP_FEATURE_ID} feature marker. + */ + static String warmUpUserAgent() { + StringBuilder uaString = + new StringBuilder(SdkUserAgentBuilder.buildSystemUserAgentString(SystemUserAgent.getOrCreate())); + appendSpaceAndField(uaString, FEATURE_METADATA, WARM_UP_FEATURE_ID); + return uaString.toString(); + } } 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 39284529d45c..4f24d7788824 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 @@ -79,9 +79,10 @@ private static Iterable discoverServices() { @Override public void warmAll() { URI endpoint = endpointProvider.get(); + String userAgent = HttpClientWarmer.warmUpUserAgent(); WarmUpDiscovery.forEachDiscovered(services.iterator(), service -> { SdkHttpClient client = service.createHttpClientBuilder().buildWithDefaults(AttributeMap.empty()); - warmClient(client, endpoint); + warmClient(client, endpoint, userAgent); }); } @@ -89,11 +90,12 @@ public void warmAll() { * 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) { + private void warmClient(SdkHttpClient client, URI endpoint, String userAgent) { try { SdkHttpRequest httpRequest = SdkHttpRequest.builder() .method(SdkHttpMethod.GET) .uri(endpoint) + .putHeader(HEADER_USER_AGENT, userAgent) .build(); HttpExecuteRequest request = HttpExecuteRequest.builder() .request(httpRequest) diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/AsyncHttpClientWarmerTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/AsyncHttpClientWarmerTest.java index 9ca0343e6328..484ddd2f1e39 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/AsyncHttpClientWarmerTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/loader/AsyncHttpClientWarmerTest.java @@ -77,6 +77,18 @@ void warmAll_whenInvoked_issuesGetToResolvedEndpoint() { assertThat(request.getValue().request().getUri()).isEqualTo(ENDPOINT); } + @Test + void warmAll_whenInvoked_addsWarmUpUserAgentHeader() { + SdkAsyncHttpClient client = stubClient(emptyBody()); + ArgumentCaptor request = ArgumentCaptor.forClass(AsyncExecuteRequest.class); + + warmer(serviceFor(client)).warmAll(); + + verify(client).execute(request.capture()); + assertThat(request.getValue().request().firstMatchingHeader("User-Agent")) + .hasValueSatisfying(userAgent -> assertThat(userAgent).contains("aws-sdk-java/2.").contains("ft/warmup")); + } + @Test void warmAll_whenRequestFails_swallowsAndStillClosesClient() { SdkAsyncHttpClient client = mock(SdkAsyncHttpClient.class); 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 438d5beb9760..228e98ba42ae 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 @@ -72,6 +72,18 @@ void warmAll_whenInvoked_issuesGetToResolvedEndpoint() { assertThat(request.getValue().httpRequest().getUri()).isEqualTo(ENDPOINT); } + @Test + void warmAll_whenInvoked_addsWarmUpUserAgentHeader() { + 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().firstMatchingHeader("User-Agent")) + .hasValueSatisfying(userAgent -> assertThat(userAgent).contains("aws-sdk-java/2.").contains("ft/warmup")); + } + @Test void warmAll_whenRequestFails_swallowsAndStillClosesClient() throws IOException { SdkHttpClient client = mock(SdkHttpClient.class);