From 5783899c57d1273d4b935d105f5412f9f4a9b099 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 19 Jun 2026 10:47:00 -0700 Subject: [PATCH 1/4] feat(sdk-core): add SdkWarmUp.prime() for CRaC auto-priming --- .../amazon/awssdk/core/crac/SdkWarmUp.java | 64 ++++++ .../internal/crac/ClasspathWarmUpInvoker.java | 76 +++++++ .../core/internal/crac/WarmUpInvoker.java | 34 ++++ .../internal/crac/WarmUpServiceLoader.java | 36 ++++ .../core/crac/CountingWarmUpProvider.java | 33 +++ .../awssdk/core/crac/SdkWarmUpTest.java | 66 ++++++ .../crac/ClasspathWarmUpInvokerTest.java | 191 ++++++++++++++++++ ....amazon.awssdk.core.crac.SdkWarmUpProvider | 15 ++ 8 files changed, 515 insertions(+) create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvoker.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpInvoker.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpServiceLoader.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/CountingWarmUpProvider.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvokerTest.java create mode 100644 core/sdk-core/src/test/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider 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 new file mode 100644 index 000000000000..8657c9ed9432 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/crac/SdkWarmUp.java @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.core.crac; + +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicBoolean; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker; + +/** + * Entry point for warming up SDK service request paths before a Coordinated Restore at Checkpoint (CRaC) + * checkpoint. + * + *

{@link #prime()} discovers every {@link SdkWarmUpProvider} registered on the classpath through {@link + * ServiceLoader} (via the {@code META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider} + * resource) and invokes {@link SdkWarmUpProvider#warmUp()} on each. + * + *

Behavior contract: + *

+ * + *

Call this once during application initialization, before a CRaC checkpoint is taken. + */ +@ThreadSafe +@SdkPublicApi +public final class SdkWarmUp { + + private static final AtomicBoolean PRIMED = new AtomicBoolean(false); + + private SdkWarmUp() { + } + + /** + * Discovers every {@link SdkWarmUpProvider} on the classpath and invokes {@link SdkWarmUpProvider#warmUp()} + * on each, honoring the idempotency, per-provider resilience, and empty-classpath behavior described on + * this class. Safe to call concurrently. + */ + public static void prime() { + if (!PRIMED.compareAndSet(false, true)) { + return; + } + + ClasspathWarmUpInvoker.create().invokeAll(); + } +} 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 new file mode 100644 index 000000000000..4624b4fdeb14 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvoker.java @@ -0,0 +1,76 @@ +/* + * 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.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} + * implementations on the classpath and invokes {@code warmUp()} on every one of them. + */ +@SdkInternalApi +public final class ClasspathWarmUpInvoker implements WarmUpInvoker { + + private static final Logger log = Logger.loggerFor(ClasspathWarmUpInvoker.class); + + private final WarmUpServiceLoader serviceLoader; + + @SdkTestInternalApi + ClasspathWarmUpInvoker(WarmUpServiceLoader serviceLoader) { + this.serviceLoader = serviceLoader; + } + + @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."); + } + } + + /** + * @return ClasspathWarmUpInvoker that discovers {@link SdkWarmUpProvider}s from the classpath. + */ + public static WarmUpInvoker create() { + return new ClasspathWarmUpInvoker(WarmUpServiceLoader.INSTANCE); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpInvoker.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpInvoker.java new file mode 100644 index 000000000000..3bd127ea6c09 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpInvoker.java @@ -0,0 +1,34 @@ +/* + * 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 software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; + +/** + * Discovers {@link SdkWarmUpProvider}s and invokes their warm-up behind the public {@code SdkWarmUp.prime()}. + * Mirrors the {@code SdkHttpServiceProvider} loader abstraction, except warm-up invokes every discovered + * provider rather than selecting one. + */ +@SdkInternalApi +public interface WarmUpInvoker { + + /** + * Invokes {@link SdkWarmUpProvider#warmUp()} on every discovered provider, containing per-provider failures + * so one failing provider does not stop the others. + */ + void invokeAll(); +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpServiceLoader.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpServiceLoader.java new file mode 100644 index 000000000000..9ea651d6a119 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/crac/WarmUpServiceLoader.java @@ -0,0 +1,36 @@ +/* + * 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.ServiceLoader; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.core.internal.util.ClassLoaderHelper; + +/** + * Thin layer over {@link ServiceLoader} for {@link SdkWarmUpProvider}. + */ +@SdkInternalApi +class WarmUpServiceLoader { + + public static final WarmUpServiceLoader INSTANCE = new WarmUpServiceLoader(); + + Iterator loadProviders() { + return ServiceLoader.load(SdkWarmUpProvider.class, + ClassLoaderHelper.classLoader(WarmUpServiceLoader.class)).iterator(); + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/CountingWarmUpProvider.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/CountingWarmUpProvider.java new file mode 100644 index 000000000000..fdbd2f3b4844 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/CountingWarmUpProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.core.crac; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Test-only {@link SdkWarmUpProvider} registered via test-scoped {@code META-INF/services} so the real static + * {@code SdkWarmUp.prime()} discovers and invokes it through {@link java.util.ServiceLoader}. It counts how many + * times {@code warmUp()} is invoked across the JVM. ServiceLoader requires a public no-arg constructor. + */ +public final class CountingWarmUpProvider implements SdkWarmUpProvider { + + public static final AtomicInteger INVOCATIONS = new AtomicInteger(); + + @Override + public void warmUp() { + INVOCATIONS.incrementAndGet(); + } +} 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 new file mode 100644 index 000000000000..b836b36eac7a --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/SdkWarmUpTest.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.crac; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.Test; + +/** + * Tests for the static {@link SdkWarmUp#prime()} entry point, exercised end to end through {@link + * java.util.ServiceLoader} with a test-scoped {@code META-INF/services} registration of {@link + * CountingWarmUpProvider}. + * + *

{@code prime()} runs at most once per JVM and there is no reset hook, so assertions here are + * order-independent: regardless of how many threads call {@code prime()} (and regardless of any other test in + * this JVM having already called it), the counting provider is invoked exactly once in total. + */ +class SdkWarmUpTest { + + @Test + void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws InterruptedException { + int threadCount = 16; + CountDownLatch start = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(threadCount); + List threads = new ArrayList<>(); + + for (int i = 0; i < threadCount; i++) { + Thread thread = new Thread(() -> { + try { + start.await(); + SdkWarmUp.prime(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + done.countDown(); + } + }); + threads.add(thread); + thread.start(); + } + + start.countDown(); + done.await(); + for (Thread thread : threads) { + thread.join(); + } + + assertThat(CountingWarmUpProvider.INVOCATIONS.get()).isEqualTo(1); + } +} 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 new file mode 100644 index 000000000000..27dd50a20157 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/crac/ClasspathWarmUpInvokerTest.java @@ -0,0 +1,191 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.core.internal.crac; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import software.amazon.awssdk.core.crac.SdkWarmUpProvider; +import software.amazon.awssdk.testutils.LogCaptor; + +/** + * Unit tests for {@link ClasspathWarmUpInvoker}. The {@link WarmUpServiceLoader} is stubbed so each test injects + * the providers it discovers, mirroring how {@code ClasspathSdkHttpServiceProviderTest} drives the loader. These + * containment and log-level scenarios cannot run through the static, run-once {@code SdkWarmUp.prime()}. + */ +class ClasspathWarmUpInvokerTest { + + @Test + void invokeAll_invokesEveryProviderOnce() { + CountingProvider first = new CountingProvider(); + CountingProvider second = new CountingProvider(); + CountingProvider third = new CountingProvider(); + + invokerLoading(first, second, third).invokeAll(); + + assertThat(first.invocations()).isEqualTo(1); + assertThat(second.invocations()).isEqualTo(1); + assertThat(third.invocations()).isEqualTo(1); + } + + @Test + void invokeAll_whenOneProviderThrows_stillInvokesOthers() { + CountingProvider before = new CountingProvider(); + SdkWarmUpProvider throwing = () -> { + throw new RuntimeException("boom"); + }; + CountingProvider after = new CountingProvider(); + + WarmUpInvoker invoker = invokerLoading(before, throwing, after); + + assertThatCode(invoker::invokeAll).doesNotThrowAnyException(); + assertThat(before.invocations()).isEqualTo(1); + assertThat(after.invocations()).isEqualTo(1); + } + + @Test + void invokeAll_whenProviderFailsToLoad_stillInvokesOthers(@TempDir Path tempDir) throws IOException { + RealProvider.INVOCATIONS.set(0); + // A real ServiceLoader over a registration that lists a class that does not exist, followed by a real + // provider. This proves ServiceLoader advances past the unloadable entry so the real one still runs. + WarmUpInvoker invoker = invokerLoading(realServiceLoader(tempDir, + "com.example.DoesNotExistProvider", + RealProvider.class.getName())); + + assertThatCode(invoker::invokeAll).doesNotThrowAnyException(); + assertThat(RealProvider.INVOCATIONS.get()).isEqualTo(1); + } + + @Test + void invokeAll_whenNoProviders_isNoOp() { + WarmUpInvoker invoker = invokerLoading(Collections.emptyIterator()); + assertThatCode(invoker::invokeAll).doesNotThrowAnyException(); + } + + @Test + void invokeAll_whenProviderThrows_logsAtWarn() { + SdkWarmUpProvider throwing = () -> { + throw new RuntimeException("boom"); + }; + + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + invokerLoading(throwing).invokeAll(); + + assertThat(logCaptor.loggedEvents()) + .filteredOn(loggedFromInvoker()) + .anyMatch(event -> event.getLevel() == Level.WARN + && event.getMessage().getFormattedMessage().contains("failed during warmUp()")); + } + } + + @Test + void invokeAll_whenProviderFailsToLoad_logsAtWarn(@TempDir Path tempDir) throws IOException { + WarmUpInvoker invoker = invokerLoading(realServiceLoader(tempDir, "com.example.DoesNotExistProvider")); + + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + invoker.invokeAll(); + + assertThat(logCaptor.loggedEvents()) + .filteredOn(loggedFromInvoker()) + .anyMatch(event -> event.getLevel() == Level.WARN + && event.getMessage().getFormattedMessage().contains("could not be loaded")); + } + } + + @Test + void invokeAll_whenNoProviders_logsAtDebug() { + try (LogCaptor logCaptor = LogCaptor.create(Level.DEBUG)) { + invokerLoading(Collections.emptyIterator()).invokeAll(); + + assertThat(logCaptor.loggedEvents()) + .filteredOn(loggedFromInvoker()) + .anyMatch(event -> event.getLevel() == Level.DEBUG + && event.getMessage().getFormattedMessage().contains("No SdkWarmUpProvider")); + } + } + + private static Predicate loggedFromInvoker() { + return event -> ClasspathWarmUpInvoker.class.getName().equals(event.getLoggerName()); + } + + private WarmUpInvoker invokerLoading(SdkWarmUpProvider... providers) { + return invokerLoading(Arrays.asList(providers).iterator()); + } + + private WarmUpInvoker invokerLoading(Iterator providers) { + WarmUpServiceLoader loader = new WarmUpServiceLoader() { + @Override + Iterator loadProviders() { + return providers; + } + }; + return new ClasspathWarmUpInvoker(loader); + } + + // Writes a real META-INF/services registration for the given class names into tempDir and returns a real + // ServiceLoader iterator over it; an unloadable name makes ServiceLoader throw from next() as in production. + private Iterator realServiceLoader(Path tempDir, String... providerClassNames) throws IOException { + Path servicesDir = tempDir.resolve("META-INF/services"); + Files.createDirectories(servicesDir); + Path registration = servicesDir.resolve(SdkWarmUpProvider.class.getName()); + Files.write(registration, Arrays.asList(providerClassNames)); + + URLClassLoader classLoader = new URLClassLoader(new URL[] {tempDir.toUri().toURL()}, + getClass().getClassLoader()); + return ServiceLoader.load(SdkWarmUpProvider.class, classLoader).iterator(); + } + + private static final class CountingProvider implements SdkWarmUpProvider { + private final AtomicInteger invocations = new AtomicInteger(); + + @Override + public void warmUp() { + invocations.incrementAndGet(); + } + + int invocations() { + return invocations.get(); + } + } + + /** + * Real provider loadable by name through {@link ServiceLoader} (public, top-level-accessible, no-arg ctor). It + * counts invocations in a static field because ServiceLoader instantiates its own copy. + */ + public static final class RealProvider implements SdkWarmUpProvider { + static final AtomicInteger INVOCATIONS = new AtomicInteger(); + + @Override + public void warmUp() { + INVOCATIONS.incrementAndGet(); + } + } +} diff --git a/core/sdk-core/src/test/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider b/core/sdk-core/src/test/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider new file mode 100644 index 000000000000..dfd168470614 --- /dev/null +++ b/core/sdk-core/src/test/resources/META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider @@ -0,0 +1,15 @@ +# +# 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. +# +software.amazon.awssdk.core.crac.CountingWarmUpProvider From 5baf796022e41c7ddd81394431003e99db5684ab Mon Sep 17 00:00:00 2001 From: John Viegas Date: Mon, 22 Jun 2026 17:25:24 -0700 Subject: [PATCH 2/4] Update review comments --- ...der.java => RegisteredWarmUpProvider.java} | 8 ++-- .../awssdk/core/crac/SdkWarmUpTest.java | 13 +++--- .../crac/ClasspathWarmUpInvokerTest.java | 40 ++++++------------- 3 files changed, 22 insertions(+), 39 deletions(-) rename core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/{CountingWarmUpProvider.java => RegisteredWarmUpProvider.java} (67%) diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/CountingWarmUpProvider.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java similarity index 67% rename from core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/CountingWarmUpProvider.java rename to core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java index fdbd2f3b4844..1aefedad8698 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/CountingWarmUpProvider.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/crac/RegisteredWarmUpProvider.java @@ -18,11 +18,11 @@ import java.util.concurrent.atomic.AtomicInteger; /** - * Test-only {@link SdkWarmUpProvider} registered via test-scoped {@code META-INF/services} so the real static - * {@code SdkWarmUp.prime()} discovers and invokes it through {@link java.util.ServiceLoader}. It counts how many - * times {@code warmUp()} is invoked across the JVM. ServiceLoader requires a public no-arg constructor. + * Test-only {@link SdkWarmUpProvider} registered in test-scoped {@code META-INF/services} so a real {@link + * java.util.ServiceLoader} discovers and instantiates it by name. {@code INVOCATIONS} is static because the loader + * builds its own instance. Must be public with a no-arg constructor for ServiceLoader. */ -public final class CountingWarmUpProvider implements SdkWarmUpProvider { +public final class RegisteredWarmUpProvider implements SdkWarmUpProvider { public static final AtomicInteger INVOCATIONS = new AtomicInteger(); 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 b836b36eac7a..76d0dfbb2c3e 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 @@ -23,18 +23,15 @@ import org.junit.jupiter.api.Test; /** - * Tests for the static {@link SdkWarmUp#prime()} entry point, exercised end to end through {@link - * java.util.ServiceLoader} with a test-scoped {@code META-INF/services} registration of {@link - * CountingWarmUpProvider}. - * - *

{@code prime()} runs at most once per JVM and there is no reset hook, so assertions here are - * order-independent: regardless of how many threads call {@code prime()} (and regardless of any other test in - * this JVM having already called it), the counting provider is invoked exactly once in total. + * Tests the static {@link SdkWarmUp#prime()} entry point end to end through {@link java.util.ServiceLoader}, + * using a test-scoped {@code META-INF/services} registration of {@link RegisteredWarmUpProvider}. {@code prime()} + * runs at most once per JVM, so many concurrent calls must invoke the provider exactly once in total. */ class SdkWarmUpTest { @Test void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws InterruptedException { + RegisteredWarmUpProvider.INVOCATIONS.set(0); int threadCount = 16; CountDownLatch start = new CountDownLatch(1); CountDownLatch done = new CountDownLatch(threadCount); @@ -61,6 +58,6 @@ void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws Interrup thread.join(); } - assertThat(CountingWarmUpProvider.INVOCATIONS.get()).isEqualTo(1); + assertThat(RegisteredWarmUpProvider.INVOCATIONS.get()).isEqualTo(1); } } 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 27dd50a20157..c7d05fb7740c 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 @@ -33,13 +33,13 @@ import org.apache.logging.log4j.core.LogEvent; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import software.amazon.awssdk.core.crac.RegisteredWarmUpProvider; import software.amazon.awssdk.core.crac.SdkWarmUpProvider; import software.amazon.awssdk.testutils.LogCaptor; /** - * Unit tests for {@link ClasspathWarmUpInvoker}. The {@link WarmUpServiceLoader} is stubbed so each test injects - * the providers it discovers, mirroring how {@code ClasspathSdkHttpServiceProviderTest} drives the loader. These - * containment and log-level scenarios cannot run through the static, run-once {@code SdkWarmUp.prime()}. + * Unit tests for {@link ClasspathWarmUpInvoker}. Most tests stub {@link WarmUpServiceLoader}; the "fails to load" + * tests use a real {@link ServiceLoader} over a temporary {@code META-INF/services} file. */ class ClasspathWarmUpInvokerTest { @@ -73,15 +73,14 @@ void invokeAll_whenOneProviderThrows_stillInvokesOthers() { @Test void invokeAll_whenProviderFailsToLoad_stillInvokesOthers(@TempDir Path tempDir) throws IOException { - RealProvider.INVOCATIONS.set(0); - // A real ServiceLoader over a registration that lists a class that does not exist, followed by a real - // provider. This proves ServiceLoader advances past the unloadable entry so the real one still runs. - WarmUpInvoker invoker = invokerLoading(realServiceLoader(tempDir, - "com.example.DoesNotExistProvider", - RealProvider.class.getName())); + // Non-existent class then a real one: ServiceLoader must advance past the bad entry. It builds the + // instance itself, so we read the static counter on RegisteredWarmUpProvider. + RegisteredWarmUpProvider.INVOCATIONS.set(0); + WarmUpInvoker invoker = invokerLoading(createAndLoadTempServicesFile( + tempDir, "com.example.DoesNotExistProvider", RegisteredWarmUpProvider.class.getName())); assertThatCode(invoker::invokeAll).doesNotThrowAnyException(); - assertThat(RealProvider.INVOCATIONS.get()).isEqualTo(1); + assertThat(RegisteredWarmUpProvider.INVOCATIONS.get()).isEqualTo(1); } @Test @@ -108,7 +107,7 @@ void invokeAll_whenProviderThrows_logsAtWarn() { @Test void invokeAll_whenProviderFailsToLoad_logsAtWarn(@TempDir Path tempDir) throws IOException { - WarmUpInvoker invoker = invokerLoading(realServiceLoader(tempDir, "com.example.DoesNotExistProvider")); + WarmUpInvoker invoker = invokerLoading(createAndLoadTempServicesFile(tempDir, "com.example.DoesNotExistProvider")); try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { invoker.invokeAll(); @@ -150,9 +149,9 @@ Iterator loadProviders() { return new ClasspathWarmUpInvoker(loader); } - // Writes a real META-INF/services registration for the given class names into tempDir and returns a real - // ServiceLoader iterator over it; an unloadable name makes ServiceLoader throw from next() as in production. - private Iterator realServiceLoader(Path tempDir, String... providerClassNames) throws IOException { + // Creates a temp META-INF/services file with the given class names and loads it through a real ServiceLoader. + private Iterator createAndLoadTempServicesFile(Path tempDir, String... providerClassNames) + throws IOException { Path servicesDir = tempDir.resolve("META-INF/services"); Files.createDirectories(servicesDir); Path registration = servicesDir.resolve(SdkWarmUpProvider.class.getName()); @@ -175,17 +174,4 @@ int invocations() { return invocations.get(); } } - - /** - * Real provider loadable by name through {@link ServiceLoader} (public, top-level-accessible, no-arg ctor). It - * counts invocations in a static field because ServiceLoader instantiates its own copy. - */ - public static final class RealProvider implements SdkWarmUpProvider { - static final AtomicInteger INVOCATIONS = new AtomicInteger(); - - @Override - public void warmUp() { - INVOCATIONS.incrementAndGet(); - } - } } From 1e72243cad8926c975e9677edb25ea003974deef Mon Sep 17 00:00:00 2001 From: John Viegas Date: Tue, 23 Jun 2026 10:07:45 -0700 Subject: [PATCH 3/4] Update review comments --- .../amazon/awssdk/core/crac/SdkWarmUp.java | 22 +++++++++++++------ ....amazon.awssdk.core.crac.SdkWarmUpProvider | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) 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 8657c9ed9432..3ab09cfcf437 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 @@ -16,7 +16,6 @@ package software.amazon.awssdk.core.crac; import java.util.ServiceLoader; -import java.util.concurrent.atomic.AtomicBoolean; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker; @@ -31,8 +30,9 @@ * *

Behavior contract: *