diff src/org/eclipse/jetty/server/session/JDBCSessionIdManager.java @ 802:3428c60d7cfc

replace jetty jars with source
author Franklin Schmidt <fschmidt@gmail.com>
date Wed, 07 Sep 2016 21:15:48 -0600
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/eclipse/jetty/server/session/JDBCSessionIdManager.java	Wed Sep 07 21:15:48 2016 -0600
@@ -0,0 +1,1040 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.sql.Blob;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import javax.naming.InitialContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.sql.DataSource;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.SessionManager;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.log.Logger;
+
+
+
+/**
+ * JDBCSessionIdManager
+ *
+ * SessionIdManager implementation that uses a database to store in-use session ids, 
+ * to support distributed sessions.
+ * 
+ */
+public class JDBCSessionIdManager extends AbstractSessionIdManager
+{    
+    final static Logger LOG = SessionHandler.LOG;
+    
+    protected final HashSet<String> _sessionIds = new HashSet<String>();
+    protected Server _server;
+    protected Driver _driver;
+    protected String _driverClassName;
+    protected String _connectionUrl;
+    protected DataSource _datasource;
+    protected String _jndiName;
+    protected String _sessionIdTable = "JettySessionIds";
+    protected String _sessionTable = "JettySessions";
+    protected String _sessionTableRowId = "rowId";
+    
+    protected Timer _timer; //scavenge timer
+    protected TimerTask _task; //scavenge task
+    protected long _lastScavengeTime;
+    protected long _scavengeIntervalMs = 1000L * 60 * 10; //10mins
+    protected String _blobType; //if not set, is deduced from the type of the database at runtime
+    protected String _longType; //if not set, is deduced from the type of the database at runtime
+    
+    protected String _createSessionIdTable;
+    protected String _createSessionTable;
+                                            
+    protected String _selectBoundedExpiredSessions;
+    protected String _deleteOldExpiredSessions;
+
+    protected String _insertId;
+    protected String _deleteId;
+    protected String _queryId;
+    
+    protected  String _insertSession;
+    protected  String _deleteSession;
+    protected  String _updateSession;
+    protected  String _updateSessionNode;
+    protected  String _updateSessionAccessTime;
+    
+    protected DatabaseAdaptor _dbAdaptor;
+
+    private String _selectExpiredSessions;
+
+    
+    /**
+     * DatabaseAdaptor
+     *
+     * Handles differences between databases.
+     * 
+     * Postgres uses the getBytes and setBinaryStream methods to access
+     * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
+     * is happy to use the "blob" type and getBlob() methods instead.
+     * 
+     * TODO if the differences become more major it would be worthwhile
+     * refactoring this class.
+     */
+    public class DatabaseAdaptor 
+    {
+        String _dbName;
+        boolean _isLower;
+        boolean _isUpper;
+       
+        
+        
+        public DatabaseAdaptor (DatabaseMetaData dbMeta)
+        throws SQLException
+        {
+            _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH); 
+            LOG.debug ("Using database {}",_dbName);
+            _isLower = dbMeta.storesLowerCaseIdentifiers();
+            _isUpper = dbMeta.storesUpperCaseIdentifiers();            
+        }
+        
+        /**
+         * Convert a camel case identifier into either upper or lower
+         * depending on the way the db stores identifiers.
+         * 
+         * @param identifier
+         * @return the converted identifier
+         */
+        public String convertIdentifier (String identifier)
+        {
+            if (_isLower)
+                return identifier.toLowerCase(Locale.ENGLISH);
+            if (_isUpper)
+                return identifier.toUpperCase(Locale.ENGLISH);
+            
+            return identifier;
+        }
+        
+        public String getDBName ()
+        {
+            return _dbName;
+        }
+        
+        public String getBlobType ()
+        {
+            if (_blobType != null)
+                return _blobType;
+            
+            if (_dbName.startsWith("postgres"))
+                return "bytea";
+            
+            return "blob";
+        }
+        
+        public String getLongType ()
+        {
+            if (_longType != null)
+                return _longType;
+            
+            if (_dbName.startsWith("oracle"))
+                return "number(20)";
+            
+            return "bigint";
+        }
+        
+        public InputStream getBlobInputStream (ResultSet result, String columnName)
+        throws SQLException
+        {
+            if (_dbName.startsWith("postgres"))
+            {
+                byte[] bytes = result.getBytes(columnName);
+                return new ByteArrayInputStream(bytes);
+            }
+            
+            Blob blob = result.getBlob(columnName);
+            return blob.getBinaryStream();
+        }
+        
+        /**
+         * rowId is a reserved word for Oracle, so change the name of this column
+         * @return
+         */
+        public String getRowIdColumnName ()
+        {
+            if (_dbName != null && _dbName.startsWith("oracle"))
+                return "srowId";
+            
+            return "rowId";
+        }
+        
+        
+        public boolean isEmptyStringNull ()
+        {
+            return (_dbName.startsWith("oracle"));
+        }
+        
+        public PreparedStatement getLoadStatement (Connection connection, String rowId, String contextPath, String virtualHosts) 
+        throws SQLException
+        {
+            if (contextPath == null || "".equals(contextPath))
+            {
+                if (isEmptyStringNull())
+                {
+                    PreparedStatement statement = connection.prepareStatement("select * from "+_sessionTable+
+                    " where sessionId = ? and contextPath is null and virtualHost = ?");
+                    statement.setString(1, rowId);
+                    statement.setString(2, virtualHosts);
+
+                    return statement;
+                }
+            }
+           
+
+
+            PreparedStatement statement = connection.prepareStatement("select * from "+_sessionTable+
+            " where sessionId = ? and contextPath = ? and virtualHost = ?");
+            statement.setString(1, rowId);
+            statement.setString(2, contextPath);
+            statement.setString(3, virtualHosts);
+
+            return statement;
+        }
+    }
+    
+    
+    
+    public JDBCSessionIdManager(Server server)
+    {
+        super();
+        _server=server;
+    }
+    
+    public JDBCSessionIdManager(Server server, Random random)
+    {
+       super(random);
+       _server=server;
+    }
+
+    /**
+     * Configure jdbc connection information via a jdbc Driver
+     * 
+     * @param driverClassName
+     * @param connectionUrl
+     */
+    public void setDriverInfo (String driverClassName, String connectionUrl)
+    {
+        _driverClassName=driverClassName;
+        _connectionUrl=connectionUrl;
+    }
+    
+    /**
+     * Configure jdbc connection information via a jdbc Driver
+     * 
+     * @param driverClass
+     * @param connectionUrl
+     */
+    public void setDriverInfo (Driver driverClass, String connectionUrl)
+    {
+        _driver=driverClass;
+        _connectionUrl=connectionUrl;
+    }
+    
+    
+    public void setDatasource (DataSource ds)
+    {
+        _datasource = ds;
+    }
+    
+    public DataSource getDataSource ()
+    {
+        return _datasource;
+    }
+    
+    public String getDriverClassName()
+    {
+        return _driverClassName;
+    }
+    
+    public String getConnectionUrl ()
+    {
+        return _connectionUrl;
+    }
+    
+    public void setDatasourceName (String jndi)
+    {
+        _jndiName=jndi;
+    }
+    
+    public String getDatasourceName ()
+    {
+        return _jndiName;
+    }
+   
+    public void setBlobType (String name)
+    {
+        _blobType = name;
+    }
+    
+    public String getBlobType ()
+    {
+        return _blobType;
+    }
+    
+    
+    
+    public String getLongType()
+    {
+        return _longType;
+    }
+
+    public void setLongType(String longType)
+    {
+        this._longType = longType;
+    }
+
+    public void setScavengeInterval (long sec)
+    {
+        if (sec<=0)
+            sec=60;
+
+        long old_period=_scavengeIntervalMs;
+        long period=sec*1000L;
+      
+        _scavengeIntervalMs=period;
+        
+        //add a bit of variability into the scavenge time so that not all
+        //nodes with the same scavenge time sync up
+        long tenPercent = _scavengeIntervalMs/10;
+        if ((System.currentTimeMillis()%2) == 0)
+            _scavengeIntervalMs += tenPercent;
+        
+        if (LOG.isDebugEnabled()) 
+            LOG.debug("Scavenging every "+_scavengeIntervalMs+" ms");
+        if (_timer!=null && (period!=old_period || _task==null))
+        {
+            synchronized (this)
+            {
+                if (_task!=null)
+                    _task.cancel();
+                _task = new TimerTask()
+                {
+                    @Override
+                    public void run()
+                    {
+                        scavenge();
+                    }   
+                };
+                _timer.schedule(_task,_scavengeIntervalMs,_scavengeIntervalMs);
+            }
+        }  
+    }
+    
+    public long getScavengeInterval ()
+    {
+        return _scavengeIntervalMs/1000;
+    }
+    
+    
+    public void addSession(HttpSession session)
+    {
+        if (session == null)
+            return;
+        
+        synchronized (_sessionIds)
+        {
+            String id = ((JDBCSessionManager.Session)session).getClusterId();            
+            try
+            {
+                insert(id);
+                _sessionIds.add(id);
+            }
+            catch (Exception e)
+            {
+                LOG.warn("Problem storing session id="+id, e);
+            }
+        }
+    }
+    
+    public void removeSession(HttpSession session)
+    {
+        if (session == null)
+            return;
+        
+        removeSession(((JDBCSessionManager.Session)session).getClusterId());
+    }
+    
+    
+    
+    public void removeSession (String id)
+    {
+
+        if (id == null)
+            return;
+        
+        synchronized (_sessionIds)
+        {  
+            if (LOG.isDebugEnabled())
+                LOG.debug("Removing session id="+id);
+            try
+            {               
+                _sessionIds.remove(id);
+                delete(id);
+            }
+            catch (Exception e)
+            {
+                LOG.warn("Problem removing session id="+id, e);
+            }
+        }
+        
+    }
+    
+
+    /** 
+     * Get the session id without any node identifier suffix.
+     * 
+     * @see org.eclipse.jetty.server.SessionIdManager#getClusterId(java.lang.String)
+     */
+    public String getClusterId(String nodeId)
+    {
+        int dot=nodeId.lastIndexOf('.');
+        return (dot>0)?nodeId.substring(0,dot):nodeId;
+    }
+    
+
+    /** 
+     * Get the session id, including this node's id as a suffix.
+     * 
+     * @see org.eclipse.jetty.server.SessionIdManager#getNodeId(java.lang.String, javax.servlet.http.HttpServletRequest)
+     */
+    public String getNodeId(String clusterId, HttpServletRequest request)
+    {
+        if (_workerName!=null)
+            return clusterId+'.'+_workerName;
+
+        return clusterId;
+    }
+
+
+    public boolean idInUse(String id)
+    {
+        if (id == null)
+            return false;
+        
+        String clusterId = getClusterId(id);
+        boolean inUse = false;
+        synchronized (_sessionIds)
+        {
+            inUse = _sessionIds.contains(clusterId);
+        }
+        
+        
+        if (inUse)
+            return true; //optimisation - if this session is one we've been managing, we can check locally
+
+        //otherwise, we need to go to the database to check
+        try
+        {
+            return exists(clusterId);
+        }
+        catch (Exception e)
+        {
+            LOG.warn("Problem checking inUse for id="+clusterId, e);
+            return false;
+        }
+    }
+
+    /** 
+     * Invalidate the session matching the id on all contexts.
+     * 
+     * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
+     */
+    public void invalidateAll(String id)
+    {            
+        //take the id out of the list of known sessionids for this node
+        removeSession(id);
+        
+        synchronized (_sessionIds)
+        {
+            //tell all contexts that may have a session object with this id to
+            //get rid of them
+            Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
+            for (int i=0; contexts!=null && i<contexts.length; i++)
+            {
+                SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
+                if (sessionHandler != null) 
+                {
+                    SessionManager manager = sessionHandler.getSessionManager();
+
+                    if (manager != null && manager instanceof JDBCSessionManager)
+                    {
+                        ((JDBCSessionManager)manager).invalidateSession(id);
+                    }
+                }
+            }
+        }
+    }
+
+
+    /** 
+     * Start up the id manager.
+     * 
+     * Makes necessary database tables and starts a Session
+     * scavenger thread.
+     */
+    @Override
+    public void doStart()
+    throws Exception
+    {           
+        initializeDatabase();
+        prepareTables();   
+        cleanExpiredSessions();
+        super.doStart();
+        if (LOG.isDebugEnabled()) 
+            LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
+        _timer=new Timer("JDBCSessionScavenger", true);
+        setScavengeInterval(getScavengeInterval());
+    }
+
+    /** 
+     * Stop the scavenger.
+     */
+    @Override
+    public void doStop () 
+    throws Exception
+    {
+        synchronized(this)
+        {
+            if (_task!=null)
+                _task.cancel();
+            if (_timer!=null)
+                _timer.cancel();
+            _timer=null;
+        }
+        _sessionIds.clear();
+        super.doStop();
+    }
+  
+    /**
+     * Get a connection from the driver or datasource.
+     * 
+     * @return the connection for the datasource
+     * @throws SQLException
+     */
+    protected Connection getConnection ()
+    throws SQLException
+    {
+        if (_datasource != null)
+            return _datasource.getConnection();
+        else
+            return DriverManager.getConnection(_connectionUrl);
+    }
+    
+    
+   
+    
+    
+    /**
+     * Set up the tables in the database
+     * @throws SQLException
+     */
+    private void prepareTables()
+    throws SQLException
+    {
+        _createSessionIdTable = "create table "+_sessionIdTable+" (id varchar(120), primary key(id))";
+        _selectBoundedExpiredSessions = "select * from "+_sessionTable+" where expiryTime >= ? and expiryTime <= ?";
+        _selectExpiredSessions = "select * from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
+        _deleteOldExpiredSessions = "delete from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
+
+        _insertId = "insert into "+_sessionIdTable+" (id)  values (?)";
+        _deleteId = "delete from "+_sessionIdTable+" where id = ?";
+        _queryId = "select * from "+_sessionIdTable+" where id = ?";
+
+        Connection connection = null;
+        try
+        {
+            //make the id table
+            connection = getConnection();
+            connection.setAutoCommit(true);
+            DatabaseMetaData metaData = connection.getMetaData();
+            _dbAdaptor = new DatabaseAdaptor(metaData);
+            _sessionTableRowId = _dbAdaptor.getRowIdColumnName();
+
+            //checking for table existence is case-sensitive, but table creation is not
+            String tableName = _dbAdaptor.convertIdentifier(_sessionIdTable);
+            ResultSet result = metaData.getTables(null, null, tableName, null);
+            if (!result.next())
+            {
+                //table does not exist, so create it
+                connection.createStatement().executeUpdate(_createSessionIdTable);
+            }
+            
+            //make the session table if necessary
+            tableName = _dbAdaptor.convertIdentifier(_sessionTable);   
+            result = metaData.getTables(null, null, tableName, null);
+            if (!result.next())
+            {
+                //table does not exist, so create it
+                String blobType = _dbAdaptor.getBlobType();
+                String longType = _dbAdaptor.getLongType();
+                _createSessionTable = "create table "+_sessionTable+" ("+_sessionTableRowId+" varchar(120), sessionId varchar(120), "+
+                                           " contextPath varchar(60), virtualHost varchar(60), lastNode varchar(60), accessTime "+longType+", "+
+                                           " lastAccessTime "+longType+", createTime "+longType+", cookieTime "+longType+", "+
+                                           " lastSavedTime "+longType+", expiryTime "+longType+", map "+blobType+", primary key("+_sessionTableRowId+"))";
+                connection.createStatement().executeUpdate(_createSessionTable);
+            }
+            
+            //make some indexes on the JettySessions table
+            String index1 = "idx_"+_sessionTable+"_expiry";
+            String index2 = "idx_"+_sessionTable+"_session";
+            
+            result = metaData.getIndexInfo(null, null, tableName, false, false);
+            boolean index1Exists = false;
+            boolean index2Exists = false;
+            while (result.next())
+            {
+                String idxName = result.getString("INDEX_NAME");
+                if (index1.equalsIgnoreCase(idxName))
+                    index1Exists = true;
+                else if (index2.equalsIgnoreCase(idxName))
+                    index2Exists = true;
+            }
+            if (!(index1Exists && index2Exists))
+            {
+                Statement statement = connection.createStatement();
+                try
+                {
+                    if (!index1Exists)
+                        statement.executeUpdate("create index "+index1+" on "+_sessionTable+" (expiryTime)");
+                    if (!index2Exists)
+                        statement.executeUpdate("create index "+index2+" on "+_sessionTable+" (sessionId, contextPath)");
+                }
+                finally
+                {
+                    if (statement!=null)
+                    {
+                        try { statement.close(); }
+                        catch(Exception e) { LOG.warn(e); }
+                    }
+                }
+            }
+
+            //set up some strings representing the statements for session manipulation
+            _insertSession = "insert into "+_sessionTable+
+            " ("+_sessionTableRowId+", sessionId, contextPath, virtualHost, lastNode, accessTime, lastAccessTime, createTime, cookieTime, lastSavedTime, expiryTime, map) "+
+            " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+
+            _deleteSession = "delete from "+_sessionTable+
+            " where "+_sessionTableRowId+" = ?";
+            
+            _updateSession = "update "+_sessionTable+
+            " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ?, map = ? where "+_sessionTableRowId+" = ?";
+
+            _updateSessionNode = "update "+_sessionTable+
+            " set lastNode = ? where "+_sessionTableRowId+" = ?";
+
+            _updateSessionAccessTime = "update "+_sessionTable+
+            " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ? where "+_sessionTableRowId+" = ?";
+
+            
+        }
+        finally
+        {
+            if (connection != null)
+                connection.close();
+        }
+    }
+    
+    /**
+     * Insert a new used session id into the table.
+     * 
+     * @param id
+     * @throws SQLException
+     */
+    private void insert (String id)
+    throws SQLException 
+    {
+        Connection connection = null;
+        PreparedStatement statement = null;
+        PreparedStatement query = null;
+        try
+        {
+            connection = getConnection();
+            connection.setAutoCommit(true);            
+            query = connection.prepareStatement(_queryId);
+            query.setString(1, id);
+            ResultSet result = query.executeQuery();
+            //only insert the id if it isn't in the db already 
+            if (!result.next())
+            {
+                statement = connection.prepareStatement(_insertId);
+                statement.setString(1, id);
+                statement.executeUpdate();
+            }
+        }
+        finally
+        {
+            if (query!=null)
+            {
+                try { query.close(); }
+                catch(Exception e) { LOG.warn(e); }
+            }
+
+            if (statement!=null)
+            {
+                try { statement.close(); }
+                catch(Exception e) { LOG.warn(e); }
+            }
+
+            if (connection != null)
+                connection.close();
+        }
+    }
+    
+    /**
+     * Remove a session id from the table.
+     * 
+     * @param id
+     * @throws SQLException
+     */
+    private void delete (String id)
+    throws SQLException
+    {
+        Connection connection = null;
+        PreparedStatement statement = null;
+        try
+        {
+            connection = getConnection();
+            connection.setAutoCommit(true);
+            statement = connection.prepareStatement(_deleteId);
+            statement.setString(1, id);
+            statement.executeUpdate();
+        }
+        finally
+        {
+            if (statement!=null)
+            {
+                try { statement.close(); }
+                catch(Exception e) { LOG.warn(e); }
+            }
+
+            if (connection != null)
+                connection.close();
+        }
+    }
+    
+    
+    /**
+     * Check if a session id exists.
+     * 
+     * @param id
+     * @return
+     * @throws SQLException
+     */
+    private boolean exists (String id)
+    throws SQLException
+    {
+        Connection connection = null;
+        PreparedStatement statement = null;
+        try
+        {
+            connection = getConnection();
+            connection.setAutoCommit(true);
+            statement = connection.prepareStatement(_queryId);
+            statement.setString(1, id);
+            ResultSet result = statement.executeQuery();
+            return result.next();
+        }
+        finally
+        {
+            if (statement!=null)
+            {
+                try { statement.close(); }
+                catch(Exception e) { LOG.warn(e); }
+            }
+
+            if (connection != null)
+                connection.close();
+        }
+    }
+    
+    /**
+     * Look for sessions in the database that have expired.
+     * 
+     * We do this in the SessionIdManager and not the SessionManager so
+     * that we only have 1 scavenger, otherwise if there are n SessionManagers
+     * there would be n scavengers, all contending for the database.
+     * 
+     * We look first for sessions that expired in the previous interval, then
+     * for sessions that expired previously - these are old sessions that no
+     * node is managing any more and have become stuck in the database.
+     */
+    private void scavenge ()
+    {
+        Connection connection = null;
+        PreparedStatement statement = null;
+        List<String> expiredSessionIds = new ArrayList<String>();
+        try
+        {            
+            if (LOG.isDebugEnabled()) 
+                LOG.debug("Scavenge sweep started at "+System.currentTimeMillis());
+            if (_lastScavengeTime > 0)
+            {
+                connection = getConnection();
+                connection.setAutoCommit(true);
+                //"select sessionId from JettySessions where expiryTime > (lastScavengeTime - scanInterval) and expiryTime < lastScavengeTime";
+                statement = connection.prepareStatement(_selectBoundedExpiredSessions);
+                long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
+                long upperBound = _lastScavengeTime;
+                if (LOG.isDebugEnabled()) 
+                    LOG.debug (" Searching for sessions expired between "+lowerBound + " and "+upperBound);
+                
+                statement.setLong(1, lowerBound);
+                statement.setLong(2, upperBound);
+                ResultSet result = statement.executeQuery();
+                while (result.next())
+                {
+                    String sessionId = result.getString("sessionId");
+                    expiredSessionIds.add(sessionId);
+                    if (LOG.isDebugEnabled()) LOG.debug (" Found expired sessionId="+sessionId); 
+                }
+
+                //tell the SessionManagers to expire any sessions with a matching sessionId in memory
+                Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
+                for (int i=0; contexts!=null && i<contexts.length; i++)
+                {
+
+                    SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
+                    if (sessionHandler != null) 
+                    { 
+                        SessionManager manager = sessionHandler.getSessionManager();
+                        if (manager != null && manager instanceof JDBCSessionManager)
+                        {
+                            ((JDBCSessionManager)manager).expire(expiredSessionIds);
+                        }
+                    }
+                }
+
+                //find all sessions that have expired at least a couple of scanIntervals ago and just delete them
+                upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
+                if (upperBound > 0)
+                {
+                    if (LOG.isDebugEnabled()) LOG.debug("Deleting old expired sessions expired before "+upperBound);
+                    try
+                    {
+                        statement = connection.prepareStatement(_deleteOldExpiredSessions);
+                        statement.setLong(1, upperBound);
+                        int rows = statement.executeUpdate();
+                        if (LOG.isDebugEnabled()) LOG.debug("Deleted "+rows+" rows of old sessions expired before "+upperBound);
+                    }
+                    finally
+                    {
+                        if (statement!=null)
+                        {
+                            try { statement.close(); }
+                            catch(Exception e) { LOG.warn(e); }
+                        }
+                    }
+                }
+            }
+        }
+        catch (Exception e)
+        {
+            if (isRunning())    
+                LOG.warn("Problem selecting expired sessions", e);
+            else
+                LOG.ignore(e);
+        }
+        finally
+        {           
+            _lastScavengeTime=System.currentTimeMillis();
+            if (LOG.isDebugEnabled()) LOG.debug("Scavenge sweep ended at "+_lastScavengeTime);
+            if (connection != null)
+            {
+                try
+                {
+                connection.close();
+                }
+                catch (SQLException e)
+                {
+                    LOG.warn(e);
+                }
+            }
+        }
+    }
+    
+    /**
+     * Get rid of sessions and sessionids from sessions that have already expired
+     * @throws Exception
+     */
+    private void cleanExpiredSessions ()
+    {
+        Connection connection = null;
+        PreparedStatement statement = null;
+        Statement sessionsTableStatement = null;
+        Statement sessionIdsTableStatement = null;
+        List<String> expiredSessionIds = new ArrayList<String>();
+        try
+        {     
+            connection = getConnection();
+            connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
+            connection.setAutoCommit(false);
+
+            statement = connection.prepareStatement(_selectExpiredSessions);
+            long now = System.currentTimeMillis();
+            if (LOG.isDebugEnabled()) LOG.debug ("Searching for sessions expired before {}", now);
+
+            statement.setLong(1, now);
+            ResultSet result = statement.executeQuery();
+            while (result.next())
+            {
+                String sessionId = result.getString("sessionId");
+                expiredSessionIds.add(sessionId);
+                if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId={}", sessionId); 
+            }
+            
+            sessionsTableStatement = null;
+            sessionIdsTableStatement = null;
+
+            if (!expiredSessionIds.isEmpty())
+            {
+                sessionsTableStatement = connection.createStatement();
+                sessionsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionTable+" where sessionId in ", expiredSessionIds));
+                sessionIdsTableStatement = connection.createStatement();
+                sessionIdsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionIdTable+" where id in ", expiredSessionIds));
+            }
+            connection.commit();
+
+            synchronized (_sessionIds)
+            {
+                _sessionIds.removeAll(expiredSessionIds); //in case they were in our local cache of session ids
+            }
+        }
+        catch (Exception e)
+        {
+            if (connection != null)
+            {
+                try 
+                { 
+                    LOG.warn("Rolling back clean of expired sessions", e);
+                    connection.rollback();
+                }
+                catch (Exception x) { LOG.warn("Rollback of expired sessions failed", x);}
+            }
+        }
+        finally
+        {
+            if (sessionIdsTableStatement!=null)
+            {
+                try { sessionIdsTableStatement.close(); }
+                catch(Exception e) { LOG.warn(e); }
+            }
+
+            if (sessionsTableStatement!=null)
+            {
+                try { sessionsTableStatement.close(); }
+                catch(Exception e) { LOG.warn(e); }
+            }
+
+            if (statement!=null)
+            {
+                try { statement.close(); }
+                catch(Exception e) { LOG.warn(e); }
+            }
+
+            try
+            {
+                if (connection != null)
+                    connection.close();
+            }
+            catch (SQLException e)
+            {
+                LOG.warn(e);
+            }
+        }
+    }
+    
+    
+    /**
+     * 
+     * @param sql
+     * @param connection
+     * @param expiredSessionIds
+     * @throws Exception
+     */
+    private String createCleanExpiredSessionsSql (String sql,Collection<String> expiredSessionIds)
+    throws Exception
+    {
+        StringBuffer buff = new StringBuffer();
+        buff.append(sql);
+        buff.append("(");
+        Iterator<String> itor = expiredSessionIds.iterator();
+        while (itor.hasNext())
+        {
+            buff.append("'"+(itor.next())+"'");
+            if (itor.hasNext())
+                buff.append(",");
+        }
+        buff.append(")");
+        
+        if (LOG.isDebugEnabled()) LOG.debug("Cleaning expired sessions with: {}", buff);
+        return buff.toString();
+    }
+    
+    private void initializeDatabase ()
+    throws Exception
+    {
+        if (_datasource != null)
+            return; //already set up
+        
+        if (_jndiName!=null)
+        {
+            InitialContext ic = new InitialContext();
+            _datasource = (DataSource)ic.lookup(_jndiName);
+        }
+        else if ( _driver != null && _connectionUrl != null )
+        {
+            DriverManager.registerDriver(_driver);
+        }
+        else if (_driverClassName != null && _connectionUrl != null)
+        {
+            Class.forName(_driverClassName);
+        }
+        else
+            throw new IllegalStateException("No database configured for sessions");
+    }
+    
+   
+}