mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Avoid unnecessary pausing of application contexts for tests
In commit 9711db787e, we introduced support for disabling test
application context pausing via a Spring property or JVM system
property, as follows.
-Dspring.test.context.cache.pause=never
However, users may actually be interested in keeping the pausing
feature enabled if contexts are not paused unnecessarily.
To address that, this commit introduces a new
PauseMode.ON_CONTEXT_SWITCH enum constant which is now used by default
in the DefaultContextCache.
With this new pause mode, an unused application context will no longer
be paused immediately. Instead, an unused application context will be
paused lazily the first time a different context is retrieved from or
stored in the ContextCache. This effectively means that an unused
context will not be paused at all if the next test class uses the same
context.
Although ON_CONTEXT_SWITCH is the now the default pause mode, users
still have the option to enable context pausing for all usage scenarios
(not only context switches) by setting the Spring property or JVM
system property to ALWAYS (case insensitive) — for example:
-Dspring.test.context.cache.pause=always
This commit also introduces a dedicated "Context Pausing" section in
the reference manual.
See gh-36117
Closes gh-36044
This commit is contained in:
+14
-4
@@ -34,8 +34,9 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* <p>A {@code ContextCache} maintains a cache of {@code ApplicationContexts}
|
||||
* keyed by {@link MergedContextConfiguration} instances, potentially configured
|
||||
* with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize maximum size} and
|
||||
* a custom eviction policy.
|
||||
* with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize maximum size},
|
||||
* {@linkplain ContextCacheUtils#retrievePauseMode() pause mode}, and custom
|
||||
* eviction policy.
|
||||
*
|
||||
* <p>As of Spring Framework 6.1, this SPI includes optional support for
|
||||
* {@linkplain #getFailureCount(MergedContextConfiguration) tracking} and
|
||||
@@ -58,6 +59,7 @@ import org.springframework.util.Assert;
|
||||
* @author Juergen Hoeller
|
||||
* @since 4.2
|
||||
* @see ContextCacheUtils#retrieveMaxCacheSize()
|
||||
* @see ContextCacheUtils#retrievePauseMode()
|
||||
*/
|
||||
public interface ContextCache {
|
||||
|
||||
@@ -90,8 +92,9 @@ public interface ContextCache {
|
||||
/**
|
||||
* 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 — for example:
|
||||
* <p>Defaults to {@code on_context_switch}. Can be set to {@code always} or
|
||||
* {@code never} to disable pausing of inactive application contexts —
|
||||
* for example:
|
||||
* <p>{@code -Dspring.test.context.cache.pause=never}
|
||||
* <p>May alternatively be configured via the
|
||||
* {@link org.springframework.core.SpringProperties} mechanism.
|
||||
@@ -366,6 +369,7 @@ public interface ContextCache {
|
||||
*
|
||||
* @since 7.0.3
|
||||
* @see #ALWAYS
|
||||
* @see #ON_CONTEXT_SWITCH
|
||||
* @see #NEVER
|
||||
* @see ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME
|
||||
*/
|
||||
@@ -376,6 +380,12 @@ public interface ContextCache {
|
||||
*/
|
||||
ALWAYS,
|
||||
|
||||
/**
|
||||
* Only pause inactive application contexts if the next context
|
||||
* retrieved from the cache is a different context.
|
||||
*/
|
||||
ON_CONTEXT_SWITCH,
|
||||
|
||||
/**
|
||||
* Never pause inactive application contexts, effectively disabling the
|
||||
* pausing feature of the {@link ContextCache}.
|
||||
|
||||
+3
-2
@@ -65,7 +65,8 @@ public abstract class ContextCacheUtils {
|
||||
* 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.
|
||||
* <p>Defaults to {@link PauseMode#ON_CONTEXT_SWITCH} 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
|
||||
@@ -81,7 +82,7 @@ public abstract class ContextCacheUtils {
|
||||
}
|
||||
return pauseMode;
|
||||
}
|
||||
return PauseMode.ALWAYS;
|
||||
return PauseMode.ON_CONTEXT_SWITCH;
|
||||
}
|
||||
|
||||
private static int retrieveProperty(String key, int defaultValue) {
|
||||
|
||||
+50
-3
@@ -21,6 +21,7 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -86,6 +87,13 @@ public class DefaultContextCache implements ContextCache {
|
||||
*/
|
||||
private final Map<MergedContextConfiguration, Set<Class<?>>> contextUsageMap = new ConcurrentHashMap<>(32);
|
||||
|
||||
/**
|
||||
* Set of keys for contexts that are currently unused and are therefore
|
||||
* candidates for pausing on context switch.
|
||||
* @since 7.0.3
|
||||
*/
|
||||
private final Set<MergedContextConfiguration> unusedContexts = new LinkedHashSet<>(4);
|
||||
|
||||
/**
|
||||
* Map of context keys to context load failure counts.
|
||||
* @since 6.1
|
||||
@@ -166,6 +174,7 @@ public class DefaultContextCache implements ContextCache {
|
||||
}
|
||||
else {
|
||||
this.hitCount.incrementAndGet();
|
||||
pauseOnContextSwitchIfNecessary(key);
|
||||
restartContextIfNecessary(context);
|
||||
}
|
||||
return context;
|
||||
@@ -191,6 +200,7 @@ public class DefaultContextCache implements ContextCache {
|
||||
Assert.notNull(context, "ApplicationContext must not be null");
|
||||
|
||||
evictLruContextIfNecessary();
|
||||
pauseOnContextSwitchIfNecessary(key);
|
||||
putInternal(key, context);
|
||||
}
|
||||
|
||||
@@ -200,6 +210,7 @@ public class DefaultContextCache implements ContextCache {
|
||||
Assert.notNull(loadFunction, "LoadFunction must not be null");
|
||||
|
||||
evictLruContextIfNecessary();
|
||||
pauseOnContextSwitchIfNecessary(key);
|
||||
ApplicationContext context = loadFunction.loadContext(key);
|
||||
Assert.state(context != null, "LoadFunction must return a non-null ApplicationContext");
|
||||
putInternal(key, context);
|
||||
@@ -253,9 +264,9 @@ public class DefaultContextCache implements ContextCache {
|
||||
Set<Class<?>> activeTestClasses = getActiveTestClasses(mergedConfig);
|
||||
activeTestClasses.remove(testClass);
|
||||
if (activeTestClasses.isEmpty()) {
|
||||
if ((this.pauseMode == PauseMode.ALWAYS) &&
|
||||
(context instanceof ConfigurableApplicationContext cac && cac.isRunning())) {
|
||||
cac.pause();
|
||||
switch (this.pauseMode) {
|
||||
case ALWAYS -> pauseIfNecessary(context);
|
||||
case ON_CONTEXT_SWITCH -> this.unusedContexts.add(mergedConfig);
|
||||
}
|
||||
this.contextUsageMap.remove(mergedConfig);
|
||||
}
|
||||
@@ -271,6 +282,38 @@ public class DefaultContextCache implements ContextCache {
|
||||
return this.contextUsageMap.computeIfAbsent(mergedConfig, key -> new HashSet<>());
|
||||
}
|
||||
|
||||
private boolean pauseOnContextSwitch() {
|
||||
return (this.pauseMode == PauseMode.ON_CONTEXT_SWITCH);
|
||||
}
|
||||
|
||||
private void pauseOnContextSwitchIfNecessary(MergedContextConfiguration activeContextKey) {
|
||||
if (pauseOnContextSwitch()) {
|
||||
removeFromUnusedContexts(activeContextKey);
|
||||
for (MergedContextConfiguration unusedContextKey : this.unusedContexts) {
|
||||
pauseIfNecessary(this.contextMap.get(unusedContextKey));
|
||||
}
|
||||
this.unusedContexts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the supplied key and any keys for parent contexts from the unused
|
||||
* contexts set. This effectively stops tracking the context (or context
|
||||
* hierarchy) as unused.
|
||||
*/
|
||||
private void removeFromUnusedContexts(MergedContextConfiguration key) {
|
||||
do {
|
||||
this.unusedContexts.remove(key);
|
||||
key = key.getParent();
|
||||
} while (key != null);
|
||||
}
|
||||
|
||||
private static void pauseIfNecessary(@Nullable ApplicationContext context) {
|
||||
if (context instanceof ConfigurableApplicationContext cac && cac.isRunning()) {
|
||||
cac.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(MergedContextConfiguration key, @Nullable HierarchyMode hierarchyMode) {
|
||||
Assert.notNull(key, "Key must not be null");
|
||||
@@ -322,6 +365,9 @@ public class DefaultContextCache implements ContextCache {
|
||||
// stack as opposed to prior to the recursive call).
|
||||
ApplicationContext context = this.contextMap.remove(key);
|
||||
this.contextUsageMap.remove(key);
|
||||
if (pauseOnContextSwitch()) {
|
||||
this.unusedContexts.remove(key);
|
||||
}
|
||||
if (context instanceof ConfigurableApplicationContext cac) {
|
||||
cac.close();
|
||||
}
|
||||
@@ -387,6 +433,7 @@ public class DefaultContextCache implements ContextCache {
|
||||
this.contextMap.clear();
|
||||
this.hierarchyMap.clear();
|
||||
this.contextUsageMap.clear();
|
||||
this.unusedContexts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user