Skip to content

Commit dbe89ab

Browse files
committed
Pause unused application contexts in the TestContext framework
Since the introduction of the Spring TestContext Framework in 2007, application contexts have always been stored in the context cache in a "running" state. However, leaving a context running means that components in the context may continue to run in the background. For example, JMS listeners may continue to consume messages from a queue; scheduled tasks may continue to perform active work, etc.; and this can lead to issues within a test suite. To address such issues, this commit introduces built-in support for pausing application contexts when they are not in use and restarting them if they are needed again. Specifically, the TestContextManager now marks a test's application context as "unused" after execution of the test class has ended, and the underlying ContextCache then "stops" the application context if no other test class is currently using the context. When a TestExecutionListener later attempts to obtain a paused application context -- for example, for a subsequent test class that shares the same application context -- the ContextCache ensures that context is restarted before returning it. See spring-projects/spring-boot#28312 See gh-35171 Closes gh-35168
1 parent f3757ce commit dbe89ab

23 files changed

+1031
-160
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ 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+
stopped 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.
73+
6574
Since having a large number of application contexts loaded within a given test suite can
6675
cause the suite to take an unnecessarily long time to run, it is often beneficial to
6776
know exactly how many contexts have been loaded and cached. To view the statistics for

spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
* {@link org.springframework.test.context.cache.ContextCache ContextCache}
3030
* behind the scenes.
3131
*
32+
* <p>As of Spring Framework 7.0, this SPI includes optional support for
33+
* {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and
34+
* {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering}
35+
* context usage.
36+
*
3237
* <p>Note: {@code CacheAwareContextLoaderDelegate} does not extend the
3338
* {@link ContextLoader} or {@link SmartContextLoader} interface.
3439
*
@@ -142,4 +147,38 @@ default boolean isContextLoaded(MergedContextConfiguration mergedConfig) {
142147
*/
143148
void closeContext(MergedContextConfiguration mergedConfig, @Nullable HierarchyMode hierarchyMode);
144149

150+
/**
151+
* Register usage of the {@linkplain ApplicationContext application context}
152+
* for the supplied {@link MergedContextConfiguration} as well as usage of the
153+
* application context for its {@linkplain MergedContextConfiguration#getParent()
154+
* parent}, recursively.
155+
* <p>This is intended to be invoked whenever a
156+
* {@link org.springframework.test.context.TestExecutionListener TestExecutionListener}
157+
* interacts with the application context(s) on behalf of the supplied test class.
158+
* @param key the context key; never {@code null}
159+
* @param testClass the test class that is using the application context(s)
160+
* @since 7.0
161+
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
162+
*/
163+
default void registerContextUsage(MergedContextConfiguration key, Class<?> testClass) {
164+
/* no-op */
165+
}
166+
167+
/**
168+
* Unregister usage of the {@linkplain ApplicationContext application context}
169+
* for the supplied {@link MergedContextConfiguration} as well as usage of the
170+
* application context for its {@linkplain MergedContextConfiguration#getParent()
171+
* parent}, recursively.
172+
* <p>This informs the {@code ContextCache} that the application context(s) can
173+
* be safely {@linkplain org.springframework.context.Lifecycle#stop() stopped}
174+
* if no other test classes are actively using the same application context(s).
175+
* @param key the context key; never {@code null}
176+
* @param testClass the test class that is no longer using the application context(s)
177+
* @since 7.0
178+
* @see #registerContextUsage(MergedContextConfiguration, Class)
179+
*/
180+
default void unregisterContextUsage(MergedContextConfiguration key, Class<?> testClass) {
181+
/* no-op */
182+
}
183+
145184
}

spring-test/src/main/java/org/springframework/test/context/TestContext.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ public interface TestContext extends AttributeAccessor, Serializable {
5353
* Determine if the {@linkplain ApplicationContext application context} for
5454
* this test context is known to be available.
5555
* <p>If this method returns {@code true}, a subsequent invocation of
56-
* {@link #getApplicationContext()} should succeed.
56+
* {@link #getApplicationContext()} or {@link #markApplicationContextUnused()}
57+
* should succeed.
5758
* <p>The default implementation of this method always returns {@code false}.
5859
* Custom {@code TestContext} implementations are therefore highly encouraged
5960
* to override this method with a more meaningful implementation. Note that
@@ -62,6 +63,7 @@ public interface TestContext extends AttributeAccessor, Serializable {
6263
* @return {@code true} if the application context has already been loaded
6364
* @since 5.2
6465
* @see #getApplicationContext()
66+
* @see #markApplicationContextUnused()
6567
*/
6668
default boolean hasApplicationContext() {
6769
return false;
@@ -77,6 +79,7 @@ default boolean hasApplicationContext() {
7779
* @throws IllegalStateException if an error occurs while retrieving the
7880
* application context
7981
* @see #hasApplicationContext()
82+
* @see #markApplicationContextUnused()
8083
*/
8184
ApplicationContext getApplicationContext();
8285

@@ -128,6 +131,24 @@ default void publishEvent(Function<TestContext, ? extends ApplicationEvent> even
128131
*/
129132
@Nullable Throwable getTestException();
130133

134+
/**
135+
* Call this method to signal that the {@linkplain #getTestClass() test class}
136+
* is no longer using the {@linkplain ApplicationContext application context}
137+
* associated with this test context.
138+
* <p>This informs the context cache that the application context can be
139+
* safely {@linkplain org.springframework.context.Lifecycle#stop() stopped}
140+
* if no other test classes are actively using the same application context.
141+
* <p>This method is intended to be invoked after execution of the test class
142+
* has ended and should not be invoked unless the application context for this
143+
* test context is known to be {@linkplain #hasApplicationContext() available}.
144+
* <p>This feature is primarily intended for use within the framework.
145+
* @since 7.0
146+
* @see TestContextManager#afterTestClass()
147+
*/
148+
default void markApplicationContextUnused() {
149+
/* no-op */
150+
}
151+
131152
/**
132153
* Call this method to signal that the {@linkplain ApplicationContext application
133154
* context} associated with this test context is <em>dirty</em> and should be

spring-test/src/main/java/org/springframework/test/context/TestContextManager.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,10 +520,14 @@ public void afterTestMethod(Object testInstance, Method testMethod, @Nullable Th
520520
* the first exception.
521521
* <p>Note that listeners will be executed in the opposite order in which they
522522
* were registered.
523+
* <p>As of Spring Framework 7.0, this method also ensures that the application
524+
* context for the current {@link #getTestContext() TestContext} is marked as
525+
* {@linkplain TestContext#markApplicationContextUnused() unused}.
523526
* @throws Exception if a registered TestExecutionListener throws an exception
524527
* @since 3.0
525528
* @see #getTestExecutionListeners()
526529
* @see Throwable#addSuppressed(Throwable)
530+
* @see TestContext#markApplicationContextUnused()
527531
*/
528532
public void afterTestClass() throws Exception {
529533
Class<?> testClass = getTestContext().getTestClass();
@@ -550,6 +554,20 @@ public void afterTestClass() throws Exception {
550554
}
551555
}
552556

557+
try {
558+
if (getTestContext().hasApplicationContext()) {
559+
getTestContext().markApplicationContextUnused();
560+
}
561+
}
562+
catch (Throwable ex) {
563+
if (afterTestClassException == null) {
564+
afterTestClassException = ex;
565+
}
566+
else {
567+
afterTestClassException.addSuppressed(ex);
568+
}
569+
}
570+
553571
this.testContextHolder.remove();
554572

555573
if (afterTestClassException != null) {

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

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
* <p>As of Spring Framework 6.1, this SPI includes optional support for
3636
* {@linkplain #getFailureCount(MergedContextConfiguration) tracking} and
3737
* {@linkplain #incrementFailureCount(MergedContextConfiguration) incrementing}
38-
* failure counts.
38+
* failure counts. As of Spring Framework 7.0, this SPI includes optional support for
39+
* {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and
40+
* {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering}
41+
* context usage.
3942
*
4043
* <h3>Rationale</h3>
4144
* <p>Context caching can have significant performance benefits if context
@@ -88,13 +91,19 @@ public interface ContextCache {
8891
boolean contains(MergedContextConfiguration key);
8992

9093
/**
91-
* Obtain a cached {@code ApplicationContext} for the given key.
92-
* <p>The {@linkplain #getHitCount() hit} and {@linkplain #getMissCount() miss}
93-
* counts must be updated accordingly.
94+
* Obtain a cached {@link ApplicationContext} for the given key.
95+
* <p>If the cached application context was previously
96+
* {@linkplain org.springframework.context.Lifecycle#stop() stopped}, it
97+
* must be
98+
* {@linkplain org.springframework.context.support.AbstractApplicationContext#restart()
99+
* restarted}. This applies to parent contexts as well.
100+
* <p>In addition, the {@linkplain #getHitCount() hit} and
101+
* {@linkplain #getMissCount() miss} counts must be updated accordingly.
94102
* @param key the context key (never {@code null})
95103
* @return the corresponding {@code ApplicationContext} instance, or {@code null}
96104
* if not found in the cache
97-
* @see #remove
105+
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
106+
* @see #remove(MergedContextConfiguration, HierarchyMode)
98107
*/
99108
@Nullable ApplicationContext get(MergedContextConfiguration key);
100109

@@ -151,6 +160,64 @@ default int getFailureCount(MergedContextConfiguration key) {
151160
* @see #getFailureCount(MergedContextConfiguration)
152161
*/
153162
default void incrementFailureCount(MergedContextConfiguration key) {
163+
/* no-op */
164+
}
165+
166+
/**
167+
* Register usage of the {@link ApplicationContext} for the supplied
168+
* {@link MergedContextConfiguration} and any of its parents.
169+
* <p>The default implementation of this method does nothing. Concrete
170+
* implementations are therefore highly encouraged to override this
171+
* method, {@link #unregisterContextUsage(MergedContextConfiguration, Class)},
172+
* and {@link #getContextUsageCount()} with appropriate behavior. Note that
173+
* the standard {@code ContextContext} implementation in Spring overrides
174+
* these methods appropriately.
175+
* @param key the context key; never {@code null}
176+
* @param testClass the test class that is using the application context(s)
177+
* @since 7.0
178+
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
179+
* @see #getContextUsageCount()
180+
*/
181+
default void registerContextUsage(MergedContextConfiguration key, Class<?> testClass) {
182+
/* no-op */
183+
}
184+
185+
/**
186+
* Unregister usage of the {@link ApplicationContext} for the supplied
187+
* {@link MergedContextConfiguration} and any of its parents.
188+
* <p>If no other test classes are actively using the same application
189+
* context(s), the application context(s) should be
190+
* {@linkplain org.springframework.context.Lifecycle#stop() stopped}.
191+
* <p>The default implementation of this method does nothing. Concrete
192+
* implementations are therefore highly encouraged to override this
193+
* method, {@link #registerContextUsage(MergedContextConfiguration, Class)},
194+
* and {@link #getContextUsageCount()} with appropriate behavior. Note that
195+
* the standard {@code ContextContext} implementation in Spring overrides
196+
* these methods appropriately.
197+
* @param key the context key; never {@code null}
198+
* @param testClass the test class that is no longer using the application context(s)
199+
* @since 7.0
200+
* @see #registerContextUsage(MergedContextConfiguration, Class)
201+
* @see #getContextUsageCount()
202+
*/
203+
default void unregisterContextUsage(MergedContextConfiguration key, Class<?> testClass) {
204+
/* no-op */
205+
}
206+
207+
/**
208+
* Determine the number of contexts within the cache that are currently in use.
209+
* <p>The default implementation of this method always returns {@code 0}.
210+
* Concrete implementations are therefore highly encouraged to override this
211+
* method, {@link #registerContextUsage(MergedContextConfiguration, Class)},
212+
* and {@link #unregisterContextUsage(MergedContextConfiguration, Class)} with
213+
* appropriate behavior. Note that the standard {@code ContextContext}
214+
* implementation in Spring overrides these methods appropriately.
215+
* @since 7.0
216+
* @see #registerContextUsage(MergedContextConfiguration, Class)
217+
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
218+
*/
219+
default int getContextUsageCount() {
220+
return 0;
154221
}
155222

156223
/**
@@ -204,6 +271,7 @@ default void incrementFailureCount(MergedContextConfiguration key) {
204271
* <ul>
205272
* <li>name of the concrete {@code ContextCache} implementation</li>
206273
* <li>{@linkplain #size}</li>
274+
* <li>{@linkplain #getContextUsageCount() context usage count}</li>
207275
* <li>{@linkplain #getParentContextCount() parent context count}</li>
208276
* <li>{@linkplain #getHitCount() hit count}</li>
209277
* <li>{@linkplain #getMissCount() miss count}</li>

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
* delegating to {@link ContextCacheUtils#retrieveContextFailureThreshold()} to
5959
* obtain the threshold value to use.
6060
*
61+
* <p>As of Spring Framework 7.0, this class provides support for
62+
* {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and
63+
* {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering}
64+
* context usage.
65+
*
6166
* @author Sam Brannen
6267
* @since 4.1
6368
*/
@@ -204,6 +209,22 @@ public void closeContext(MergedContextConfiguration mergedConfig, @Nullable Hier
204209
}
205210
}
206211

212+
@Override
213+
public void registerContextUsage(MergedContextConfiguration mergedConfig, Class<?> testClass) {
214+
mergedConfig = replaceIfNecessary(mergedConfig);
215+
synchronized (this.contextCache) {
216+
this.contextCache.registerContextUsage(mergedConfig, testClass);
217+
}
218+
}
219+
220+
@Override
221+
public void unregisterContextUsage(MergedContextConfiguration mergedConfig, Class<?> testClass) {
222+
mergedConfig = replaceIfNecessary(mergedConfig);
223+
synchronized (this.contextCache) {
224+
this.contextCache.unregisterContextUsage(mergedConfig, testClass);
225+
}
226+
}
227+
207228
/**
208229
* Get the {@link ContextCache} used by this context loader delegate.
209230
*/

0 commit comments

Comments
 (0)