A week or two ago I found I had need of the persistent session manager, PersistentManagerBase -- and also noticed its experimental status.

Looking at the sources I found "FIXME" comments regarding: (1) a race condition between session passivation and session usage and (2) a lack of LRU sorting to passivate oldest sessions first. I also discovered that all passivated sessions are regularly (every minuted by default) read back into memory in their entirety to check if they should be expired.

The attached set of patches is intended to address all of these issues -- and seems to do so to the best of my (admittedly limited) testing.

There are additional fixes that should be made to JDBCStore (i.e. in general it seems to a trip to the database for every row in many cases where 1 per 'n' would suffice), but this was of less interest to me for the time being than FileStore, so I have not pursued these. Also, I introduced a new attribute to PersistentManagerBase, but have not yet exposed it via JMX. This is intentional at this point as I am uncertain as to the merits of the non-default value of this operation as of yet.

Comments and questions are welcome. All of the patches are against 5.0.30, but I could update them for 5.5.x if there was sufficient interest (e.g. if a committer was interested in committing them).

--
Jess Holle
[EMAIL PROTECTED]

--- StandardSession-5.0.30.java 2004-12-13 21:28:06.000000000 -0600
+++ StandardSession.java        2004-12-13 21:28:04.000000000 -0600
@@ -593,7 +593,14 @@
      * should be called by the context when a request comes in for a particular
      * session, even if the application does not reference it.
      */
-    public void access() {
+    public synchronized void access() {
+
+        ManagerBase  managerBase = ( ( manager instanceof ManagerBase ) ? 
(ManagerBase) manager : null );
+        if ( managerBase != null )
+            managerBase.preAccess( this );
+        
+        if ( !isValid || expiring )
+          return;
 
         this.lastAccessedTime = this.thisAccessedTime;
         this.thisAccessedTime = System.currentTimeMillis();
@@ -601,7 +608,10 @@
         evaluateIfValid();
 
         accessCount++;
-
+        
+        if ( isValid )
+            if ( managerBase != null )
+                managerBase.access( this );
     }
 
 
@@ -611,11 +621,12 @@
     public void endAccess() {
 
         isNew = false;
-        accessCount--;
-
+        if ( accessCount > 0 )
+            --accessCount;
+        else if ( debug > 0 )
+            log( "Session " + getId() + " accessCount [" + ( accessCount - 1 ) 
+ "] < 0 " );  // show access count as it would be if we had decremented
     }
 
-
     /**
      * Add a session event listener to this component.
      */
@@ -790,6 +801,20 @@
 
     }
 
+    /**
+     * Copy all non-transient fields other than id from 'otherSession' to
+     * this session.
+     */
+    protected void copyFieldsOf( StandardSession otherSession )
+    {
+      this.attributes = (HashMap) otherSession.attributes.clone();
+      this.creationTime = otherSession.creationTime ;
+      this.lastAccessedTime = otherSession.lastAccessedTime;
+      this.maxInactiveInterval = otherSession.maxInactiveInterval;
+      this.isNew = otherSession.isNew;
+      this.isValid = otherSession.isValid;
+      this.thisAccessedTime = otherSession.thisAccessedTime;
+    }
 
     /**
      * Release all object references, and initialize instance variables, in
@@ -802,7 +827,7 @@
         setAuthType(null);
         creationTime = 0L;
         expiring = false;
-        id = null;
+        // id = null;
         lastAccessedTime = 0L;
         maxInactiveInterval = -1;
         accessCount = 0;
@@ -810,7 +835,7 @@
         setPrincipal(null);
         isNew = false;
         isValid = false;
-        manager = null;
+        // manager = null;
 
     }
 
--- StoreBase-5.0.30.java       2004-12-13 21:29:12.000000000 -0600
+++ StoreBase.java      2004-12-13 21:29:08.000000000 -0600
@@ -204,7 +204,7 @@
         }
 
         try {
-            keys = keys();
+            keys = keysThatMayBeExpired();
         } catch (IOException e) {
             log (e.toString());
             e.printStackTrace();
@@ -213,13 +213,10 @@
 
         for (int i = 0; i < keys.length; i++) {
             try {
-                StandardSession session = (StandardSession) load(keys[i]);
+                StandardSession session = (StandardSession) 
loadSessionIfShouldBeExpired(keys[i]);
                 if (session == null) {
                     continue;
                 }
-                if (session.isValid()) {
-                    continue;
-                }
                 if ( ( (PersistentManagerBase) manager).isLoaded( keys[i] )) {
                     // recycle old backup session
                     session.recycle();
@@ -239,7 +236,34 @@
             }
         }
     }
+    
+    protected String[] keysThatMayBeExpired()
+        throws IOException
+    {
+        return ( keys() );
+    }
 
+    protected StandardSession loadSessionIfShouldBeExpired( String sessionId )
+    {
+        StandardSession session;
+        try
+        {
+            session = (StandardSession) load(sessionId);
+        }
+        catch ( ClassNotFoundException e )
+        {
+            session = null;
+        }
+        catch ( IOException e )
+        {
+            session = null;
+        }
+        if ( session != null )
+            if (session.isValid())
+                return ( null );
+        return ( session );
+    }
+    
     /**
      * Log a message on the Logger associated with our Container (if any).
      *
--- CoyoteRequest-5.0.30.java   2004-12-13 21:21:32.000000000 -0600
+++ CoyoteRequest.java  2004-12-13 21:21:26.000000000 -0600
@@ -2253,11 +2253,11 @@
             } catch (IOException e) {
                 session = null;
             }
-            if ((session != null) && !session.isValid())
-                session = null;
             if (session != null) {
                 session.access();
-                return (session.getSession());
+                if (session.isValid())  // check isValid() *after* access()!
+                    return (session.getSession());
+                session = null;
             }
         }
 
--- FileStore-5.0.30.java       2004-12-13 21:23:06.000000000 -0600
+++ FileStore.java      2004-12-13 21:23:08.000000000 -0600
@@ -373,8 +373,31 @@
             oos.close();
         }
 
+        // give file modification date to match last session access time
+        if ( !file.setLastModified( 
((StandardSession)session).thisAccessedTime ) )
+          if (debug >= 1)
+              log( "Could not set last modified date on file store for " + 
session.getId() );
+
     }
 
+    // use file modification date to check if we should bother loading session
+    protected StandardSession loadSessionIfShouldBeExpired( String sessionId )
+    {
+        File file = file(sessionId);
+        if (file != null) {
+            long sessionAccessTime = file.lastModified();
+            if ( sessionAccessTime > 0 ) {
+                int  maxInactiveInterval = 
((ManagerBase)manager).maxInactiveInterval;
+                if ( maxInactiveInterval >= 0 ) { 
+                    long timeNow = System.currentTimeMillis();
+                    int timeIdle = (int) ((timeNow - sessionAccessTime) / 
1000L);
+                    if (timeIdle >= maxInactiveInterval)
+                        return ( super.loadSessionIfShouldBeExpired( sessionId 
) );
+                }
+            }
+        }
+        return ( null );
+    }
 
     // -------------------------------------------------------- Private Methods
 
--- JDBCStore-5.0.30.java       2004-12-13 21:24:34.000000000 -0600
+++ JDBCStore.java      2004-12-13 21:24:32.000000000 -0600
@@ -151,6 +151,11 @@
     protected PreparedStatement preparedKeysSql = null;
 
     /**
+     * Variable to hold the <code>keysThatMayBeExpired()</code> prepared 
statement.
+     */
+    protected PreparedStatement preparedKeysThatMayBeExpiredSql = null;
+    
+    /**
      * Variable to hold the <code>save()</code> prepared statement.
      */
     protected PreparedStatement preparedSaveSql = null;
@@ -491,6 +496,66 @@
         return (keys);
     }
 
+    protected String[] keysThatMayBeExpired()
+        throws IOException
+    {
+        int maxInactiveInterval = ((ManagerBase)manager).maxInactiveInterval;
+        if ( maxInactiveInterval < 0 )
+            return new String[0];
+        
+        String keysSql =
+                "SELECT " + sessionIdCol + " FROM " + sessionTable +
+                " WHERE " + sessionAppCol + " = ? and " +
+                            sessionLastAccessedCol + " <= ?";
+        ResultSet rst = null;
+        String keys[] = null;
+        synchronized (this) {
+            int numberOfTries = 2;
+            while (numberOfTries > 0) {
+
+                Connection _conn = getConnection();
+                if (_conn == null) {
+                    return (new String[0]);
+                }
+                try {
+                    if (preparedKeysThatMayBeExpiredSql == null) {
+                        preparedKeysThatMayBeExpiredSql = 
_conn.prepareStatement(keysSql);
+                    }
+
+                    preparedKeysThatMayBeExpiredSql.setString(1, getName());
+                    preparedKeysThatMayBeExpiredSql.setLong(2, 
System.currentTimeMillis() - maxInactiveInterval*1000L );
+                    rst = preparedKeysThatMayBeExpiredSql.executeQuery();
+                    ArrayList tmpkeys = new ArrayList();
+                    if (rst != null) {
+                        while (rst.next()) {
+                            tmpkeys.add(rst.getString(1));
+                        }
+                    }
+                    keys = (String[]) tmpkeys.toArray(new 
String[tmpkeys.size()]);
+                } catch (SQLException e) {
+                    log(sm.getString(getStoreName() + ".SQLException", e));
+                    keys = new String[0];
+                    // Close the connection so that it gets reopened next time
+                    if (dbConnection != null)
+                        close(dbConnection);
+                } finally {
+                    try {
+                        if (rst != null) {
+                            rst.close();
+                        }
+                    } catch (SQLException e) {
+                        ;
+                    }
+
+                    release(_conn);
+                }
+                numberOfTries--;
+            }
+        }
+
+        return (keys);
+    }
+
     /**
      * Return an integer containing a count of all Sessions
      * currently saved in this Store.  If there are no Sessions,
@@ -775,7 +840,7 @@
                     preparedSaveSql.setBinaryStream(3, in, size);
                     preparedSaveSql.setString(4, session.isValid() ? "1" : 
"0");
                     preparedSaveSql.setInt(5, 
session.getMaxInactiveInterval());
-                    preparedSaveSql.setLong(6, session.getLastAccessedTime());
+                    preparedSaveSql.setLong(6, 
((StandardSession)session).thisAccessedTime);
                     preparedSaveSql.execute();
                 } catch (SQLException e) {
                     log(sm.getString(getStoreName() + ".SQLException", e));
--- ManagerBase5.0.30.java      2004-12-13 21:25:26.000000000 -0600
+++ ManagerBase.java    2004-12-13 21:25:22.000000000 -0600
@@ -719,6 +719,32 @@
 
 
     /**
+     * Allow manager to adjust session prior to normal session.access() 
operation.
+     * <p>
+     * This can be used by subclasses to reload session if it was swapped out
+     * due to race condition.
+     *
+     * @param session Session that is being accessed.
+     */
+    protected void preAccess( Session session )
+    {
+      // do nothing
+    }
+
+    /**
+     * Signal to Manager that session has been accessed.
+     * <p>
+     * This can be used by subclasses to maintain MRU/LRU data on
+     * sessions.
+     *
+     * @param session Session that is being accessed.
+     */
+    protected void access( Session session ) {
+        // do nothing here...
+    }
+
+
+    /**
      * Return the active Session, associated with this Manager, with the
      * specified session id (if any); otherwise return <code>null</code>.
      *
--- PersistentManagerBase-5.0.30.java   2004-12-13 21:26:26.000000000 -0600
+++ PersistentManagerBase.java  2004-12-13 21:26:32.000000000 -0600
@@ -20,6 +20,11 @@
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.Map;
 import java.security.AccessController;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
@@ -210,6 +215,33 @@
     protected long processingTime = 0;
 
 
+    /**
+     * Whether session should be synchronously swapped out immediately upon
+     * exceeding maxActiveSessions.  Defaults to 'false'.
+     */
+    protected boolean immediateSwapOnMaxActiveExceeded = false;
+    
+    
+    // ------------------------------------------------------------- 
Constructors
+
+
+    public PersistentManagerBase() {
+        sessions = new LinkedHashMap() {
+            protected boolean removeEldestEntry(Map.Entry eldest) {
+                if ( immediateSwapOnMaxActiveExceeded ) {
+                  int  maxActiveSessions = getMaxActiveSessions();
+                  if ( maxActiveSessions >= 0 )
+                      if ( size() > maxActiveSessions )
+                          if ( isStarted() )
+                              // try to swap out oldest entry; if entry is 
still too fresh then processMaxActiveSwaps() will swap out sessions when 
possible in the background
+                              swapOutSessionIfOldEnough( (StandardSession) 
eldest.getValue(), System.currentTimeMillis(), minIdleSwap, 
"persistentManager.swapTooManyActive" );
+                }
+                return ( false );
+            }
+        };
+    }
+
+    
     // ------------------------------------------------------------- Properties
 
 
@@ -544,6 +576,24 @@
     }
 
 
+    public boolean isImmediateSwapOnMaxActiveExceeded() {
+        return immediateSwapOnMaxActiveExceeded;
+    }
+    
+
+    
+    public void  setImmediateSwapOnMaxActiveExceeded( boolean 
immediateSwapOnMaxActiveExceeded ) {
+        if ( this.immediateSwapOnMaxActiveExceeded == 
immediateSwapOnMaxActiveExceeded )
+          return;
+        boolean oldImmediateSwapOnMaxActiveExceeded  = 
this.immediateSwapOnMaxActiveExceeded ;
+        this.immediateSwapOnMaxActiveExceeded  = 
immediateSwapOnMaxActiveExceeded ;
+        support.firePropertyChange("immediateSwapOnMaxActiveExceeded ",
+                                   new 
Boolean(oldImmediateSwapOnMaxActiveExceeded ),
+                                   new 
Boolean(this.immediateSwapOnMaxActiveExceeded ));
+    }
+    
+    
+    
     // --------------------------------------------------------- Public Methods
 
 
@@ -641,6 +691,52 @@
         super.remove (session);
     }
 
+    /** If upon accessing a session we find it initially invalid see if it was
+     *  just swapped out.  If so, attempt to read the data back into this
+     *  session object as someone got a copy prior to the swapOut.
+     *
+     * @param session Session that is being accessed.
+     */
+    protected void preAccess( Session session )
+    {
+      StandardSession  stdSession = (StandardSession) session;
+      if ( stdSession.isValid || stdSession.expiring )
+        return;
+      String  id = stdSession.getId();
+      StandardSession  savedSession = null;
+      try
+      {
+        savedSession = (StandardSession) swapInWithoutAdd( id );
+      }
+      catch ( IOException e )
+      {
+        if (log.isDebugEnabled())
+            log.debug("Load from store failed for " + id );
+      }
+      if ( savedSession != null )
+      {
+        stdSession.copyFieldsOf( savedSession );
+        addSwappedInSession( stdSession );
+        savedSession.recycle();
+      }
+    }
+        
+    /**
+     * Signal to Manager that session has been accessed.
+     * <p>
+     * This is used here to maintain MRU/LRU data on sessions.
+     *
+     * @param session Session that is being accessed.
+     */
+    protected void  access( Session session ) {
+        synchronized (sessions) {
+            // pull session out of map and put it back in, thus sorting it
+            String sid = session.getId();
+            sessions.remove(sid);
+            sessions.put(sid, session);
+        }
+    }
+
     /**
      * Load all sessions found in the persistence mechanism, assuming
      * they are marked as valid and have not passed their expiration
@@ -692,9 +788,42 @@
                 log.error("Failed load session from store, " + e.getMessage(), 
e);
             }
 
+        sortSessionsByAccessTimes();
     }
 
 
+    /** Sort sessions by access times, i.e. during load().
+     */
+    private void sortSessionsByAccessTimes() {
+        ArrayList sessionList;
+        {
+            Session  sessionArr[] = findSessions();
+            sessionList = new ArrayList( sessionArr.length );
+            for ( int i = 0; i < sessionArr.length; ++i )
+              sessionList.add( sessionArr[i] );
+        }
+        Collections.sort( sessionList,
+            new Comparator() {
+                public int compare(Object o1, Object o2) {
+                    StandardSession  session1 = (StandardSession) o1;
+                    StandardSession  session2 = (StandardSession) o2;
+                    long  accessTime1 = session1.thisAccessedTime;
+                    long  accessTime2 = session2.thisAccessedTime;
+                    if ( accessTime1 < accessTime2 )
+                        return -1;
+                    if ( accessTime1 == accessTime2 )
+                        return 0;
+                    // if ( accessTime1 > accessTime2 )
+                        return 1;
+                }
+            }
+        );
+        int  nSessions = sessionList.size();
+        for ( int i = 0; i < nSessions; ++i )
+            access( (Session) sessionList.get( i ) );
+    }
+    
+    
     /**
      * Remove this Session from the active Sessions for this Manager,
      * and from the Store.
@@ -780,7 +909,13 @@
      * is invalid or past its expiration.
      */
     protected Session swapIn(String id) throws IOException {
-
+        Session  session = swapInWithoutAdd( id );
+        if ( session != null )
+          addSwappedInSession( session );
+        return ( session );
+    }
+    
+    protected Session swapInWithoutAdd(String id) throws IOException {
         if (store == null)
             return null;
 
@@ -821,18 +956,20 @@
         if(log.isDebugEnabled())
             log.debug(sm.getString("persistentManager.swapIn", id));
 
+        return (session);
+
+    }
+
+    protected void  addSwappedInSession( Session session )
+    {
         session.setManager(this);
         // make sure the listeners know about it.
         ((StandardSession)session).tellNew();
         add(session);
         ((StandardSession)session).activate();
         session.endAccess();
-
-        return (session);
-
     }
 
-
     /**
      * Remove the session from the Manager's list of active
      * sessions and write it out to the Store. If the session
@@ -1052,28 +1189,9 @@
         long timeNow = System.currentTimeMillis();
 
         // Swap out all sessions idle longer than maxIdleSwap
-        // FIXME: What's preventing us from mangling a session during
-        // a request?
-        if (maxIdleSwap >= 0) {
-            for (int i = 0; i < sessions.length; i++) {
-                StandardSession session = (StandardSession) sessions[i];
-                if (!session.isValid())
-                    continue;
-                int timeIdle = // Truncate, do not round up
-                    (int) ((timeNow - session.getLastAccessedTime()) / 1000L);
-                if (timeIdle > maxIdleSwap && timeIdle > minIdleSwap) {
-                    if (log.isDebugEnabled())
-                        log.debug(sm.getString
-                            ("persistentManager.swapMaxIdle",
-                             session.getId(), new Integer(timeIdle)));
-                    try {
-                        swapOut(session);
-                    } catch (IOException e) {
-                        ;   // This is logged in writeSession()
-                    }
-                }
-            }
-        }
+        int  minSwapSecs = ( ( maxIdleSwap > minIdleSwap ) ? maxIdleSwap : 
minIdleSwap );
+        for (int i = sessions.length - 1; i >= 0; --i)
+            swapOutSessionIfOldEnough( (StandardSession) sessions[i], timeNow, 
minSwapSecs, "persistentManager.swapMaxIdle" );
 
     }
 
@@ -1088,7 +1206,6 @@
 
         Session sessions[] = findSessions();
 
-        // FIXME: Smarter algorithm (LRU)
         if (getMaxActiveSessions() >= sessions.length)
             return;
 
@@ -1100,24 +1217,36 @@
         int toswap = sessions.length - getMaxActiveSessions();
         long timeNow = System.currentTimeMillis();
 
-        for (int i = 0; i < sessions.length && toswap > 0; i++) {
-            int timeIdle = // Truncate, do not round up
-                (int) ((timeNow - sessions[i].getLastAccessedTime()) / 1000L);
-            if (timeIdle > minIdleSwap) {
-                if(log.isDebugEnabled())
-                    log.debug(sm.getString
-                        ("persistentManager.swapTooManyActive",
-                         sessions[i].getId(), new Integer(timeIdle)));
-                try {
-                    swapOut(sessions[i]);
-                } catch (IOException e) {
-                    ;   // This is logged in writeSession()
-                }
-                toswap--;
-            }
-        }
+        for (int i = sessions.length - 1; i >= 0 && toswap > 0; --i)
+            if ( swapOutSessionIfOldEnough( (StandardSession) sessions[i], 
timeNow, minIdleSwap, "persistentManager.swapTooManyActive" ) )
+              --toswap;
 
     }
+    
+    
+    private boolean  swapOutSessionIfOldEnough( StandardSession session, long 
timeNow, int minSwapSecs, String debugString ) {
+        synchronized ( session ) {
+          int timeIdle = // Truncate, do not round up
+              (int) ((timeNow - session.thisAccessedTime) / 1000L);
+          if (timeIdle > minSwapSecs ) {
+              if ( session.accessCount > 0 ) {
+                  if (log.isDebugEnabled())
+                      log.debug( 
"PersistentManagerBase.swapOutSessionIfOldEnough(): session [" + 
session.getId() +
+                                 "] access count [" + session.accessCount + "] 
> 0 " );
+                  return ( false );  // session may still be being accessed
+              }
+              if(log.isDebugEnabled())
+                  log.debug(sm.getString(debugString, session.getId(), new 
Integer(timeIdle)));
+              try {
+                  swapOut(session);
+              } catch (IOException e) {
+                  ;   // This is logged in writeSession()
+              }
+              return ( true );
+          }
+        }
+        return ( false );
+    }
 
 
     /**
@@ -1133,22 +1262,31 @@
 
         // Back up all sessions idle longer than maxIdleBackup
         if (maxIdleBackup >= 0) {
-            for (int i = 0; i < sessions.length; i++) {
+            for (int i = sessions.length - 1; i >= 0; --i) {
                 StandardSession session = (StandardSession) sessions[i];
-                if (!session.isValid())
-                    continue;
-                int timeIdle = // Truncate, do not round up
-                    (int) ((timeNow - session.getLastAccessedTime()) / 1000L);
-                if (timeIdle > maxIdleBackup) {
-                    if (log.isDebugEnabled())
-                        log.debug(sm.getString
-                            ("persistentManager.backupMaxIdle",
-                            session.getId(), new Integer(timeIdle)));
-
-                    try {
-                        writeSession(session);
-                    } catch (IOException e) {
-                        ;   // This is logged in writeSession()
+                synchronized ( session )
+                {
+                    if (!session.isValid())
+                        continue;
+                    int timeIdle = // Truncate, do not round up
+                        (int) ((timeNow - session.thisAccessedTime) / 1000L);
+                    if (timeIdle > maxIdleBackup) {
+                        if ( session.accessCount > 0 ) {
+                            if (log.isDebugEnabled())
+                                log.debug( 
"PersistentManagerBase.processMaxIdleBackups(): session [" + session.getId() +
+                                           "] access count [" + 
session.accessCount + "] > 0 " );
+                            continue;  // session may still be being accessed
+                        }
+                        if (log.isDebugEnabled())
+                            log.debug(sm.getString
+                                ("persistentManager.backupMaxIdle",
+                                session.getId(), new Integer(timeIdle)));
+
+                        try {
+                            writeSession(session);
+                        } catch (IOException e) {
+                            ;   // This is logged in writeSession()
+                        }
                     }
                 }
             }

---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to