Introduce Spring property to disable context pausing for tests

Spring Framework 7.0 introduced support for pausing inactive
application contexts between test classes and restarting them once they
are needed again. If pausing and restarting are fast, this feature does
not have a negative impact on test suites.

However, if the pausing or restarting of certain Lifecycle components
in the application context is slow, that can have a negative impact on
the duration of the overall test suite.

In gh-36044, we hope to find a way to avoid unnecessarily pausing an
application context after a test class if the same context is used by
the next test class that is run. That should help reduce the risk of a
negative impact caused by the pause/restart feature; however, for
certain scenarios that may not be enough. In light of that, this commit
introduces a mechanism for completely disabling the pausing feature via
a Spring property or JVM system property, as follows.

-Dspring.test.context.cache.pause=never

See gh-35168
See gh-36044
Closes gh-36117
This commit is contained in:
Sam Brannen
2026-01-09 12:01:02 +01:00
parent d3a385d222
commit 9711db787e
9 changed files with 487 additions and 45 deletions
@@ -16,6 +16,10 @@
package org.springframework.test.context.cache;
import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.context.ApplicationContext;
@@ -83,6 +87,24 @@ public interface ContextCache {
*/
String MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME = "spring.test.context.cache.maxSize";
/**
* System property used to configure whether inactive application contexts
* stored in the {@link ContextCache} should be paused: {@value}.
* <p>Defaults to {@code always}. Set this property to {@code never} to
* disable pausing of inactive application contexts &mdash; for example:
* <p>{@code -Dspring.test.context.cache.pause=never}
* <p>May alternatively be configured via the
* {@link org.springframework.core.SpringProperties} mechanism.
* <p>Note that implementations of {@code ContextCache} are not required to
* support context pausing. Consult the documentation of the corresponding
* implementation for details.
* @since 7.0.3
* @see PauseMode
* @see org.springframework.context.ConfigurableApplicationContext#pause()
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
*/
String CONTEXT_CACHE_PAUSE_PROPERTY_NAME = "spring.test.context.cache.pause";
/**
* Determine whether there is a cached context for the given key.
@@ -221,7 +243,8 @@ public interface ContextCache {
* {@link MergedContextConfiguration} and any of its parents.
* <p>If no other test classes are actively using the same application
* context(s), the application context(s) should be
* {@linkplain org.springframework.context.ConfigurableApplicationContext#pause() paused}.
* {@linkplain org.springframework.context.ConfigurableApplicationContext#pause()
* paused} according to the configured {@link PauseMode}.
* <p>The default implementation of this method does nothing. Concrete
* implementations are therefore highly encouraged to override this
* method, {@link #registerContextUsage(MergedContextConfiguration, Class)},
@@ -336,4 +359,53 @@ public interface ContextCache {
}
/**
* Enumeration of <em>modes</em> that dictate whether inactive application contexts
* stored in the {@link ContextCache} should be
* {@linkplain org.springframework.context.ConfigurableApplicationContext#pause() paused}.
*
* @since 7.0.3
* @see #ALWAYS
* @see #NEVER
* @see ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME
*/
enum PauseMode {
/**
* Always pause inactive application contexts.
*/
ALWAYS,
/**
* Never pause inactive application contexts, effectively disabling the
* pausing feature of the {@link ContextCache}.
*/
NEVER;
/**
* Get the {@code PauseMode} enum constant with the supplied name,
* {@linkplain String#strip() stripped} and ignoring case.
* @param name the name of the enum constant to retrieve
* @return the corresponding enum constant or {@code null} if not found
* @see PauseMode#valueOf(String)
*/
public static @Nullable PauseMode from(@Nullable String name) {
if (name == null) {
return null;
}
try {
return PauseMode.valueOf(name.strip().toUpperCase(Locale.ROOT));
}
catch (IllegalArgumentException ex) {
Log logger = LogFactory.getLog(PauseMode.class);
if (logger.isDebugEnabled()) {
logger.debug("Failed to parse PauseMode from '%s': %s"
.formatted(name, ex.getMessage()));
}
return null;
}
}
}
}
@@ -18,6 +18,7 @@ package org.springframework.test.context.cache;
import org.springframework.core.SpringProperties;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.cache.ContextCache.PauseMode;
import org.springframework.util.StringUtils;
/**
@@ -60,6 +61,29 @@ public abstract class ContextCacheUtils {
return retrieveProperty(propertyName, defaultValue);
}
/**
* Retrieve the {@link PauseMode} for the {@link ContextCache}.
* <p>Uses {@link SpringProperties} to retrieve a system property or Spring
* property named {@value ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME}.
* <p>Defaults to {@link PauseMode#ALWAYS} if no such property has been set.
* @return the configured or default {@code PauseMode}
* @since 7.0.3
* @see ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME
* @see PauseMode#from(String)
*/
public static PauseMode retrievePauseMode() {
String value = SpringProperties.getProperty(ContextCache.CONTEXT_CACHE_PAUSE_PROPERTY_NAME);
if (StringUtils.hasText(value)) {
PauseMode pauseMode = PauseMode.from(value);
if (pauseMode == null) {
throw new IllegalArgumentException("Unsupported value '%s' for property '%s'"
.formatted(value, ContextCache.CONTEXT_CACHE_PAUSE_PROPERTY_NAME));
}
return pauseMode;
}
return PauseMode.ALWAYS;
}
private static int retrieveProperty(String key, int defaultValue) {
try {
String value = SpringProperties.getProperty(key);
@@ -49,10 +49,15 @@ import org.springframework.util.Assert;
* constructor argument} or set via a system property or Spring property named
* {@value ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME}.
*
* <p>The {@link PauseMode} may be supplied as a {@linkplain #DefaultContextCache(int, PauseMode)
* constructor argument} or set via a system property or Spring property named
* {@value ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME}.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @since 2.5
* @see ContextCacheUtils#retrieveMaxCacheSize()
* @see ContextCacheUtils#retrievePauseMode()
*/
public class DefaultContextCache implements ContextCache {
@@ -91,6 +96,8 @@ public class DefaultContextCache implements ContextCache {
private final int maxSize;
private final PauseMode pauseMode;
private final AtomicInteger hitCount = new AtomicInteger();
private final AtomicInteger missCount = new AtomicInteger();
@@ -98,10 +105,13 @@ public class DefaultContextCache implements ContextCache {
/**
* Create a new {@code DefaultContextCache} using the maximum cache size
* obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}.
* obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()} and the
* {@link PauseMode} obtained via {@link ContextCacheUtils#retrievePauseMode()}.
* @since 4.3
* @see #DefaultContextCache(int)
* @see #DefaultContextCache(int, PauseMode)
* @see ContextCacheUtils#retrieveMaxCacheSize()
* @see ContextCacheUtils#retrievePauseMode()
*/
public DefaultContextCache() {
this(ContextCacheUtils.retrieveMaxCacheSize());
@@ -109,16 +119,35 @@ public class DefaultContextCache implements ContextCache {
/**
* Create a new {@code DefaultContextCache} using the supplied maximum
* cache size.
* cache size and the {@link PauseMode} obtained via
* {@link ContextCacheUtils#retrievePauseMode()}.
* @param maxSize the maximum cache size
* @throws IllegalArgumentException if the supplied {@code maxSize} value
* is not positive
* @since 4.3
* @see #DefaultContextCache()
* @see #DefaultContextCache(int, PauseMode)
* @see ContextCacheUtils#retrievePauseMode()
*/
public DefaultContextCache(int maxSize) {
this(maxSize, ContextCacheUtils.retrievePauseMode());
}
/**
* Create a new {@code DefaultContextCache} using the supplied maximum
* cache size and {@link PauseMode}.
* @param maxSize the maximum cache size
* @param pauseMode the {@code PauseMode} to use
* @throws IllegalArgumentException if the supplied {@code maxSize} value
* is not positive or if the supplied {@code PauseMode} is {@code null}
* @since 7.0.3
* @see #DefaultContextCache()
*/
public DefaultContextCache(int maxSize, PauseMode pauseMode) {
Assert.isTrue(maxSize > 0, "'maxSize' must be positive");
Assert.notNull(pauseMode, "'pauseMode' must not be null");
this.maxSize = maxSize;
this.pauseMode = pauseMode;
}
@@ -222,7 +251,8 @@ public class DefaultContextCache implements ContextCache {
Set<Class<?>> activeTestClasses = getActiveTestClasses(mergedConfig);
activeTestClasses.remove(testClass);
if (activeTestClasses.isEmpty()) {
if (context instanceof ConfigurableApplicationContext cac && cac.isRunning()) {
if ((this.pauseMode == PauseMode.ALWAYS) &&
(context instanceof ConfigurableApplicationContext cac && cac.isRunning())) {
cac.pause();
}
this.contextUsageMap.remove(mergedConfig);