Skip to content

Commit c092acb

Browse files
committed
optimization draft
1 parent 113c101 commit c092acb

File tree

4 files changed

+108
-55
lines changed

4 files changed

+108
-55
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version=7.0.0-SNAPSHOT
1+
version=7.0.0b-SNAPSHOT
22

33
org.gradle.caching=true
44
org.gradle.jvmargs=-Xmx2048m

spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
package org.springframework.test.context.cache;
1818

1919
import org.jspecify.annotations.Nullable;
20-
2120
import org.springframework.context.ApplicationContext;
2221
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
2322
import org.springframework.test.context.MergedContextConfiguration;
2423

24+
import java.util.concurrent.Future;
25+
import java.util.function.Function;
26+
2527
/**
2628
* {@code ContextCache} defines the SPI for caching Spring
2729
* {@link ApplicationContext ApplicationContexts} within the
@@ -96,7 +98,10 @@ public interface ContextCache {
9698
* if not found in the cache
9799
* @see #remove
98100
*/
99-
@Nullable ApplicationContext get(MergedContextConfiguration key);
101+
@Nullable
102+
ApplicationContext get(MergedContextConfiguration key);
103+
104+
Future<ApplicationContext> computeIfAbsent(MergedContextConfiguration key, Function<MergedContextConfiguration, ApplicationContext> mappingFunction);
100105

101106
/**
102107
* Explicitly add an {@code ApplicationContext} instance to the cache

spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.test.context.cache;
1818

1919
import java.util.List;
20+
import java.util.concurrent.ExecutionException;
2021

2122
import org.apache.commons.logging.Log;
2223
import org.apache.commons.logging.LogFactory;
@@ -125,43 +126,58 @@ private DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache, int fa
125126
@Override
126127
public boolean isContextLoaded(MergedContextConfiguration mergedConfig) {
127128
mergedConfig = replaceIfNecessary(mergedConfig);
128-
synchronized (this.contextCache) {
129129
return this.contextCache.contains(mergedConfig);
130130
}
131-
}
132131

133132
@Override
134133
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) {
135134
mergedConfig = replaceIfNecessary(mergedConfig);
136-
synchronized (this.contextCache) {
137-
ApplicationContext context = this.contextCache.get(mergedConfig);
135+
138136
try {
139-
if (context == null) {
140-
int failureCount = this.contextCache.getFailureCount(mergedConfig);
137+
var contextLoader = this.contextCache.computeIfAbsent(mergedConfig, this::loadContextForReal);
138+
139+
var context = contextLoader.get();
140+
141+
if (context != null && logger.isTraceEnabled()) {
142+
logger.trace("Retrieved ApplicationContext [%s] from cache with key %s".formatted(
143+
System.identityHashCode(context), mergedConfig));
144+
}
145+
146+
return context;
147+
} catch (InterruptedException e) {
148+
throw new RuntimeException(e); //FIXME: Better message
149+
} catch (ExecutionException e) {
150+
throw new RuntimeException(e); //FIXME: Better message
151+
} finally {
152+
this.contextCache.logStatistics();
153+
}
154+
}
155+
156+
private ApplicationContext loadContextForReal(MergedContextConfiguration k) {
157+
int failureCount = this.contextCache.getFailureCount(k);
141158
if (failureCount >= this.failureThreshold) {
142159
throw new IllegalStateException("""
143160
ApplicationContext failure threshold (%d) exceeded: \
144161
skipping repeated attempt to load context for %s"""
145-
.formatted(this.failureThreshold, mergedConfig));
162+
.formatted(this.failureThreshold, k));
146163
}
147164
try {
148-
if (mergedConfig instanceof AotMergedContextConfiguration aotMergedConfig) {
149-
context = loadContextInAotMode(aotMergedConfig);
150-
}
151-
else {
152-
context = loadContextInternal(mergedConfig);
165+
ApplicationContext contextToReturn;
166+
if (k instanceof AotMergedContextConfiguration aotMergedConfig) {
167+
contextToReturn = loadContextInAotMode(aotMergedConfig);
168+
} else {
169+
contextToReturn = loadContextInternal(k);
153170
}
154171
if (logger.isTraceEnabled()) {
155172
logger.trace("Storing ApplicationContext [%s] in cache under key %s".formatted(
156-
System.identityHashCode(context), mergedConfig));
173+
System.identityHashCode(contextToReturn), k));
157174
}
158-
this.contextCache.put(mergedConfig, context);
159-
}
160-
catch (Exception ex) {
175+
return contextToReturn;
176+
} catch (Exception ex) {
161177
if (logger.isTraceEnabled()) {
162-
logger.trace("Incrementing ApplicationContext failure count for " + mergedConfig);
178+
logger.trace("Incrementing ApplicationContext failure count for " + k);
163179
}
164-
this.contextCache.incrementFailureCount(mergedConfig);
180+
this.contextCache.incrementFailureCount(k);
165181
Throwable cause = ex;
166182
if (ex instanceof ContextLoadException cle) {
167183
cause = cle.getCause();
@@ -178,38 +194,15 @@ ApplicationContext failure threshold (%d) exceeded: \
178194
}
179195
}
180196
throw new IllegalStateException(
181-
"Failed to load ApplicationContext for " + mergedConfig, cause);
182-
}
183-
}
184-
else {
185-
if (logger.isTraceEnabled()) {
186-
logger.trace("Retrieved ApplicationContext [%s] from cache with key %s".formatted(
187-
System.identityHashCode(context), mergedConfig));
188-
}
189-
}
190-
}
191-
finally {
192-
this.contextCache.logStatistics();
193-
}
194-
195-
return context;
197+
"Failed to load ApplicationContext for " + k, cause);
196198
}
197199
}
198200

199201
@Override
200202
public void closeContext(MergedContextConfiguration mergedConfig, @Nullable HierarchyMode hierarchyMode) {
201203
mergedConfig = replaceIfNecessary(mergedConfig);
202-
synchronized (this.contextCache) {
203204
this.contextCache.remove(mergedConfig, hierarchyMode);
204205
}
205-
}
206-
207-
/**
208-
* Get the {@link ContextCache} used by this context loader delegate.
209-
*/
210-
protected ContextCache getContextCache() {
211-
return this.contextCache;
212-
}
213206

214207
/**
215208
* Load the {@code ApplicationContext} for the supplied merged context configuration.

spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@
2323
import java.util.List;
2424
import java.util.Map;
2525
import java.util.Set;
26-
import java.util.concurrent.ConcurrentHashMap;
26+
import java.util.concurrent.*;
2727
import java.util.concurrent.atomic.AtomicInteger;
28+
import java.util.function.Function;
2829

2930
import org.apache.commons.logging.Log;
3031
import org.apache.commons.logging.LogFactory;
31-
import org.jspecify.annotations.Nullable;
3232

33+
import org.jspecify.annotations.Nullable;
3334
import org.springframework.context.ApplicationContext;
3435
import org.springframework.context.ConfigurableApplicationContext;
3536
import org.springframework.core.style.ToStringCreator;
@@ -57,11 +58,12 @@ public class DefaultContextCache implements ContextCache {
5758

5859
private static final Log statsLogger = LogFactory.getLog(CONTEXT_CACHE_LOGGING_CATEGORY);
5960

61+
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //TODO: Make this parametric
6062

6163
/**
6264
* Map of context keys to Spring {@code ApplicationContext} instances.
6365
*/
64-
private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
66+
private final Map<MergedContextConfiguration, Future<ApplicationContext>> contextMap =
6567
Collections.synchronizedMap(new LruCache(32, 0.75f));
6668

6769
/**
@@ -120,25 +122,69 @@ public boolean contains(MergedContextConfiguration key) {
120122
return this.contextMap.containsKey(key);
121123
}
122124

123-
@Override
125+
@Override//TODO: This is not used anymore in spring but make sense to keep it for retro compatibility, right?
124126
public @Nullable ApplicationContext get(MergedContextConfiguration key) {
125127
Assert.notNull(key, "Key must not be null");
126-
ApplicationContext context = this.contextMap.get(key);
128+
129+
try {
130+
Future<ApplicationContext> context = this.contextMap.get(key);
131+
127132
if (context == null) {
128133
this.missCount.incrementAndGet();
134+
135+
return null;
129136
}
130137
else {
138+
this.hitCount.incrementAndGet();
139+
140+
return context.get();
141+
}
142+
} catch (InterruptedException e) {
143+
throw new RuntimeException(e);//FIXME: fix the message
144+
} catch (ExecutionException e) {
145+
throw new RuntimeException(e);//FIXME: fix the message
146+
}
147+
}
148+
149+
150+
@Override
151+
public Future<ApplicationContext> computeIfAbsent(MergedContextConfiguration key, Function<MergedContextConfiguration, ApplicationContext> mappingFunction) {
152+
Assert.notNull(key, "Key must not be null");
153+
154+
if(contextMap.containsKey(key)) {
131155
this.hitCount.incrementAndGet();
132156
}
133-
return context;
157+
158+
return contextMap.computeIfAbsent(key, (k) ->
159+
{
160+
this.missCount.incrementAndGet();
161+
return CompletableFuture.supplyAsync(() -> mappingFunction.apply(k), executorService)
162+
.thenApply(
163+
(contextLoaded) -> {
164+
MergedContextConfiguration child = key;
165+
MergedContextConfiguration parent = child.getParent();
166+
while (parent != null) {
167+
Set<MergedContextConfiguration> list = this.hierarchyMap.computeIfAbsent(parent, k2 -> new HashSet<>());
168+
list.add(child);
169+
child = parent;
170+
parent = child.getParent();
171+
}
172+
173+
return contextLoaded;
174+
}
175+
);
176+
177+
}
178+
);
134179
}
135180

181+
//TODO: This is not used anymore in spring but make sense to keep it for retro compatibility, right?
136182
@Override
137183
public void put(MergedContextConfiguration key, ApplicationContext context) {
138184
Assert.notNull(key, "Key must not be null");
139185
Assert.notNull(context, "ApplicationContext must not be null");
140186

141-
this.contextMap.put(key, context);
187+
this.contextMap.put(key, CompletableFuture.completedFuture(context));
142188
MergedContextConfiguration child = key;
143189
MergedContextConfiguration parent = child.getParent();
144190
while (parent != null) {
@@ -198,10 +244,19 @@ private void remove(List<MergedContextConfiguration> removedContexts, MergedCont
198244

199245
// Physically remove and close leaf nodes first (i.e., on the way back up the
200246
// stack as opposed to prior to the recursive call).
201-
ApplicationContext context = this.contextMap.remove(key);
247+
Future<ApplicationContext> contextLoader = this.contextMap.remove(key);
248+
249+
try {
250+
ApplicationContext context = contextLoader.get();
202251
if (context instanceof ConfigurableApplicationContext cac) {
203252
cac.close();
204253
}
254+
} catch (InterruptedException e) {
255+
throw new RuntimeException(e); //FIXME: fix the message
256+
} catch (ExecutionException e) {
257+
throw new RuntimeException(e); //FIXME: fix the message
258+
}
259+
205260
removedContexts.add(key);
206261
}
207262

@@ -303,7 +358,7 @@ public String toString() {
303358
* @since 4.3
304359
*/
305360
@SuppressWarnings("serial")
306-
private class LruCache extends LinkedHashMap<MergedContextConfiguration, ApplicationContext> {
361+
private class LruCache extends LinkedHashMap<MergedContextConfiguration, Future<ApplicationContext>> {
307362

308363
/**
309364
* Create a new {@code LruCache} with the supplied initial capacity
@@ -316,7 +371,7 @@ private class LruCache extends LinkedHashMap<MergedContextConfiguration, Applica
316371
}
317372

318373
@Override
319-
protected boolean removeEldestEntry(Map.Entry<MergedContextConfiguration, ApplicationContext> eldest) {
374+
protected boolean removeEldestEntry(Map.Entry<MergedContextConfiguration, Future<ApplicationContext>> eldest) {
320375
if (this.size() > DefaultContextCache.this.getMaxSize()) {
321376
// Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally
322377
// invoke java.util.Map.remove(Object, Object).

0 commit comments

Comments
 (0)