APP Java FX Application Shutdown Guideline
This is a recommendation which will work best when using our common libraries. Other solutions can work as well.
The goal is to allow applications and services to close connections and resources where necessary while reducing the effort to do so. If a hard system exit is used before the Fx shutdown code was executed, the application can not and will not be able to perform shutdown code in the application context / in the FX application thread.
Two main shutdown scenarios are relevant for FX applications.
- App close via closing windows or calling Platform.exit()
- Java FX platform and rendering threads are still operational during the shutdown
- The
Application#stop
Method is called
- Process / JVM exit via system signal – also Ctrl+c
- Java FX thread will be automatically terminated at this point
-
Runtime.getRuntime().addShutdownHook(Thread)
can be used to implement shutdown code
- Code will be executed in almost any case (Except in the case of
Runtime#halt
(discouraged))
This means that shutdown code in JVM shutdown handlers provides a single place to implement shutdown code.
Applications
- The Java FX
setOnCloseRequest
handler always calls Platform.exit()
(Implemented in GsiCommonApplicationBase
)
- A Java system property
app.usePlainPlatformExit with default value false
is implemented
- Applications should be manually reviewed, and those that can properly shut down without a hard
System.exit()
can set this property to true
- Running daemon threads is the main reason why it does not shut down properly. Exit those threads or configure them to be a
daemon thread
to resolve this.
- In
GsiCommonApplicationBase#stop
(Overridden from Application#stop
):
- The de.gsi.fcc.applications.common.uilib.fx.utils.UiShutdownHandlerManager is executed
- Application can register handlers which will be called on the FX platform thread to perform cleanup actions
- Their primary purpose is to terminate threads that cannot be realized as daemon threads or to possibly save open data
- Most connections etc. can be centrally terminated through runtime shutdown hooks
- Implementations of the shutdown handler must ensure that it terminates in an acceptable time frame
- Depending on
app.usePlainPlatformExit
, System.exit()
is called
- General cleanup tasks, if necessary, should be done individually in
Runtime.getRuntime().addShutdownHook(Thread)
threads
- Tasks that must complete fully should be implemented with a limited waiting shutdown handler, see "Controlled thread shutdown"
- If spring is used spring contexts should be provided with
registerShutdownHook()
and @PreDestroy
annotations can be used to clean up beans (resources)
- Spring webflux / reactor connection (
Mono
/ Flux
) connections are centrally closed by spring, the server is notified directly. No need to explicitly close them.
- But application shutdown time can be improved by reducing the interaction with those connections since spring uses a "graceful shutdown" before finally terminating them
Services / Fat-Clients with JAPC Connections
- General cleanup tasks, if necessary, should be done individually in
Runtime.getRuntime().addShutdownHook(Thread)
threads
- Tasks that must complete fully should be implemented with a limited waiting shutdown handler, see "Controlled thread shutdown"
- If spring is used spring contexts should be provided with
registerShutdownHook()
and @PreDestroy
annotations can be used to clean up beans (resources)
- A central shutdown registry for subscriptions, which cleans up all remaining Subscriptions is provided. Use it to register and remove subscriptions:
de.gsi.fcc.commons.dcl.utils.SubscriptionShutdownRegistry
Controlled thread shutdown
Just an Example, please adapt!
public class IonSourceServerMain {
private static final Logger LOGGER = LoggerFactory.getLogger(IonSourceServerMain.class);
private static Thread threadToBeShutDownGracefully;
@SuppressWarnings("resource")
public static void main(final String... args) {
init();
SpringApplication.run(IonSourceServerMain.class, args);
}
public static void init() {
PropertyConfig.loadPropertiesFromConfigLocationByFilename("lsa.properties");
PropertyConfig.loadDefaultProperties();
threadToBeShutDownGracefully = createPreciousThread();
threadToBeShutDownGracefully.start();
Runtime.getRuntime().addShutdownHook(new Thread(IonSourceServerMain::interruptTheThreadAndWait));
}
private static Thread createPreciousThread() {
return new Thread(() -> {
LOGGER.info("Non daemon thread started");
int shutdownInSteps = Integer.MAX_VALUE; // Simulate something to do
while (true) {
try {
if (shutdownInSteps <= 0) {
LOGGER.info("Non daemon thread shut down counter at zero, exiting. {}", Instant.now());
return;
}
LOGGER.debug("Non daemon thread tick {} (shutdown counter {})", Instant.now(), shutdownInSteps);
Thread.sleep(1000);
shutdownInSteps--;
} catch (final InterruptedException e) {
LOGGER.info("Non daemon thread interrupted, changing shutdown counter");
shutdownInSteps = 15;
}
}
});
}
private static void interruptTheThreadAndWait() {
LOGGER.info("Non daemon thread shutdown hook called, interrupting");
threadToBeShutDownGracefully.interrupt();
try {
LOGGER.info("Non daemon thread interrupted, waiting for it to end");
threadToBeShutDownGracefully.join(30_000 /* ms */);
} catch (final InterruptedException e) {
LOGGER.info("Non daemon thread clean shutdown interrupted, ¯\\_(ツ)_/¯.");
}
}
}
Output:
…
I 24-02-09 09:52:26,214 Thread-6 g. ionsource.IonSourceServerMain.lambda$1 Non daemon thread shutdown hook called, interrupting
I 24-02-09 09:52:26,214 Thread-6 g. ionsource.IonSourceServerMain.lambda$1 Non daemon thread interrupted, waiting for it to end
D 24-02-09 09:52:26,215 ngContextShutdownHook nnotationConfigApplicationContext.doClose Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@1233ba5a, started on Fri Feb 09 09:51:56 UTC 2024
D 24-02-09 09:52:26,215 ngContextShutdownHook nnotationConfigApplicationContext.doClose Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@4cf303de, started on Fri Feb 09 09:51:57 UTC 2024
D 24-02-09 09:52:26,216 HttpResources-Dispose g. client.WebfluxClient.run Disposing HttpResources
D 24-02-09 09:52:26,217 Thread-99 g. namingservice.NamingServiceGSI.run Service was interrupted - sleep interrupted
I 24-02-09 09:52:26,217 Thread-113 g. omSubscriptionShutdownRegistry.closeAll Closing known subscriptions: 76
I 24-02-09 09:52:26,217 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread interrupted, starting shutdown counter
I 24-02-09 09:52:26,217 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:26.217315525Z
…
D 24-02-09 09:52:40,906 ngServerListUpdater-0 loadbalancer.BaseLoadBalancer.setServersList LoadBalancer [ds--827522292]: addServer [cmwpro00a.acc.gsi.de:7500]
D 24-02-09 09:52:40,906 ngServerListUpdater-0 cer.DynamicServerListLoadBalancer.setServerListForZones Setting server list for zones: {unknown=[cmwpro00a.acc.gsi.de:7500]}
D 24-02-09 09:52:40,906 ngServerListUpdater-0 loadbalancer.BaseLoadBalancer.setServersList LoadBalancer [ds--827522292_unknown]: clearing server list (SET op)
D 24-02-09 09:52:40,906 ngServerListUpdater-0 loadbalancer.BaseLoadBalancer.setServersList LoadBalancer [ds--827522292_unknown]: addServer [cmwpro00a.acc.gsi.de:7500]
I 24-02-09 09:52:41,223 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:41.222851204Z
D 24-02-09 09:52:41,400 er.DeadlineCheckTimer g. ifc.LifesignWatcher.checkPassedDeadlines checking for 0 deadlines
I 24-02-09 09:52:42,223 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:42.223224884Z
I 24-02-09 09:52:43,223 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:43.223554862Z
I 24-02-09 09:52:44,224 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:44.223934774Z
I 24-02-09 09:52:45,224 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:45.224303985Z
I 24-02-09 09:52:46,224 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:46.224664451Z
I 24-02-09 09:52:47,225 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:47.225123119Z
I 24-02-09 09:52:48,225 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:48.225479766Z
I 24-02-09 09:52:49,226 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:49.226548861Z
I 24-02-09 09:52:50,226 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:50.226816574Z
I 24-02-09 09:52:51,227 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:51.227170747Z
D 24-02-09 09:52:51,400 er.DeadlineCheckTimer g. ifc.LifesignWatcher.checkPassedDeadlines checking for 0 deadlines
I 24-02-09 09:52:52,227 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:52.227543545Z
I 24-02-09 09:52:53,228 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:53.227886488Z
I 24-02-09 09:52:54,228 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:54.228356377Z
I 24-02-09 09:52:55,228 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread tick 2024-02-09T09:52:55.228799787Z
I 24-02-09 09:52:56,229 Thread-5 g. ionsource.IonSourceServerMain.lambda$0 Non daemon thread shut down counter at zero, exiting. 2024-02-09T09:52:56.229142509Z
[END]
Issue reference: ION-1177