Overview

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, we should introduce built-in support for pausing application contexts when they are not in use and restarting them if they are needed again.

Related Issues

  • https://github.com/spring-projects/spring-boot/issues/28312
  • 35171

Original Issue Description

There have been several reports of message listeners still actively listening while being the in the application context cache during integration tests. There does not seem to be a mechanism to keep these listener beans in the cache while also switching off the listening.

Would it make sense to extend the SmartLifecycle interface?

Something like onEnterActiveContext and onLeaveActiveContext could be intercepted in the listener beans to toggle the listening. These methods would be called when a bean moves into or out of the active application context. It's not the same as start and stop, which are only called when the bean is loaded for the first time and when the application shuts down.

Comment From: jhoeller

Actually, stop() and re-start() is a quite good fit when becoming inactive and then active again. Those are not just for startup and shutdown, they also allow for intermediate pausing and restarting, with a lot of refinements having gone into that in 6.1 in particular (not least of it all for CRaC snapshots).

ConfigurableApplicationContext itself extends Lifecycle for that reason, providing context-level stop() and start() methods that propagate to all contained Lifecycle beans. We could possibly call those methods when a context becomes inactive (entering the context cache) and then active again (taken out of the context cache).

Comment From: fabian-flechtmann

Thanks for getting back to me!

That sounds like a good fit. The listener i'm working with is already reacting to start() and stop() as you propose and others probably are as well.

Do you think it would break any existing behavior to call these methods when a context becomes inactive?

Comment From: jhoeller

I see it as a pretty straightforward step for the state of the context itself.

That said, the Test Context Framework needs to be able to track when a context actually becomes inactive. At the moment, multiple tests can use the same context from the cache without 'returning' it, so there is no actual tracking when a context becomes inactive (in the sense of not being used by any test at the moment). I need to discuss this with @sbrannen, let's see what we can do there.

Comment From: sbrannen

After discussing this with @jhoeller, we have decided that it is worthwhile investigating whether we can stop an inactive ApplicationContext in the TestContext framework.

This could potentially be achieved via the following.

  • Track "active" contexts in the ContextCache via some sort of active usage counter.
  • Increment the active usage counter and restart any stopped ApplicationContext whenever TestContext.getApplicationContext() is invoked for a given test class.
  • Decrement the active usage counter whenever a test class completes (for example, via an "after test class" callback).
  • If the decrement results in the active usage counter dropping to zero, stop the ApplicationContext.

Figuring out when TestContext.getApplicationContext() is invoked the first time for a given test class is likely not possible without tracking the "active test class" along with the "active usage counter". Thus, we might need some form of concurrent data structure (or a structure within DefaultContextCache) for that purpose. Actually, we might be able to track only the "active test classes" instead of any actual counter.

Comment From: sbrannen

As a side note, this is somewhat related to the following issue which resulted in the introduction of test events such as BeforeTestClassEvent and AfterTestClassEvent.

  • 18490

Comment From: sbrannen

Current work on this issue can be viewed in the following feature branch.

https://github.com/spring-projects/spring-framework/compare/main...sbrannen:spring-framework:issues/gh-35168-pause-inactive-ApplicationContext

Comment From: sbrannen

This has been implemented in commit dbe89abd7b39217044b3e021b4fe740c7ef88a4b for inclusion in Spring Framework 7.0 M7.

Comment From: fabian-flechtmann

https://github.com/spring-projects/spring-security/issues/17543 makes me wonder if this implementation might break other existing libraries. Before, libraries as far as I know did not have to support calling start() and stop() multiple times.

What do you think about adding a Pauseable interface for beans? We could use the logic of the current implementation to call pause() and resume() on any beans that implement this interface. This would leave existing behavior unchanged and give libraries a new hook.

Comment From: jhoeller

@fabian-flechtmann

Pausing has always been part of the Lifecycle.start/stop contract (which originally got introduced for message listener containers which supported stop/restart from day one), and has been enforced rather strongly in 6.1 as part of our larger lifecycle support revision (driven by CRaC compability but also for general purposes, with executors and schedulers pausing now etc). This new pausing in the Test Context Framework essentially uses the long-existing ConfigurationApplicationContext.start/stop arrangement.

That said, without any explicit calls to ConfigurationApplicationContext.start/stop on the user's side, an incomplete Lifecycle implementation in a library might have remained unnoticed before. In 7.0, we are making this more obvious now, so there is indeed a risk of some Lifecycle implementations having to be tightened. While we expect some hiccups, there are no new rules here, just enforcement of the same existing Lifecycle contract. A separate contract would be awkward next to that 6.1+ state of the art.

Comment From: fabian-flechtmann

Thanks @jhoeller, I'm just asking to make sure that some kind of paused state can exist where a bean can suspend its activity but also keep its system resources like ports reserved for later use.

To clarify, is stop() intended to make a bean enter such a state? I did not find this explicitly stated in the documentation of Lifecycle or SmartLifecycle.

Comment From: jhoeller

@fabian-flechtmann this is effectively left up to the individual Lifecycle implementation. Let me elaborate a bit.

The general contract just expects a stop()/start() sequence to restore the same effective state after pausing (where the actual meaning of "pausing" is not concretely defined). If this can be achieved through un-binding and re-binding the same port, it's recommendable to temporarily release the port. If it is necessary to hold on to a port (or other system resource) in order to be able to restore the same effective state afterwards, it is ok to hold on to it, possibly just pausing some local processing where possible.

A stop()/start() sequence is effectively best-effort: The most important requirement is to suspend background activity as far as possible, whereas the release of resources is a bonus. If all you do is hold on to a resource, you can simply shortcut stop() with an early return if there is nothing you can actually stop (that's what Spring Security ended up doing for ephemeral port scenarios). In that case, isRunning() can keep returning true which means that the container won't even call start() again on context restart.

That's also a key difference between stop() and destroy(): Stopping means best-effort suspension of activities, either early on shutdown (for graceful shutdown purposes) or for pausing (leaving room for a potential restart); the steps taken should make sense in both cases. Whereas destroy() is about unconditionally releasing all opened resources before the application runtime ends; destroy() should be able to follow up on a preceding stop() sequence but not hard-assume that stop() has been called before.

Last but not least, there are the CRaC requirements which are stronger but not mandatory for all components in all scenarios. For CRaC compatibility, stop() must release all system resources and stop all background threads. This is a bonus requirement that should ideally be met - but if for certain configurations it cannot be met, it's ok to skip it (ideally documenting the settings that are effectively CRaC compatible). So for ports, there might be CRaC compatibility for a pre-defined port but not for ephemeral ports.

Hope that makes sense :-)

Comment From: jhoeller

One more note: If a stop() implementation must differentiate between early shutdown versus pausing, it can take the containing ConfigurableApplicationContext (from ApplicationContextAware or autowiring) and check isClosed() on it (which will only be true in the shutdown case). However, ideally a stop() implementation should not have to differentiate at all but rather leave shutdown-only steps to a destroy() implementation on the same component.