Search code examples
javatomcatopenshifttomcat8openshift-enterprise

Tomcat - How to persist a session immediately to disk using PersistentManager + FileStore


I want to persist Tomcat's HttpSessions to disk so that it can be used in a scalable cloud environment. The point is that there will be a number of Tomcat nodes up (in a cloud PaaS) and clients can be directed to any of them. We want to persist and load the sessions from a shared disk unit.

I have configured the PersistentManager this way:

context.xml

<Manager className="org.apache.catalina.session.PersistentManager">
   <Store className="org.apache.catalina.session.FileStore" directory="c:/somedir"/>
</Manager>

The problem is that sessions are, apparently, never flushed to disk.

I changed the <Manager> config adding maxIdleBackup:

<Manager className="org.apache.catalina.session.PersistentManager maxIdleBackup="1">

This way it takes almost a minute until I see the session persisted to disk. Oddly enough, the doc states that it should take around a second:

maxIdleBackup: The time interval (in seconds) since the last access to a session before it is eligible for being persisted to the session store, or -1 to disable this feature. By default, this feature is disabled.

Other config:

Following the documentation I set the system property

org.apache.catalina.session.StandardSession.ACTIVITY_CHECK -> true

Is there a way to immediately flush the session to disk? Is is possible to make that any change in the session is also persisted right away?

UPDATE:

I have tried to force the passivation of the session and flushing to disk with maxIdleBackup="0" minIdleSwap="0" maxIdleSwap="1", but it still takes almost a minute.


Solution

  • I finally managed to solve this:

    1. I extended org.apache.catalina.session.ManagerBase overriding every method that used the superclass' sessions map, so that it attacked a file (or cache) directly.

    Example:

    @Override
    public HashMap<String, String> getSession(String sessionId) {
        Session s = getSessionFromStore(sessionId);
        if (s == null) {
            if (log.isInfoEnabled()) {
                log.info("Session not found " + sessionId);
            }
            return null;
        }
    
        Enumeration<String> ee = s.getSession().getAttributeNames();
        if (ee == null || !ee.hasMoreElements()) {
            return null;
        }
    
        HashMap<String, String> map = new HashMap<>();
        while (ee.hasMoreElements()) {
            String attrName = ee.nextElement();
            map.put(attrName, getSessionAttribute(sessionId, attrName));
        }
    
        return map;
    
    }
    

    IMPORTANT:

    load and unload methods must be left empty:

        @Override
        public void load() throws ClassNotFoundException, IOException {
            // TODO Auto-generated method stub
    
        }
    
        @Override
        public void unload() throws IOException {
            // TODO Auto-generated method stub
    
        }
    

    You have to override startInternal and stopInternal to prevent Lifecycle errors:

    @Override
    protected synchronized void startInternal() throws LifecycleException {
    
        super.startInternal();
    
        // Load unloaded sessions, if any
        try {
            load();
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("standardManager.managerLoad"), t);
        }
    
        setState(LifecycleState.STARTING);
    }
    
    @Override
    protected synchronized void stopInternal() throws LifecycleException {
    
        if (log.isDebugEnabled()) {
            log.debug("Stopping");
        }
    
        setState(LifecycleState.STOPPING);
    
        // Write out sessions
        try {
            unload();
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("standardManager.managerUnload"), t);
        }
    
        // Expire all active sessions
        Session sessions[] = findSessions();
        for (int i = 0; i < sessions.length; i++) {
            Session session = sessions[i];
            try {
                if (session.isValid()) {
                    session.expire();
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
            } finally {
                // Measure against memory leaking if references to the session
                // object are kept in a shared field somewhere
                session.recycle();
            }
        }
    
        // Require a new random number generator if we are restarted
        super.stopInternal();
    } 
    
    1. The above allows to read always from the file (or cache) but what about the write operations?. For this, I extended org.apache.catalina.session.StandardSession overriding public void setAttribute(String name, Object value, boolean notify) and public void removeAttribute(String name, boolean notify).

    Example:

    @Override
    public void setAttribute(String name, Object value, boolean notify) {
        super.setAttribute(name, value, notify);
        ((DataGridManager)this.getManager()).getCacheManager().getCache("sessions").put(this.getIdInternal(), this);
    }
    
    @Override
    public void removeAttribute(String name, boolean notify) {
        super.removeAttribute(name, notify);
        ((DataGridManager)this.getManager()).getCacheManager().getCache("sessions").put(this.getIdInternal(), this);
    }
    

    IMPORTANT:

    In our case the real session backup ended up being a cache (not a file) and when we read the extended Tomcat session from it (in our ManagerBase impl class) we had to tweak it in an kind of ugly way so that everything worked:

        private Session getSessionFromStore(String sessionId){
            DataGridSession s = (DataGridSession)cacheManager.getCache("sessions").get(sessionId);
            if(s!=null){
                try {
                    Field notesField;
                    notesField = StandardSession.class.getDeclaredField("notes");
                    notesField.setAccessible(true);
                    notesField.set(s, new HashMap<String, Object>());
                    s.setManager(this);
                } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
                    throw new RuntimeException(e);
                }
            }
            return s;
        }