Skip to content

Commit

Permalink
Add property to stop the JVM from exiting
Browse files Browse the repository at this point in the history
spring.main.keep-alive=true will spawn a non-daemon thread which stops
if the context is closed

Closes gh-37736
  • Loading branch information
mhalbritter committed Oct 12, 2023
1 parent 6880fb0 commit fcf77ed
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,17 @@ Spring Boot ships with the `BufferingApplicationStartup` variant; this implement
Applications can ask for the bean of type `BufferingApplicationStartup` in any component.

Spring Boot can also be configured to expose a {spring-boot-actuator-restapi-docs}/#startup[`startup` endpoint] that provides this information as a JSON document.



[[features.spring-application.virtual-threads]]
=== Virtual threads
If you're running on Java 21 or up, you can enable virtual threads by setting the property configprop:spring.threads.virtual.enabled[] to `true`.

WARNING: One side effect of virtual threads is that these threads are daemon threads.
A JVM will exit if there are no non-daemon threads.
This behavior can be a problem when you rely on, e.g. `@Scheduled` beans to keep your application alive.
If you use virtual threads, the scheduler thread is a virtual thread and therefore a daemon thread and won't keep the JVM alive.
This does not only affect scheduling, but can be the case with other technologies, too!
To keep the JVM running in all cases, it is recommended to set the property configprop:spring.main.keep-alive[] to `true`.
This ensures that the JVM is kept alive, even if all threads are virtual threads.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.aot.AotApplicationContextInitializer;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.GenericTypeResolver;
Expand Down Expand Up @@ -163,6 +164,7 @@
* @author Brian Clozel
* @author Ethan Rubinson
* @author Chris Bono
* @author Moritz Halbritter
* @since 1.0.0
* @see #run(Class, String[])
* @see #run(Class[], String[])
Expand Down Expand Up @@ -240,6 +242,8 @@ public class SpringApplication {

private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;

private boolean keepAlive;

/**
* Create a new {@link SpringApplication} instance. The application context will load
* beans from the specified primary sources (see {@link SpringApplication class-level}
Expand Down Expand Up @@ -409,6 +413,11 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
if (this.keepAlive) {
KeepAlive keepAlive = new KeepAlive();
keepAlive.start();
context.addApplicationListener(keepAlive);
}
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
if (!AotDetector.useGeneratedArtifacts()) {
// Load the sources
Expand Down Expand Up @@ -1277,6 +1286,26 @@ public ApplicationStartup getApplicationStartup() {
return this.applicationStartup;
}

/**
* Whether to keep the application alive even if there are no more non-daemon threads.
* @return whether to keep the application alive even if there are no more non-daemon
* threads
* @since 3.2.0
*/
public boolean isKeepAlive() {
return this.keepAlive;
}

/**
* Whether to keep the application alive even if there are no more non-daemon threads.
* @param keepAlive whether to keep the application alive even if there are no more
* non-daemon threads
* @since 3.2.0
*/
public void setKeepAlive(boolean keepAlive) {
this.keepAlive = keepAlive;
}

/**
* Return a {@link SpringApplicationShutdownHandlers} instance that can be used to add
* or remove handlers that perform actions before the JVM is shutdown.
Expand Down Expand Up @@ -1601,4 +1630,34 @@ public SpringApplicationRunListener getRunListener(SpringApplication springAppli

}

/**
* A non-daemon thread to keep the JVM alive. Reacts to {@link ContextClosedEvent} to
* stop itself when the application context is closed.
*/
private static final class KeepAlive extends Thread implements ApplicationListener<ContextClosedEvent> {

KeepAlive() {
setName("keep-alive");
setDaemon(false);
}

@Override
public void onApplicationEvent(ContextClosedEvent event) {
interrupt();
}

@Override
public void run() {
while (true) {
try {
Thread.sleep(Long.MAX_VALUE);
}
catch (InterruptedException ex) {
break;
}
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,13 @@
"type": "org.springframework.boot.cloud.CloudPlatform",
"description": "Override the Cloud Platform auto-detection."
},
{
"name": "spring.main.keep-alive",
"type": "java.lang.Boolean",
"sourceType": "org.springframework.boot.SpringApplication",
"description": "Whether to keep the application alive even if there are no more non-daemon threads.",
"defaultValue": false
},
{
"name": "spring.main.lazy-initialization",
"type": "java.lang.Boolean",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
* @author Nguyen Bao Sach
* @author Chris Bono
* @author Sebastien Deleuze
* @author Moritz Halbritter
*/
@ExtendWith(OutputCaptureExtension.class)
class SpringApplicationTests {
Expand Down Expand Up @@ -1390,6 +1391,30 @@ void fromWithMultipleApplicationsOnlyAppliesAdditionalSourcesOnce() {
assertThatNoException().isThrownBy(() -> this.context.getBean(SingleUseAdditionalConfig.class));
}

@Test
void shouldStartDaemonThreadIfKeepAliveIsEnabled() {
SpringApplication application = new SpringApplication(ExampleConfig.class);
application.setWebApplicationType(WebApplicationType.NONE);
this.context = application.run("--spring.main.keep-alive=true");
Set<Thread> threads = getCurrentThreads();
assertThat(threads).filteredOn((thread) -> thread.getName().equals("keep-alive"))
.singleElement()
.satisfies((thread) -> assertThat(thread.isDaemon()).isFalse());
}

@Test
void shouldStopKeepAliveThreadIfContextIsClosed() {
SpringApplication application = new SpringApplication(ExampleConfig.class);
application.setWebApplicationType(WebApplicationType.NONE);
application.setKeepAlive(true);
this.context = application.run();
Set<Thread> threadsBeforeClose = getCurrentThreads();
assertThat(threadsBeforeClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isNotEmpty();
this.context.close();
Set<Thread> threadsAfterClose = getCurrentThreads();
assertThat(threadsAfterClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isEmpty();
}

private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
S state) {
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)
Expand Down Expand Up @@ -1432,6 +1457,10 @@ public boolean matches(ConfigurableApplicationContext value) {
};
}

private Set<Thread> getCurrentThreads() {
return Thread.getAllStackTraces().keySet();
}

static class TestEventListener<E extends ApplicationEvent> implements SmartApplicationListener {

private final Class<E> eventType;
Expand Down

0 comments on commit fcf77ed

Please sign in to comment.