Skip to content

Commit e18d5b5

Browse files
committed
Limit size of context cache in the TestContext framework
Prior to this commit, the size of the ApplicationContext cache in the Spring TestContext Framework could grow without bound, leading to issues with memory and performance in large test suites. This commit addresses this issue by introducing support for setting the maximum cache size via a JVM system property or Spring property called "spring.test.context.cache.maxSize". If no such property is set, a default value of 32 will be used. Furthermore, the DefaultContextCache has been refactored to use a synchronized LRU cache internally instead of a ConcurrentHashMap. The LRU cache is a simple bounded cache with a "least recently used" (LRU) eviction policy. Issue: SPR-8055
1 parent 26378cd commit e18d5b5

File tree

9 files changed

+452
-19
lines changed

9 files changed

+452
-19
lines changed

spring-core/src/main/java/org/springframework/core/SpringProperties.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -42,6 +42,7 @@
4242
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
4343
* @see org.springframework.beans.CachedIntrospectionResults#IGNORE_BEANINFO_PROPERTY_NAME
4444
* @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME
45+
* @see org.springframework.test.context.cache.ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME
4546
*/
4647
public abstract class SpringProperties {
4748

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,7 +26,9 @@
2626
* TestContext Framework</em>.
2727
*
2828
* <p>A {@code ContextCache} maintains a cache of {@code ApplicationContexts}
29-
* keyed by {@link MergedContextConfiguration} instances.
29+
* keyed by {@link MergedContextConfiguration} instances, potentially
30+
* configured with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize
31+
* maximum size} and a custom eviction policy.
3032
*
3133
* <h3>Rationale</h3>
3234
* <p>Context caching can have significant performance benefits if context
@@ -40,6 +42,7 @@
4042
* @author Sam Brannen
4143
* @author Juergen Hoeller
4244
* @since 4.2
45+
* @see ContextCacheUtils#retrieveMaxCacheSize()
4346
*/
4447
public interface ContextCache {
4548

@@ -49,6 +52,24 @@ public interface ContextCache {
4952
*/
5053
public static final String CONTEXT_CACHE_LOGGING_CATEGORY = "org.springframework.test.context.cache";
5154

55+
/**
56+
* The default maximum size of the context cache: {@value #DEFAULT_MAX_CONTEXT_CACHE_SIZE}.
57+
* @see #MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME
58+
*/
59+
public static final int DEFAULT_MAX_CONTEXT_CACHE_SIZE = 32;
60+
61+
/**
62+
* System property used to configure the maximum size of the {@link ContextCache}
63+
* as a positive integer.
64+
* <p>May alternatively be configured via
65+
* {@link org.springframework.core.SpringProperties SpringProperties}.
66+
* <p>Note that implementations of {@code ContextCache} are not required
67+
* to support a maximum cache size. Consult the documentation of the
68+
* corresponding implementation for details.
69+
* @see #DEFAULT_MAX_CONTEXT_CACHE_SIZE
70+
*/
71+
public static final String MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME = "spring.test.context.cache.maxSize";
72+
5273

5374
/**
5475
* Determine whether there is a cached context for the given key.
@@ -59,8 +80,8 @@ public interface ContextCache {
5980

6081
/**
6182
* Obtain a cached {@code ApplicationContext} for the given key.
62-
* <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts
63-
* must be updated accordingly.
83+
* <p>The {@linkplain #getHitCount() hit} and {@linkplain #getMissCount() miss}
84+
* counts must be updated accordingly.
6485
* @param key the context key (never {@code null})
6586
* @return the corresponding {@code ApplicationContext} instance, or {@code null}
6687
* if not found in the cache
@@ -70,7 +91,7 @@ public interface ContextCache {
7091

7192
/**
7293
* Explicitly add an {@code ApplicationContext} instance to the cache
73-
* under the given key.
94+
* under the given key, potentially honoring a custom eviction policy.
7495
* @param key the context key (never {@code null})
7596
* @param context the {@code ApplicationContext} instance (never {@code null})
7697
*/
@@ -80,9 +101,10 @@ public interface ContextCache {
80101
* Remove the context with the given key from the cache and explicitly
81102
* {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close}
82103
* it if it is an instance of {@code ConfigurableApplicationContext}.
83-
* <p>Generally speaking, this method should be called if the state of
84-
* a singleton bean has been modified, potentially affecting future
85-
* interaction with the context.
104+
* <p>Generally speaking, this method should be called to properly evict
105+
* a context from the cache (e.g., due to a custom eviction policy) or if
106+
* the state of a singleton bean has been modified, potentially affecting
107+
* future interaction with the context.
86108
* <p>In addition, the semantics of the supplied {@code HierarchyMode} must
87109
* be honored. See the Javadoc for {@link HierarchyMode} for details.
88110
* @param key the context key; never {@code null}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2002-2016 the original author or authors.
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+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.cache;
18+
19+
import org.springframework.core.SpringProperties;
20+
import org.springframework.util.StringUtils;
21+
22+
/**
23+
* Collection of utilities for working with {@link ContextCache ContextCaches}.
24+
*
25+
* @author Sam Brannen
26+
* @since 4.3
27+
*/
28+
public abstract class ContextCacheUtils {
29+
30+
private ContextCacheUtils() {
31+
/* no-op */
32+
}
33+
34+
35+
/**
36+
* Retrieve the maximum size of the {@link ContextCache}.
37+
* <p>Uses {@link SpringProperties} to retrieve a system property or Spring
38+
* property named {@code spring.test.context.cache.maxSize}.
39+
* <p>Falls back to the value of the {@link ContextCache#DEFAULT_MAX_CONTEXT_CACHE_SIZE}
40+
* if no such property has been set or if the property is not an integer.
41+
* @return the maximum size of the context cache
42+
* @see ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME
43+
*/
44+
public static int retrieveMaxCacheSize() {
45+
try {
46+
String maxSize = SpringProperties.getProperty(ContextCache.MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME);
47+
if (StringUtils.hasText(maxSize)) {
48+
return Integer.parseInt(maxSize.trim());
49+
}
50+
}
51+
catch (Exception ex) {
52+
/* ignore */
53+
}
54+
55+
// Fallback
56+
return ContextCache.DEFAULT_MAX_CONTEXT_CACHE_SIZE;
57+
}
58+
59+
}

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

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,7 +17,9 @@
1717
package org.springframework.test.context.cache;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collections;
2021
import java.util.HashSet;
22+
import java.util.LinkedHashMap;
2123
import java.util.List;
2224
import java.util.Map;
2325
import java.util.Set;
@@ -37,12 +39,18 @@
3739
/**
3840
* Default implementation of the {@link ContextCache} API.
3941
*
40-
* <p>Uses {@link ConcurrentHashMap ConcurrentHashMaps} to cache
41-
* {@link ApplicationContext} and {@link MergedContextConfiguration} instances.
42+
* <p>Uses a synchronized {@link Map} configured with a maximum size
43+
* and a <em>least recently used</em> (LRU) eviction policy to cache
44+
* {@link ApplicationContext} instances.
45+
*
46+
* <p>The maximum size may be supplied as a {@linkplain #DefaultContextCache(int)
47+
* constructor argument} or set via a system property or Spring property named
48+
* {@code spring.test.context.cache.maxSize}.
4249
*
4350
* @author Sam Brannen
4451
* @author Juergen Hoeller
4552
* @since 2.5
53+
* @see ContextCacheUtils#retrieveMaxCacheSize()
4654
*/
4755
public class DefaultContextCache implements ContextCache {
4856

@@ -52,7 +60,7 @@ public class DefaultContextCache implements ContextCache {
5260
* Map of context keys to Spring {@code ApplicationContext} instances.
5361
*/
5462
private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
55-
new ConcurrentHashMap<MergedContextConfiguration, ApplicationContext>(64);
63+
Collections.synchronizedMap(new LruCache(32, 0.75f));
5664

5765
/**
5866
* Map of parent keys to sets of children keys, representing a top-down <em>tree</em>
@@ -61,13 +69,41 @@ public class DefaultContextCache implements ContextCache {
6169
* of other contexts.
6270
*/
6371
private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap =
64-
new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64);
72+
new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(32);
73+
74+
private final int maxSize;
6575

6676
private final AtomicInteger hitCount = new AtomicInteger();
6777

6878
private final AtomicInteger missCount = new AtomicInteger();
6979

7080

81+
/**
82+
* Create a new {@code DefaultContextCache} using the maximum cache size
83+
* obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}.
84+
* @since 4.3
85+
* @see #DefaultContextCache(int)
86+
* @see ContextCacheUtils#retrieveMaxCacheSize()
87+
*/
88+
public DefaultContextCache() {
89+
this(ContextCacheUtils.retrieveMaxCacheSize());
90+
}
91+
92+
/**
93+
* Create a new {@code DefaultContextCache} using the supplied maximum
94+
* cache size.
95+
* @param maxSize the maximum cache size
96+
* @throws IllegalArgumentException if the supplied {@code maxSize} value
97+
* is not positive
98+
* @since 4.3
99+
* @see #DefaultContextCache()
100+
*/
101+
public DefaultContextCache(int maxSize) {
102+
Assert.isTrue(maxSize > 0, "maxSize must be positive");
103+
this.maxSize = maxSize;
104+
}
105+
106+
71107
/**
72108
* {@inheritDoc}
73109
*/
@@ -181,6 +217,13 @@ public int size() {
181217
return this.contextMap.size();
182218
}
183219

220+
/**
221+
* Get the maximum size of this cache.
222+
*/
223+
public int getMaxSize() {
224+
return this.maxSize;
225+
}
226+
184227
/**
185228
* {@inheritDoc}
186229
*/
@@ -210,7 +253,7 @@ public int getMissCount() {
210253
*/
211254
@Override
212255
public void reset() {
213-
synchronized (contextMap) {
256+
synchronized (this.contextMap) {
214257
clear();
215258
clearStatistics();
216259
}
@@ -221,7 +264,7 @@ public void reset() {
221264
*/
222265
@Override
223266
public void clear() {
224-
synchronized (contextMap) {
267+
synchronized (this.contextMap) {
225268
this.contextMap.clear();
226269
this.hierarchyMap.clear();
227270
}
@@ -232,7 +275,7 @@ public void clear() {
232275
*/
233276
@Override
234277
public void clearStatistics() {
235-
synchronized (contextMap) {
278+
synchronized (this.contextMap) {
236279
this.hitCount.set(0);
237280
this.missCount.set(0);
238281
}
@@ -259,10 +302,46 @@ public void logStatistics() {
259302
public String toString() {
260303
return new ToStringCreator(this)
261304
.append("size", size())
305+
.append("maxSize", getMaxSize())
262306
.append("parentContextCount", getParentContextCount())
263307
.append("hitCount", getHitCount())
264308
.append("missCount", getMissCount())
265309
.toString();
266310
}
267311

312+
313+
/**
314+
* Simple cache implementation based on {@link LinkedHashMap} with a maximum
315+
* size and a <em>least recently used</em> (LRU) eviction policy that
316+
* properly closes application contexts.
317+
*
318+
* @author Sam Brannen
319+
* @since 4.3
320+
*/
321+
@SuppressWarnings("serial")
322+
private class LruCache extends LinkedHashMap<MergedContextConfiguration, ApplicationContext> {
323+
324+
/**
325+
* Create a new {@code LruCache} with the supplied initial capacity and
326+
* load factor.
327+
* @param initialCapacity the initial capacity
328+
* @param loadFactor the load factor
329+
*/
330+
LruCache(int initialCapacity, float loadFactor) {
331+
super(initialCapacity, loadFactor, true);
332+
}
333+
334+
@Override
335+
protected boolean removeEldestEntry(Map.Entry<MergedContextConfiguration, ApplicationContext> eldest) {
336+
if (this.size() > DefaultContextCache.this.getMaxSize()) {
337+
// Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally
338+
// invoke java.util.Map.remove(Object, Object).
339+
DefaultContextCache.this.remove(eldest.getKey(), HierarchyMode.CURRENT_LEVEL);
340+
}
341+
342+
// Return false since we invoke a custom eviction algorithm.
343+
return false;
344+
}
345+
}
346+
268347
}

spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -42,6 +42,7 @@
4242
* @author Sam Brannen
4343
* @author Michail Nikolaev
4444
* @since 3.1
45+
* @see LruContextCacheTests
4546
* @see SpringRunnerContextCacheTests
4647
*/
4748
public class ContextCacheTests {

0 commit comments

Comments
 (0)