Skip to content

Commit

Permalink
Limit size of context cache in the TestContext framework
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Apr 4, 2016
1 parent 26378cd commit e18d5b5
Show file tree
Hide file tree
Showing 9 changed files with 452 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 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.
Expand Down Expand Up @@ -42,6 +42,7 @@
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
* @see org.springframework.beans.CachedIntrospectionResults#IGNORE_BEANINFO_PROPERTY_NAME
* @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME
* @see org.springframework.test.context.cache.ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME
*/
public abstract class SpringProperties {

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -26,7 +26,9 @@
* 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
Expand All @@ -40,6 +42,7 @@
* @author Sam Brannen
* @author Juergen Hoeller
* @since 4.2
* @see ContextCacheUtils#retrieveMaxCacheSize()
*/
public interface ContextCache {

Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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})
*/
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -37,12 +39,18 @@
/**
* 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 {

Expand All @@ -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>
Expand All @@ -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}
*/
Expand Down Expand Up @@ -181,6 +217,13 @@ public int size() {
return this.contextMap.size();
}

/**
* Get the maximum size of this cache.
*/
public int getMaxSize() {
return this.maxSize;
}

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -210,7 +253,7 @@ public int getMissCount() {
*/
@Override
public void reset() {
synchronized (contextMap) {
synchronized (this.contextMap) {
clear();
clearStatistics();
}
Expand All @@ -221,7 +264,7 @@ public void reset() {
*/
@Override
public void clear() {
synchronized (contextMap) {
synchronized (this.contextMap) {
this.contextMap.clear();
this.hierarchyMap.clear();
}
Expand All @@ -232,7 +275,7 @@ public void clear() {
*/
@Override
public void clearStatistics() {
synchronized (contextMap) {
synchronized (this.contextMap) {
this.hitCount.set(0);
this.missCount.set(0);
}
Expand All @@ -259,10 +302,46 @@ public void logStatistics() {
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;
}
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -42,6 +42,7 @@
* @author Sam Brannen
* @author Michail Nikolaev
* @since 3.1
* @see LruContextCacheTests
* @see SpringRunnerContextCacheTests
*/
public class ContextCacheTests {
Expand Down
Loading

0 comments on commit e18d5b5

Please sign in to comment.