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.

General Information

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
Topic revision: r4 - 15 Mar 2024, BenjaminPeter
This site is powered by FoswikiCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding Foswiki? Send feedback