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
This commit is contained in:
Sam Brannen
2016-04-03 20:41:54 +02:00
parent 26378cd604
commit e18d5b591a
9 changed files with 452 additions and 19 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,9 @@ import org.springframework.test.context.MergedContextConfiguration;
* TestContext Framework</em>.
*
* <p>A {@code ContextCache} maintains a cache of {@code ApplicationContexts}
* keyed by {@link MergedContextConfiguration} instances.
* keyed by {@link MergedContextConfiguration} instances, potentially
* configured with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize
* maximum size} and a custom eviction policy.
*
* <h3>Rationale</h3>
* <p>Context caching can have significant performance benefits if context
@@ -40,6 +42,7 @@ import org.springframework.test.context.MergedContextConfiguration;
* @author Sam Brannen
* @author Juergen Hoeller
* @since 4.2
* @see ContextCacheUtils#retrieveMaxCacheSize()
*/
public interface ContextCache {
@@ -49,6 +52,24 @@ public interface ContextCache {
*/
public static final String CONTEXT_CACHE_LOGGING_CATEGORY = "org.springframework.test.context.cache";
/**
* The default maximum size of the context cache: {@value #DEFAULT_MAX_CONTEXT_CACHE_SIZE}.
* @see #MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME
*/
public static final int DEFAULT_MAX_CONTEXT_CACHE_SIZE = 32;
/**
* System property used to configure the maximum size of the {@link ContextCache}
* as a positive integer.
* <p>May alternatively be configured via
* {@link org.springframework.core.SpringProperties SpringProperties}.
* <p>Note that implementations of {@code ContextCache} are not required
* to support a maximum cache size. Consult the documentation of the
* corresponding implementation for details.
* @see #DEFAULT_MAX_CONTEXT_CACHE_SIZE
*/
public static final String MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME = "spring.test.context.cache.maxSize";
/**
* Determine whether there is a cached context for the given key.
@@ -59,8 +80,8 @@ public interface ContextCache {
/**
* Obtain a cached {@code ApplicationContext} for the given key.
* <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts
* must be updated accordingly.
* <p>The {@linkplain #getHitCount() hit} and {@linkplain #getMissCount() miss}
* counts must be updated accordingly.
* @param key the context key (never {@code null})
* @return the corresponding {@code ApplicationContext} instance, or {@code null}
* if not found in the cache
@@ -70,7 +91,7 @@ public interface ContextCache {
/**
* Explicitly add an {@code ApplicationContext} instance to the cache
* under the given key.
* under the given key, potentially honoring a custom eviction policy.
* @param key the context key (never {@code null})
* @param context the {@code ApplicationContext} instance (never {@code null})
*/
@@ -80,9 +101,10 @@ public interface ContextCache {
* Remove the context with the given key from the cache and explicitly
* {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close}
* it if it is an instance of {@code ConfigurableApplicationContext}.
* <p>Generally speaking, this method should be called if the state of
* a singleton bean has been modified, potentially affecting future
* interaction with the context.
* <p>Generally speaking, this method should be called to properly evict
* a context from the cache (e.g., due to a custom eviction policy) or if
* the state of a singleton bean has been modified, potentially affecting
* future interaction with the context.
* <p>In addition, the semantics of the supplied {@code HierarchyMode} must
* be honored. See the Javadoc for {@link HierarchyMode} for details.
* @param key the context key; never {@code null}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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 org.springframework.test.context.cache;
import org.springframework.core.SpringProperties;
import org.springframework.util.StringUtils;
/**
* Collection of utilities for working with {@link ContextCache ContextCaches}.
*
* @author Sam Brannen
* @since 4.3
*/
public abstract class ContextCacheUtils {
private ContextCacheUtils() {
/* no-op */
}
/**
* Retrieve the maximum size of the {@link ContextCache}.
* <p>Uses {@link SpringProperties} to retrieve a system property or Spring
* property named {@code spring.test.context.cache.maxSize}.
* <p>Falls back to the value of the {@link ContextCache#DEFAULT_MAX_CONTEXT_CACHE_SIZE}
* if no such property has been set or if the property is not an integer.
* @return the maximum size of the context cache
* @see ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME
*/
public static int retrieveMaxCacheSize() {
try {
String maxSize = SpringProperties.getProperty(ContextCache.MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME);
if (StringUtils.hasText(maxSize)) {
return Integer.parseInt(maxSize.trim());
}
}
catch (Exception ex) {
/* ignore */
}
// Fallback
return ContextCache.DEFAULT_MAX_CONTEXT_CACHE_SIZE;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,9 @@
package org.springframework.test.context.cache;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -37,12 +39,18 @@ import org.springframework.util.Assert;
/**
* Default implementation of the {@link ContextCache} API.
*
* <p>Uses {@link ConcurrentHashMap ConcurrentHashMaps} to cache
* {@link ApplicationContext} and {@link MergedContextConfiguration} instances.
* <p>Uses a synchronized {@link Map} configured with a maximum size
* and a <em>least recently used</em> (LRU) eviction policy to cache
* {@link ApplicationContext} instances.
*
* <p>The maximum size may be supplied as a {@linkplain #DefaultContextCache(int)
* constructor argument} or set via a system property or Spring property named
* {@code spring.test.context.cache.maxSize}.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @since 2.5
* @see ContextCacheUtils#retrieveMaxCacheSize()
*/
public class DefaultContextCache implements ContextCache {
@@ -52,7 +60,7 @@ public class DefaultContextCache implements ContextCache {
* Map of context keys to Spring {@code ApplicationContext} instances.
*/
private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
new ConcurrentHashMap<MergedContextConfiguration, ApplicationContext>(64);
Collections.synchronizedMap(new LruCache(32, 0.75f));
/**
* 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 {
* of other contexts.
*/
private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap =
new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64);
new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(32);
private final int maxSize;
private final AtomicInteger hitCount = new AtomicInteger();
private final AtomicInteger missCount = new AtomicInteger();
/**
* Create a new {@code DefaultContextCache} using the maximum cache size
* obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}.
* @since 4.3
* @see #DefaultContextCache(int)
* @see ContextCacheUtils#retrieveMaxCacheSize()
*/
public DefaultContextCache() {
this(ContextCacheUtils.retrieveMaxCacheSize());
}
/**
* Create a new {@code DefaultContextCache} using the supplied maximum
* cache size.
* @param maxSize the maximum cache size
* @throws IllegalArgumentException if the supplied {@code maxSize} value
* is not positive
* @since 4.3
* @see #DefaultContextCache()
*/
public DefaultContextCache(int maxSize) {
Assert.isTrue(maxSize > 0, "maxSize must be positive");
this.maxSize = maxSize;
}
/**
* {@inheritDoc}
*/
@@ -181,6 +217,13 @@ public class DefaultContextCache implements ContextCache {
return this.contextMap.size();
}
/**
* Get the maximum size of this cache.
*/
public int getMaxSize() {
return this.maxSize;
}
/**
* {@inheritDoc}
*/
@@ -210,7 +253,7 @@ public class DefaultContextCache implements ContextCache {
*/
@Override
public void reset() {
synchronized (contextMap) {
synchronized (this.contextMap) {
clear();
clearStatistics();
}
@@ -221,7 +264,7 @@ public class DefaultContextCache implements ContextCache {
*/
@Override
public void clear() {
synchronized (contextMap) {
synchronized (this.contextMap) {
this.contextMap.clear();
this.hierarchyMap.clear();
}
@@ -232,7 +275,7 @@ public class DefaultContextCache implements ContextCache {
*/
@Override
public void clearStatistics() {
synchronized (contextMap) {
synchronized (this.contextMap) {
this.hitCount.set(0);
this.missCount.set(0);
}
@@ -259,10 +302,46 @@ public class DefaultContextCache implements ContextCache {
public String toString() {
return new ToStringCreator(this)
.append("size", size())
.append("maxSize", getMaxSize())
.append("parentContextCount", getParentContextCount())
.append("hitCount", getHitCount())
.append("missCount", getMissCount())
.toString();
}
/**
* Simple cache implementation based on {@link LinkedHashMap} with a maximum
* size and a <em>least recently used</em> (LRU) eviction policy that
* properly closes application contexts.
*
* @author Sam Brannen
* @since 4.3
*/
@SuppressWarnings("serial")
private class LruCache extends LinkedHashMap<MergedContextConfiguration, ApplicationContext> {
/**
* Create a new {@code LruCache} with the supplied initial capacity and
* load factor.
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
*/
LruCache(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
@Override
protected boolean removeEldestEntry(Map.Entry<MergedContextConfiguration, ApplicationContext> eldest) {
if (this.size() > DefaultContextCache.this.getMaxSize()) {
// Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally
// invoke java.util.Map.remove(Object, Object).
DefaultContextCache.this.remove(eldest.getKey(), HierarchyMode.CURRENT_LEVEL);
}
// Return false since we invoke a custom eviction algorithm.
return false;
}
}
}