Skip to content

Commit 5783899

Browse files
committed
feat(sdk-core): add SdkWarmUp.prime() for CRaC auto-priming
1 parent 88a7b4c commit 5783899

8 files changed

Lines changed: 515 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.crac;
17+
18+
import java.util.ServiceLoader;
19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
import software.amazon.awssdk.annotations.SdkPublicApi;
21+
import software.amazon.awssdk.annotations.ThreadSafe;
22+
import software.amazon.awssdk.core.internal.crac.ClasspathWarmUpInvoker;
23+
24+
/**
25+
* Entry point for warming up SDK service request paths before a Coordinated Restore at Checkpoint (CRaC)
26+
* checkpoint.
27+
*
28+
* <p>{@link #prime()} discovers every {@link SdkWarmUpProvider} registered on the classpath through {@link
29+
* ServiceLoader} (via the {@code META-INF/services/software.amazon.awssdk.core.crac.SdkWarmUpProvider}
30+
* resource) and invokes {@link SdkWarmUpProvider#warmUp()} on each.
31+
*
32+
* <p>Behavior contract:
33+
* <ul>
34+
* <li><b>Idempotent:</b> {@code prime()} runs at most once per JVM. Subsequent calls return without
35+
* re-running discovery or invoking any provider.</li>
36+
* <li><b>Per-provider resilience:</b> a single provider that throws from {@code warmUp()}, or that fails
37+
* to load, does not prevent the remaining providers from running.</li>
38+
* <li><b>Safe when empty:</b> if no providers are registered, {@code prime()} is a no-op.</li>
39+
* </ul>
40+
*
41+
* <p>Call this once during application initialization, before a CRaC checkpoint is taken.
42+
*/
43+
@ThreadSafe
44+
@SdkPublicApi
45+
public final class SdkWarmUp {
46+
47+
private static final AtomicBoolean PRIMED = new AtomicBoolean(false);
48+
49+
private SdkWarmUp() {
50+
}
51+
52+
/**
53+
* Discovers every {@link SdkWarmUpProvider} on the classpath and invokes {@link SdkWarmUpProvider#warmUp()}
54+
* on each, honoring the idempotency, per-provider resilience, and empty-classpath behavior described on
55+
* this class. Safe to call concurrently.
56+
*/
57+
public static void prime() {
58+
if (!PRIMED.compareAndSet(false, true)) {
59+
return;
60+
}
61+
62+
ClasspathWarmUpInvoker.create().invokeAll();
63+
}
64+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.internal.crac;
17+
18+
import java.util.Iterator;
19+
import java.util.ServiceConfigurationError;
20+
import java.util.ServiceLoader;
21+
import software.amazon.awssdk.annotations.SdkInternalApi;
22+
import software.amazon.awssdk.annotations.SdkTestInternalApi;
23+
import software.amazon.awssdk.core.crac.SdkWarmUpProvider;
24+
import software.amazon.awssdk.utils.Logger;
25+
26+
/**
27+
* {@link WarmUpInvoker} implementation that uses {@link ServiceLoader} to find {@link SdkWarmUpProvider}
28+
* implementations on the classpath and invokes {@code warmUp()} on every one of them.
29+
*/
30+
@SdkInternalApi
31+
public final class ClasspathWarmUpInvoker implements WarmUpInvoker {
32+
33+
private static final Logger log = Logger.loggerFor(ClasspathWarmUpInvoker.class);
34+
35+
private final WarmUpServiceLoader serviceLoader;
36+
37+
@SdkTestInternalApi
38+
ClasspathWarmUpInvoker(WarmUpServiceLoader serviceLoader) {
39+
this.serviceLoader = serviceLoader;
40+
}
41+
42+
@Override
43+
public void invokeAll() {
44+
Iterator<SdkWarmUpProvider> iterator = serviceLoader.loadProviders();
45+
boolean invokedAny = false;
46+
47+
while (iterator.hasNext()) {
48+
SdkWarmUpProvider provider;
49+
try {
50+
provider = iterator.next();
51+
} catch (ServiceConfigurationError e) {
52+
// next() has already advanced past the bad provider, so it is safe to continue to the next one.
53+
log.warn(() -> "Skipping an SdkWarmUpProvider that could not be loaded.", e);
54+
continue;
55+
}
56+
57+
invokedAny = true;
58+
try {
59+
provider.warmUp();
60+
} catch (RuntimeException e) {
61+
log.warn(() -> "An SdkWarmUpProvider failed during warmUp() and was skipped.", e);
62+
}
63+
}
64+
65+
if (!invokedAny) {
66+
log.debug(() -> "No SdkWarmUpProvider implementations were discovered on the classpath.");
67+
}
68+
}
69+
70+
/**
71+
* @return ClasspathWarmUpInvoker that discovers {@link SdkWarmUpProvider}s from the classpath.
72+
*/
73+
public static WarmUpInvoker create() {
74+
return new ClasspathWarmUpInvoker(WarmUpServiceLoader.INSTANCE);
75+
}
76+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.internal.crac;
17+
18+
import software.amazon.awssdk.annotations.SdkInternalApi;
19+
import software.amazon.awssdk.core.crac.SdkWarmUpProvider;
20+
21+
/**
22+
* Discovers {@link SdkWarmUpProvider}s and invokes their warm-up behind the public {@code SdkWarmUp.prime()}.
23+
* Mirrors the {@code SdkHttpServiceProvider} loader abstraction, except warm-up invokes every discovered
24+
* provider rather than selecting one.
25+
*/
26+
@SdkInternalApi
27+
public interface WarmUpInvoker {
28+
29+
/**
30+
* Invokes {@link SdkWarmUpProvider#warmUp()} on every discovered provider, containing per-provider failures
31+
* so one failing provider does not stop the others.
32+
*/
33+
void invokeAll();
34+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.internal.crac;
17+
18+
import java.util.Iterator;
19+
import java.util.ServiceLoader;
20+
import software.amazon.awssdk.annotations.SdkInternalApi;
21+
import software.amazon.awssdk.core.crac.SdkWarmUpProvider;
22+
import software.amazon.awssdk.core.internal.util.ClassLoaderHelper;
23+
24+
/**
25+
* Thin layer over {@link ServiceLoader} for {@link SdkWarmUpProvider}.
26+
*/
27+
@SdkInternalApi
28+
class WarmUpServiceLoader {
29+
30+
public static final WarmUpServiceLoader INSTANCE = new WarmUpServiceLoader();
31+
32+
Iterator<SdkWarmUpProvider> loadProviders() {
33+
return ServiceLoader.load(SdkWarmUpProvider.class,
34+
ClassLoaderHelper.classLoader(WarmUpServiceLoader.class)).iterator();
35+
}
36+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.crac;
17+
18+
import java.util.concurrent.atomic.AtomicInteger;
19+
20+
/**
21+
* Test-only {@link SdkWarmUpProvider} registered via test-scoped {@code META-INF/services} so the real static
22+
* {@code SdkWarmUp.prime()} discovers and invokes it through {@link java.util.ServiceLoader}. It counts how many
23+
* times {@code warmUp()} is invoked across the JVM. ServiceLoader requires a public no-arg constructor.
24+
*/
25+
public final class CountingWarmUpProvider implements SdkWarmUpProvider {
26+
27+
public static final AtomicInteger INVOCATIONS = new AtomicInteger();
28+
29+
@Override
30+
public void warmUp() {
31+
INVOCATIONS.incrementAndGet();
32+
}
33+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.core.crac;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.concurrent.CountDownLatch;
23+
import org.junit.jupiter.api.Test;
24+
25+
/**
26+
* Tests for the static {@link SdkWarmUp#prime()} entry point, exercised end to end through {@link
27+
* java.util.ServiceLoader} with a test-scoped {@code META-INF/services} registration of {@link
28+
* CountingWarmUpProvider}.
29+
*
30+
* <p>{@code prime()} runs at most once per JVM and there is no reset hook, so assertions here are
31+
* order-independent: regardless of how many threads call {@code prime()} (and regardless of any other test in
32+
* this JVM having already called it), the counting provider is invoked exactly once in total.
33+
*/
34+
class SdkWarmUpTest {
35+
36+
@Test
37+
void prime_concurrentCalls_invokeRegisteredProviderExactlyOnce() throws InterruptedException {
38+
int threadCount = 16;
39+
CountDownLatch start = new CountDownLatch(1);
40+
CountDownLatch done = new CountDownLatch(threadCount);
41+
List<Thread> threads = new ArrayList<>();
42+
43+
for (int i = 0; i < threadCount; i++) {
44+
Thread thread = new Thread(() -> {
45+
try {
46+
start.await();
47+
SdkWarmUp.prime();
48+
} catch (InterruptedException e) {
49+
Thread.currentThread().interrupt();
50+
} finally {
51+
done.countDown();
52+
}
53+
});
54+
threads.add(thread);
55+
thread.start();
56+
}
57+
58+
start.countDown();
59+
done.await();
60+
for (Thread thread : threads) {
61+
thread.join();
62+
}
63+
64+
assertThat(CountingWarmUpProvider.INVOCATIONS.get()).isEqualTo(1);
65+
}
66+
}

0 commit comments

Comments
 (0)