With platform threads, a method annotated with @Scheduled keeps an empty Spring Boot application running. When virtual threads are enabled, this is no longer the case and the application stops once it has initialized.
Example application (generated by Spring Initializr, Spring Boot 3.2.0-M3, no dependencies, Java 21):
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Scheduled(fixedDelay = 1000)
public void run() {
System.out.println("Running");
}
}
With virtual threads enabled, there's only one line of "Running":
$ java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --spring.threads.virtual.enabled=true
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0-M3)
2023-10-05T18:37:14.740+02:00 INFO 96833 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication v0.0.1-SNAPSHOT using Java 21 with PID 96833 (/Users/andreas/Downloads/demo/build/libs/demo-0.0.1-SNAPSHOT.jar started by andreas in /Users/andreas/Downloads/demo)
2023-10-05T18:37:14.741+02:00 INFO 96833 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2023-10-05T18:37:14.997+02:00 INFO 96833 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.409 seconds (process running for 0.606)
Running
$
With virtual threads disabled, "Running" is printed until the application is stopped with CTRL+C:
$ java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --spring.threads.virtual.enabled=false
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.0-M3)
2023-10-05T18:37:05.777+02:00 INFO 96817 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication v0.0.1-SNAPSHOT using Java 21 with PID 96817 (/Users/andreas/Downloads/demo/build/libs/demo-0.0.1-SNAPSHOT.jar started by andreas in /Users/andreas/Downloads/demo)
2023-10-05T18:37:05.777+02:00 INFO 96817 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
Running
2023-10-05T18:37:06.037+02:00 INFO 96817 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.411 seconds (process running for 0.612)
Running
Running
Running
Running
^C
$
On the one hand, it makes sense because Spring Boot stops if there is not at least one non-daemon thread. On the other hand, it is no small change in behaviour. Is it intentional? If so, I could not find any mention in the reference or the release notes. The relevant entry is probably this one from the 3.2.0-M1 release notes about task execution.
Comment From: philwebb
I'm not sure we'd consider it intentional, but it certainly makes sense given your description about non-daemon threads. We'll need to discuss it as a team to see what our options are.
Comment From: wilkinsona
We discussed this today. Our feeling was that there's nothing that we can do by default as it's very hard for us to know that the JVM should be kept alive through the existence of a non-daemon thread. If we get it wrong and someone isn't calling close() on the context to cause that thread to die, the JVM will never exit. The best alternative that we can think of is a configuration property to opt into a non-daemon thread being created for the sole purpose of keeping the JVM alive. That thread would stop running when the context is closed.
Comment From: aahlenst
Thanks a lot for giving it a thought.
I'm primarily interested in it being documented because I wasn't sure whether I missed something. I doubt I'm going to be the only one.
Having an official mechanism to keep a Spring Boot application alive would be nice, especially because one of the popular workarounds (@Scheduled) no longer does the trick with virtual threads. An alternative could be better guidance what to do in those cases. Some technologies like servlet containers take care of it behind the scenes but others like RSocket clients do not.
Comment From: mhalbritter
There's now the property spring.main.keep-alive. If set to true, it will spawn a non-daemon platform thread which keeps the JVM alive. This thread is stopped if the context is closed. I added some documentation around that, too.