Skip to content

Commit fa40406

Browse files
committed
Avoid unnecessary pausing of application contexts for tests
In commit 9711db7, 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
1 parent 948af8b commit fa40406

10 files changed

Lines changed: 408 additions & 87 deletions

File tree

framework-docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@
343343
**** xref:testing/testcontext-framework/ctx-management/web.adoc[]
344344
**** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[]
345345
**** xref:testing/testcontext-framework/ctx-management/caching.adoc[]
346+
**** xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[]
346347
**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[]
347348
**** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[]
348349
*** xref:testing/testcontext-framework/fixture-di.adoc[]

framework-docs/modules/ROOT/pages/appendix.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching].
126126

127127
| `spring.test.context.cache.pause`
128128
| The pause mode for the context cache in the _Spring TestContext Framework_. See
129-
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching].
129+
xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[Context Pausing].
130130

131131
| `spring.test.context.failure.threshold`
132132
| The failure threshold for errors encountered while attempting to load an `ApplicationContext`

framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,33 +62,6 @@ script by setting a JVM system property named `spring.test.context.cache.maxSize
6262
alternative, you can set the same property via the
6363
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
6464

65-
As of Spring Framework 7.0, an application context stored in the context cache will be
66-
_paused_ when it is no longer actively in use and automatically _restarted_ the next time
67-
the context is retrieved from the cache. Specifically, the latter will restart all
68-
auto-startup beans in the application context, effectively restoring the lifecycle state.
69-
This ensures that background processes within the context are not actively running while
70-
the context is not used by tests. For example, JMS listener containers, scheduled tasks,
71-
and any other components in the context that implement `Lifecycle` or `SmartLifecycle`
72-
will be in a "stopped" state until the context is used again by a test. Note, however,
73-
that `SmartLifecycle` components can opt out of pausing by returning `false` from
74-
`SmartLifecycle#isPauseable()`.
75-
76-
[TIP]
77-
====
78-
If you encounter issues with `Lifecycle` components that cannot or should not opt out of
79-
pausing, or if you discover that your test suite runs more slowly due to the pausing and
80-
restarting of application contexts, you can disable the pausing feature from the command
81-
line or a build script by setting a JVM system property named
82-
`spring.test.context.cache.pause` to `never`. For example:
83-
84-
```shell
85-
-Dspring.test.context.cache.pause=never
86-
```
87-
88-
As an alternative, you can set the same property via the
89-
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
90-
====
91-
9265
Since having a large number of application contexts loaded within a given test suite can
9366
cause the suite to take an unnecessarily long time to run, it is often beneficial to
9467
know exactly how many contexts have been loaded and cached. To view the statistics for
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
[[testcontext-ctx-management-pausing]]
2+
= Context Pausing
3+
4+
As of Spring Framework 7.0, an `ApplicationContext` stored in the context cache (see
5+
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]) may be
6+
_paused_ when it is no longer actively in use and automatically _restarted_ the next time
7+
the context is retrieved from the cache. Specifically, the latter will restart all
8+
auto-startup beans in the application context, effectively restoring the lifecycle state.
9+
This ensures that background processes within the context are not actively running while
10+
the context is not used by tests. For example, JMS listener containers, scheduled tasks,
11+
and any other components in the context that implement `Lifecycle` or `SmartLifecycle`
12+
will be in a "stopped" state until the context is used again by a test. Note, however,
13+
that `SmartLifecycle` components can opt out of pausing by returning `false` from
14+
`SmartLifecycle#isPauseable()`.
15+
16+
You can control whether unused application contexts should be paused by setting the
17+
`PauseMode` to one of the following supported values.
18+
19+
`ALWAYS` :: Always pause inactive application contexts.
20+
`ON_CONTEXT_SWITCH` :: Only pause inactive application contexts if the next context
21+
retrieved from the context cache is a different context.
22+
`NEVER` :: Never pause inactive application contexts, effectively disabling the pausing
23+
feature of the context cache.
24+
25+
The `PauseMode` defaults to `ON_CONTEXT_SWITCH`, but it can be changed from the command
26+
line or a build script by setting a JVM system property named
27+
`spring.test.context.cache.pause` to one of the supported values (case insensitive). As
28+
an alternative, you can set the property via the
29+
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
30+
31+
For example, if you encounter issues with `Lifecycle` components that cannot or should
32+
not opt out of pausing, or if you discover that your test suite runs more slowly due to
33+
the pausing and restarting of application contexts, you can disable the pausing feature
34+
by setting the `spring.test.context.cache.pause` property to `never`.
35+
36+
```shell
37+
-Dspring.test.context.cache.pause=never
38+
```
39+
40+
Although `ON_CONTEXT_SWITCH` is the default pause mode, you still have the option to
41+
enable context pausing for all usage scenarios (including context switches) by setting
42+
the `spring.test.context.cache.pause` property to `always`.
43+
44+
```shell
45+
-Dspring.test.context.cache.pause=always
46+
```

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434
*
3535
* <p>A {@code ContextCache} maintains a cache of {@code ApplicationContexts}
3636
* keyed by {@link MergedContextConfiguration} instances, potentially configured
37-
* with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize maximum size} and
38-
* a custom eviction policy.
37+
* with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize maximum size},
38+
* {@linkplain ContextCacheUtils#retrievePauseMode() pause mode}, and custom
39+
* eviction policy.
3940
*
4041
* <p>As of Spring Framework 6.1, this SPI includes optional support for
4142
* {@linkplain #getFailureCount(MergedContextConfiguration) tracking} and
@@ -58,6 +59,7 @@
5859
* @author Juergen Hoeller
5960
* @since 4.2
6061
* @see ContextCacheUtils#retrieveMaxCacheSize()
62+
* @see ContextCacheUtils#retrievePauseMode()
6163
*/
6264
public interface ContextCache {
6365

@@ -90,8 +92,9 @@ public interface ContextCache {
9092
/**
9193
* System property used to configure whether inactive application contexts
9294
* stored in the {@link ContextCache} should be paused: {@value}.
93-
* <p>Defaults to {@code always}. Set this property to {@code never} to
94-
* disable pausing of inactive application contexts &mdash; for example:
95+
* <p>Defaults to {@code on_context_switch}. Can be set to {@code always} or
96+
* {@code never} to disable pausing of inactive application contexts &mdash;
97+
* for example:
9598
* <p>{@code -Dspring.test.context.cache.pause=never}
9699
* <p>May alternatively be configured via the
97100
* {@link org.springframework.core.SpringProperties} mechanism.
@@ -366,6 +369,7 @@ interface LoadFunction {
366369
*
367370
* @since 7.0.3
368371
* @see #ALWAYS
372+
* @see #ON_CONTEXT_SWITCH
369373
* @see #NEVER
370374
* @see ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME
371375
*/
@@ -376,6 +380,12 @@ enum PauseMode {
376380
*/
377381
ALWAYS,
378382

383+
/**
384+
* Only pause inactive application contexts if the next context
385+
* retrieved from the cache is a different context.
386+
*/
387+
ON_CONTEXT_SWITCH,
388+
379389
/**
380390
* Never pause inactive application contexts, effectively disabling the
381391
* pausing feature of the {@link ContextCache}.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ public static int retrieveContextFailureThreshold() {
6565
* Retrieve the {@link PauseMode} for the {@link ContextCache}.
6666
* <p>Uses {@link SpringProperties} to retrieve a system property or Spring
6767
* property named {@value ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME}.
68-
* <p>Defaults to {@link PauseMode#ALWAYS} if no such property has been set.
68+
* <p>Defaults to {@link PauseMode#ON_CONTEXT_SWITCH} if no such property has
69+
* been set.
6970
* @return the configured or default {@code PauseMode}
7071
* @since 7.0.3
7172
* @see ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME
@@ -81,7 +82,7 @@ public static PauseMode retrievePauseMode() {
8182
}
8283
return pauseMode;
8384
}
84-
return PauseMode.ALWAYS;
85+
return PauseMode.ON_CONTEXT_SWITCH;
8586
}
8687

8788
private static int retrieveProperty(String key, int defaultValue) {

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

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.HashSet;
2222
import java.util.Iterator;
2323
import java.util.LinkedHashMap;
24+
import java.util.LinkedHashSet;
2425
import java.util.List;
2526
import java.util.Map;
2627
import java.util.Set;
@@ -86,6 +87,13 @@ public class DefaultContextCache implements ContextCache {
8687
*/
8788
private final Map<MergedContextConfiguration, Set<Class<?>>> contextUsageMap = new ConcurrentHashMap<>(32);
8889

90+
/**
91+
* Set of keys for contexts that are currently unused and are therefore
92+
* candidates for pausing on context switch.
93+
* @since 7.0.3
94+
*/
95+
private final Set<MergedContextConfiguration> unusedContexts = new LinkedHashSet<>(4);
96+
8997
/**
9098
* Map of context keys to context load failure counts.
9199
* @since 6.1
@@ -166,6 +174,7 @@ public boolean contains(MergedContextConfiguration key) {
166174
}
167175
else {
168176
this.hitCount.incrementAndGet();
177+
pauseOnContextSwitchIfNecessary(key);
169178
restartContextIfNecessary(context);
170179
}
171180
return context;
@@ -191,6 +200,7 @@ public void put(MergedContextConfiguration key, ApplicationContext context) {
191200
Assert.notNull(context, "ApplicationContext must not be null");
192201

193202
evictLruContextIfNecessary();
203+
pauseOnContextSwitchIfNecessary(key);
194204
putInternal(key, context);
195205
}
196206

@@ -200,6 +210,7 @@ public ApplicationContext put(MergedContextConfiguration key, LoadFunction loadF
200210
Assert.notNull(loadFunction, "LoadFunction must not be null");
201211

202212
evictLruContextIfNecessary();
213+
pauseOnContextSwitchIfNecessary(key);
203214
ApplicationContext context = loadFunction.loadContext(key);
204215
Assert.state(context != null, "LoadFunction must return a non-null ApplicationContext");
205216
putInternal(key, context);
@@ -253,9 +264,9 @@ public void unregisterContextUsage(MergedContextConfiguration mergedConfig, Clas
253264
Set<Class<?>> activeTestClasses = getActiveTestClasses(mergedConfig);
254265
activeTestClasses.remove(testClass);
255266
if (activeTestClasses.isEmpty()) {
256-
if ((this.pauseMode == PauseMode.ALWAYS) &&
257-
(context instanceof ConfigurableApplicationContext cac && cac.isRunning())) {
258-
cac.pause();
267+
switch (this.pauseMode) {
268+
case ALWAYS -> pauseIfNecessary(context);
269+
case ON_CONTEXT_SWITCH -> this.unusedContexts.add(mergedConfig);
259270
}
260271
this.contextUsageMap.remove(mergedConfig);
261272
}
@@ -271,6 +282,38 @@ private Set<Class<?>> getActiveTestClasses(MergedContextConfiguration mergedConf
271282
return this.contextUsageMap.computeIfAbsent(mergedConfig, key -> new HashSet<>());
272283
}
273284

285+
private boolean pauseOnContextSwitch() {
286+
return (this.pauseMode == PauseMode.ON_CONTEXT_SWITCH);
287+
}
288+
289+
private void pauseOnContextSwitchIfNecessary(MergedContextConfiguration activeContextKey) {
290+
if (pauseOnContextSwitch()) {
291+
removeFromUnusedContexts(activeContextKey);
292+
for (MergedContextConfiguration unusedContextKey : this.unusedContexts) {
293+
pauseIfNecessary(this.contextMap.get(unusedContextKey));
294+
}
295+
this.unusedContexts.clear();
296+
}
297+
}
298+
299+
/**
300+
* Remove the supplied key and any keys for parent contexts from the unused
301+
* contexts set. This effectively stops tracking the context (or context
302+
* hierarchy) as unused.
303+
*/
304+
private void removeFromUnusedContexts(MergedContextConfiguration key) {
305+
do {
306+
this.unusedContexts.remove(key);
307+
key = key.getParent();
308+
} while (key != null);
309+
}
310+
311+
private static void pauseIfNecessary(@Nullable ApplicationContext context) {
312+
if (context instanceof ConfigurableApplicationContext cac && cac.isRunning()) {
313+
cac.pause();
314+
}
315+
}
316+
274317
@Override
275318
public void remove(MergedContextConfiguration key, @Nullable HierarchyMode hierarchyMode) {
276319
Assert.notNull(key, "Key must not be null");
@@ -322,6 +365,9 @@ private void remove(List<MergedContextConfiguration> removedContexts, MergedCont
322365
// stack as opposed to prior to the recursive call).
323366
ApplicationContext context = this.contextMap.remove(key);
324367
this.contextUsageMap.remove(key);
368+
if (pauseOnContextSwitch()) {
369+
this.unusedContexts.remove(key);
370+
}
325371
if (context instanceof ConfigurableApplicationContext cac) {
326372
cac.close();
327373
}
@@ -387,6 +433,7 @@ public void clear() {
387433
this.contextMap.clear();
388434
this.hierarchyMap.clear();
389435
this.contextUsageMap.clear();
436+
this.unusedContexts.clear();
390437
}
391438
}
392439

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,55 @@ void topLevelTestClassesWithPauseModeAlways() {
103103
clearApplicationEvents();
104104
}
105105

106+
@Test
107+
void topLevelTestClassesWithPauseModeOnContextSwitch() {
108+
this.contextCache = new DefaultContextCache(DEFAULT_MAX_CONTEXT_CACHE_SIZE, PauseMode.ON_CONTEXT_SWITCH);
109+
110+
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 0, 1);
111+
assertThat(EventTracker.events).containsExactly("ContextRefreshed:TestCase1A");
112+
clearApplicationEvents();
113+
114+
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 1, 1);
115+
assertThat(EventTracker.events).isEmpty();
116+
clearApplicationEvents();
117+
118+
loadCtxAndAssertStats(TestCase1B.class, 1, 1, 2, 1);
119+
assertThat(EventTracker.events).isEmpty();
120+
clearApplicationEvents();
121+
122+
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 3, 1);
123+
assertThat(EventTracker.events).isEmpty();
124+
clearApplicationEvents();
125+
126+
loadCtxAndAssertStats(TestCase2.class, 2, 1, 3, 2);
127+
assertThat(EventTracker.events).containsExactly("ContextPaused:TestCase1A", "ContextRefreshed:TestCase2");
128+
clearApplicationEvents();
129+
130+
loadCtxAndAssertStats(TestCase1B.class, 2, 1, 4, 2);
131+
assertThat(EventTracker.events).containsExactly("ContextPaused:TestCase2", "ContextRestarted:TestCase1A");
132+
clearApplicationEvents();
133+
134+
loadCtxAndAssertStats(TestCase1A.class, 2, 1, 5, 2);
135+
assertThat(EventTracker.events).isEmpty();
136+
clearApplicationEvents();
137+
138+
loadCtxAndAssertStats(TestCase2.class, 2, 1, 6, 2);
139+
assertThat(EventTracker.events).containsExactly("ContextPaused:TestCase1A", "ContextRestarted:TestCase2");
140+
clearApplicationEvents();
141+
142+
loadCtxAndAssertStats(TestCase2.class, 2, 1, 7, 2);
143+
assertThat(EventTracker.events).isEmpty();
144+
clearApplicationEvents();
145+
146+
markContextDirty(TestCase2.class);
147+
assertThat(EventTracker.events).containsExactly("ContextClosed:TestCase2");
148+
clearApplicationEvents();
149+
150+
loadCtxAndAssertStats(TestCase2.class, 2, 1, 7, 3);
151+
assertThat(EventTracker.events).containsExactly("ContextRefreshed:TestCase2");
152+
clearApplicationEvents();
153+
}
154+
106155
@Test
107156
void topLevelTestClassesWithPauseModeNever() {
108157
this.contextCache = new DefaultContextCache(DEFAULT_MAX_CONTEXT_CACHE_SIZE, PauseMode.NEVER);

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ class PauseModeTests {
112112
"\talways\u000B"
113113
};
114114

115+
static final String[] ON_CONTEXT_SWITCH_VALUES = {
116+
"on_context_switch",
117+
"On_Context_Switch",
118+
"ON_CONTEXT_SWITCH",
119+
"\ton_context_switch\u000B"
120+
};
121+
115122
static final String[] NEVER_VALUES = {
116123
"never",
117124
"Never",
@@ -129,7 +136,7 @@ void clearProperties() {
129136

130137
@Test
131138
void retrievePauseModeFromDefault() {
132-
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
139+
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ON_CONTEXT_SWITCH);
133140
}
134141

135142
@Test
@@ -151,6 +158,13 @@ void retrievePauseModeFromSystemPropertyWithValueAlways(String value) {
151158
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
152159
}
153160

161+
@ParameterizedTest
162+
@FieldSource("ON_CONTEXT_SWITCH_VALUES")
163+
void retrievePauseModeFromSystemPropertyWithValueOnContextSwitch(String value) {
164+
System.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, value);
165+
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ON_CONTEXT_SWITCH);
166+
}
167+
154168
@ParameterizedTest
155169
@FieldSource("NEVER_VALUES")
156170
void retrievePauseModeFromSystemPropertyWithValueNever(String value) {
@@ -165,6 +179,13 @@ void retrievePauseModeFromSpringPropertyWithValueAlways(String value) {
165179
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
166180
}
167181

182+
@ParameterizedTest
183+
@FieldSource("ON_CONTEXT_SWITCH_VALUES")
184+
void retrievePauseModeFromSpringPropertyWithValueOnContextSwitch(String value) {
185+
SpringProperties.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, value);
186+
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ON_CONTEXT_SWITCH);
187+
}
188+
168189
@ParameterizedTest
169190
@FieldSource("NEVER_VALUES")
170191
void retrievePauseModeFromSpringPropertyWithValueNever(String value) {

0 commit comments

Comments
 (0)