javaservletsjdbcjnashutdown-hook

Library shutdown routine that works well in a "normal" Java application and in a web application


I maintain a JDBC driver that also has an embedded database server mode provided through a native library, which is accessed through JNA. The shutdown process, done as part of unloading the native library itself, runs into problems on Windows due to the order of unloading its dependencies. To avoid access violations or other issues, I need to explicitly shut down the embedded engine before unloading this library.

Due to the nature of its use, it is difficult to determine an appropriate moment to call for a shutdown. The only correct way to do this for a normal Java application is by registering a shutdown hook using

Runtime.getRuntime().addShutdownHook with a subclass of Thread that implements the shutdown logic.

This works fine for a normal Java application, but for web applications that include my library as part of the application (in the WEB-INF/lib of the WAR), this will cause a memory leak on undeploy as the shutdown hook will maintain a strong reference to my shutdown implementation and to the classloader of the web application.

What would be a suitable and appropriate way to address this? Options I'm looking into right now are:

Is there a shutdown mechanism in Java I have overlooked that could be suitable for my needs?


Solution

  • I tried to figure this out as this seems such an interesting case. I'm posting my findings here, although I feel I might still have misunderstood something, or made some too far-fetched simplifications. Actually, it's also possible that I totally misunderstood your case, and this answer is all useless (if so, I apologize).

    What I assembled here is based on two notions:

    I propose an EmbeddedEngineHandler.loadEmbeddedEngineIfNeeded method that would be called:

    If I got it right, you won't need to call Runtime.removeShutdownHook at all.

    The main thing that I'm uncertain about here is this - if the driver is deployed globally, would it be registered before any servlet is initialized? If not, then I got it wrong, and this won't work. But maybe inspecting the ClassLoader of EmbeddedEngineHandler could help then?


    This is the EmbeddedEngineHandler:

    final class EmbeddedEngineHandler {
    
        private static final String PREFIX = ""; // some ID for your library here
        private static final String IS_SERVLET_CONTEXT = PREFIX + "-is-servlet-context";
        private static final String GLOBAL_ENGINE_LOADED = PREFIX + "-global-engine-loaded";
    
        private static final String TRUE = "true";
    
        private static volatile boolean localEngineLoaded = false;
    
        // LOADING
        static void loadEmbeddedEngineIfNeeded() {
            if (isServletContext()) {
                // handles only engine per container case
                loadEmbeddedEngineInLocalContextIfNeeded();
            } else {
                // handles both normal Java application & global driver cases
                loadEmbeddedEngineInGlobalContextIfNeeded();
            }
    
        }
    
        private static void loadEmbeddedEngineInLocalContextIfNeeded() {
            if (!isGlobalEngineLoaded() && !isLocalEngineLoaded()) { // will not load if we have a global driver
                loadEmbeddedEngine();
                markLocalEngineAsLoaded();
            }
        }
    
        private static void loadEmbeddedEngineInGlobalContextIfNeeded() {
            if (!isGlobalEngineLoaded()) {
                loadEmbeddedEngine();
                markGlobalEngineAsLoaded();
                Runtime.getRuntime().addShutdownHook(new Thread(EmbeddedEngineHandler::unloadEmbeddedEngine));
            }
        }
    
        private static void loadEmbeddedEngine() {
        }
    
        static void unloadEmbeddedEngine() {
        }
    
        // SERVLET CONTEXT (state shared between containers)
        private static boolean isServletContext() {
            return TRUE.equals(System.getProperty(IS_SERVLET_CONTEXT));
        }
    
        static void markAsServletContext() {
            System.setProperty(IS_SERVLET_CONTEXT, TRUE);
        }
    
        // GLOBAL ENGINE (state shared between containers)
        private static boolean isGlobalEngineLoaded() {
            return TRUE.equals(System.getProperty(GLOBAL_ENGINE_LOADED));
        }
    
        private static void markGlobalEngineAsLoaded() {
            System.setProperty(GLOBAL_ENGINE_LOADED, TRUE);
        }
    
        // LOCAL ENGINE (container-specific state)
        static boolean isLocalEngineLoaded() {
            return localEngineLoaded;
        }
    
        private static void markLocalEngineAsLoaded() {
            localEngineLoaded = true;
        }
    }
    

    and this is the ServletContextListener:

    @WebListener
    final class YourServletContextListener implements ServletContextListener {
    
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            EmbeddedEngineHandler.markAsServletContext();
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent sce) {
            if (EmbeddedEngineHandler.isLocalEngineLoaded()) {
                EmbeddedEngineHandler.unloadEmbeddedEngine();
            }
        }
    }