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

11,400

Solution 1

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;
    }

Solution 2

You can also use this valve which is part of the Tomcat distribution (at least in version 8) :

<Valve className="org.apache.catalina.valves.PersistentValve"/>

This node has to be inserted before the <Manager className="org.apache.catalina.session.PersistentManager"> node in the context.xml file.

It will then use the store to maintain the session on each http request. Note that the documentation assumes that only one http request will be made by the same client at a time.

This will allow you to use non sticky session load balancer in front of your java ee servers.

Solution 3

I came across this because Tomcat was taking a minute to shutdown once I added the PersistentManager to the configuration, but it relates to your problem too:

The reason you it takes a minute to persist with the PersistentManager is because you haven't adjusted the processExpiresFrequency. This setting regulates how often the PersistentManager will run it's background processes to expire session, persist them, etc. The default value is 6. (See docs: http://tomcat.apache.org/tomcat-8.5-doc/config/manager.html#Standard_Implementation)

Per the code, this value is multiplied by engine.backgroundProcessorDelay, which you set on your <Engine> element. It's default value is 10. So 6*10 is 60 seconds. If you add processExpiresFrequency="1" on your <Manager> element, you'll see it will shutdown much quicker (10 seconds). If that's not fast enough, you can adjust the backgroundProcessorDelay to be lower too. You'll also still want to set maxIdleBackup to 1. You won't get absolutely immediate persistence, but it's very quick and doesn't require the self-described "ugly tweak" in the accepted answer.

(See comments about backgroundProcessorDelay on setMaxIdleBackup method in http://svn.apache.org/repos/asf/tomcat/tc8.5.x/tags/TOMCAT_8_5_6/java/org/apache/catalina/session/PersistentManagerBase.java)

Share:
11,400
codependent
Author by

codependent

By day: I code for 8 hours and a half. Cloud, Kubernetes, Spring, NodeJS... By night: I code a little more, work out and try to get some sleep.

Updated on June 04, 2022

Comments

  • codependent
    codependent almost 2 years

    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.

  • codependent
    codependent over 7 years
    I appreciate your input, I'll give it a try.
  • Mohammed Javad
    Mohammed Javad almost 5 years
    Can someone please tell the maven used for the DataGridSession and DataGridManager
  • 100ferhas
    100ferhas almost 4 years
    how they can think that only 1 request at time can be served? what about css/js assets? at this time is useless
  • Toofy
    Toofy about 3 years
    Thank you, you are my hero. This was driving me nuts.