X-Git-Url: https://code.wpia.club/?p=gigi.git;a=blobdiff_plain;f=lib%2Fjetty%2Forg%2Feclipse%2Fjetty%2Fserver%2Fsession%2FJDBCSessionIdManager.java;fp=lib%2Fjetty%2Forg%2Feclipse%2Fjetty%2Fserver%2Fsession%2FJDBCSessionIdManager.java;h=4c7227d7717a5323b5e30cf21c8479855458c48d;hp=0000000000000000000000000000000000000000;hb=73ef54a38e3930a1a789cdc6b5fa23cdd4c9d086;hpb=515007c7c1351045420669d65b59c08fa46850f2 diff --git a/lib/jetty/org/eclipse/jetty/server/session/JDBCSessionIdManager.java b/lib/jetty/org/eclipse/jetty/server/session/JDBCSessionIdManager.java new file mode 100644 index 00000000..4c7227d7 --- /dev/null +++ b/lib/jetty/org/eclipse/jetty/server/session/JDBCSessionIdManager.java @@ -0,0 +1,1496 @@ +// +// ======================================================================== +// 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.HashSet; +import java.util.Locale; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +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; +import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; +import org.eclipse.jetty.util.thread.Scheduler; + + + +/** + * 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; + public final static int MAX_INTERVAL_NOT_SET = -999; + + protected final HashSet _sessionIds = new HashSet(); + protected Server _server; + protected Driver _driver; + protected String _driverClassName; + protected String _connectionUrl; + protected DataSource _datasource; + protected String _jndiName; + + protected int _deleteBlockSize = 10; //number of ids to include in where 'in' clause + + protected Scheduler.Task _task; //scavenge task + protected Scheduler _scheduler; + protected Scavenger _scavenger; + protected boolean _ownScheduler; + protected long _lastScavengeTime; + protected long _scavengeIntervalMs = 1000L * 60 * 10; //10mins + + + protected String _createSessionIdTable; + protected String _createSessionTable; + + protected String _selectBoundedExpiredSessions; + private String _selectExpiredSessions; + + 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 = new DatabaseAdaptor(); + protected SessionIdTableSchema _sessionIdTableSchema = new SessionIdTableSchema(); + protected SessionTableSchema _sessionTableSchema = new SessionTableSchema(); + + + + + /** + * SessionTableSchema + * + */ + public static class SessionTableSchema + { + protected DatabaseAdaptor _dbAdaptor; + protected String _tableName = "JettySessions"; + protected String _rowIdColumn = "rowId"; + protected String _idColumn = "sessionId"; + protected String _contextPathColumn = "contextPath"; + protected String _virtualHostColumn = "virtualHost"; + protected String _lastNodeColumn = "lastNode"; + protected String _accessTimeColumn = "accessTime"; + protected String _lastAccessTimeColumn = "lastAccessTime"; + protected String _createTimeColumn = "createTime"; + protected String _cookieTimeColumn = "cookieTime"; + protected String _lastSavedTimeColumn = "lastSavedTime"; + protected String _expiryTimeColumn = "expiryTime"; + protected String _maxIntervalColumn = "maxInterval"; + protected String _mapColumn = "map"; + + + protected void setDatabaseAdaptor(DatabaseAdaptor dbadaptor) + { + _dbAdaptor = dbadaptor; + } + + + public String getTableName() + { + return _tableName; + } + public void setTableName(String tableName) + { + checkNotNull(tableName); + _tableName = tableName; + } + public String getRowIdColumn() + { + if ("rowId".equals(_rowIdColumn) && _dbAdaptor.isRowIdReserved()) + _rowIdColumn = "srowId"; + return _rowIdColumn; + } + public void setRowIdColumn(String rowIdColumn) + { + checkNotNull(rowIdColumn); + if (_dbAdaptor == null) + throw new IllegalStateException ("DbAdaptor is null"); + + if (_dbAdaptor.isRowIdReserved() && "rowId".equals(rowIdColumn)) + throw new IllegalArgumentException("rowId is reserved word for Oracle"); + + _rowIdColumn = rowIdColumn; + } + public String getIdColumn() + { + return _idColumn; + } + public void setIdColumn(String idColumn) + { + checkNotNull(idColumn); + _idColumn = idColumn; + } + public String getContextPathColumn() + { + return _contextPathColumn; + } + public void setContextPathColumn(String contextPathColumn) + { + checkNotNull(contextPathColumn); + _contextPathColumn = contextPathColumn; + } + public String getVirtualHostColumn() + { + return _virtualHostColumn; + } + public void setVirtualHostColumn(String virtualHostColumn) + { + checkNotNull(virtualHostColumn); + _virtualHostColumn = virtualHostColumn; + } + public String getLastNodeColumn() + { + return _lastNodeColumn; + } + public void setLastNodeColumn(String lastNodeColumn) + { + checkNotNull(lastNodeColumn); + _lastNodeColumn = lastNodeColumn; + } + public String getAccessTimeColumn() + { + return _accessTimeColumn; + } + public void setAccessTimeColumn(String accessTimeColumn) + { + checkNotNull(accessTimeColumn); + _accessTimeColumn = accessTimeColumn; + } + public String getLastAccessTimeColumn() + { + return _lastAccessTimeColumn; + } + public void setLastAccessTimeColumn(String lastAccessTimeColumn) + { + checkNotNull(lastAccessTimeColumn); + _lastAccessTimeColumn = lastAccessTimeColumn; + } + public String getCreateTimeColumn() + { + return _createTimeColumn; + } + public void setCreateTimeColumn(String createTimeColumn) + { + checkNotNull(createTimeColumn); + _createTimeColumn = createTimeColumn; + } + public String getCookieTimeColumn() + { + return _cookieTimeColumn; + } + public void setCookieTimeColumn(String cookieTimeColumn) + { + checkNotNull(cookieTimeColumn); + _cookieTimeColumn = cookieTimeColumn; + } + public String getLastSavedTimeColumn() + { + return _lastSavedTimeColumn; + } + public void setLastSavedTimeColumn(String lastSavedTimeColumn) + { + checkNotNull(lastSavedTimeColumn); + _lastSavedTimeColumn = lastSavedTimeColumn; + } + public String getExpiryTimeColumn() + { + return _expiryTimeColumn; + } + public void setExpiryTimeColumn(String expiryTimeColumn) + { + checkNotNull(expiryTimeColumn); + _expiryTimeColumn = expiryTimeColumn; + } + public String getMaxIntervalColumn() + { + return _maxIntervalColumn; + } + public void setMaxIntervalColumn(String maxIntervalColumn) + { + checkNotNull(maxIntervalColumn); + _maxIntervalColumn = maxIntervalColumn; + } + public String getMapColumn() + { + return _mapColumn; + } + public void setMapColumn(String mapColumn) + { + checkNotNull(mapColumn); + _mapColumn = mapColumn; + } + + public String getCreateStatementAsString () + { + if (_dbAdaptor == null) + throw new IllegalStateException ("No DBAdaptor"); + + String blobType = _dbAdaptor.getBlobType(); + String longType = _dbAdaptor.getLongType(); + + return "create table "+_tableName+" ("+getRowIdColumn()+" varchar(120), "+_idColumn+" varchar(120), "+ + _contextPathColumn+" varchar(60), "+_virtualHostColumn+" varchar(60), "+_lastNodeColumn+" varchar(60), "+_accessTimeColumn+" "+longType+", "+ + _lastAccessTimeColumn+" "+longType+", "+_createTimeColumn+" "+longType+", "+_cookieTimeColumn+" "+longType+", "+ + _lastSavedTimeColumn+" "+longType+", "+_expiryTimeColumn+" "+longType+", "+_maxIntervalColumn+" "+longType+", "+ + _mapColumn+" "+blobType+", primary key("+getRowIdColumn()+"))"; + } + + public String getCreateIndexOverExpiryStatementAsString (String indexName) + { + return "create index "+indexName+" on "+getTableName()+" ("+getExpiryTimeColumn()+")"; + } + + public String getCreateIndexOverSessionStatementAsString (String indexName) + { + return "create index "+indexName+" on "+getTableName()+" ("+getIdColumn()+", "+getContextPathColumn()+")"; + } + + public String getAlterTableForMaxIntervalAsString () + { + if (_dbAdaptor == null) + throw new IllegalStateException ("No DBAdaptor"); + String longType = _dbAdaptor.getLongType(); + return "alter table "+getTableName()+" add "+getMaxIntervalColumn()+" "+longType+" not null default "+MAX_INTERVAL_NOT_SET; + } + + private void checkNotNull(String s) + { + if (s == null) + throw new IllegalArgumentException(s); + } + public String getInsertSessionStatementAsString() + { + return "insert into "+getTableName()+ + " ("+getRowIdColumn()+", "+getIdColumn()+", "+getContextPathColumn()+", "+getVirtualHostColumn()+", "+getLastNodeColumn()+ + ", "+getAccessTimeColumn()+", "+getLastAccessTimeColumn()+", "+getCreateTimeColumn()+", "+getCookieTimeColumn()+ + ", "+getLastSavedTimeColumn()+", "+getExpiryTimeColumn()+", "+getMaxIntervalColumn()+", "+getMapColumn()+") "+ + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } + public String getDeleteSessionStatementAsString() + { + return "delete from "+getTableName()+ + " where "+getRowIdColumn()+" = ?"; + } + public String getUpdateSessionStatementAsString() + { + return "update "+getTableName()+ + " set "+getIdColumn()+" = ?, "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+ + getLastAccessTimeColumn()+" = ?, "+getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+ + getMaxIntervalColumn()+" = ?, "+getMapColumn()+" = ? where "+getRowIdColumn()+" = ?"; + } + public String getUpdateSessionNodeStatementAsString() + { + return "update "+getTableName()+ + " set "+getLastNodeColumn()+" = ? where "+getRowIdColumn()+" = ?"; + } + public String getUpdateSessionAccessTimeStatementAsString() + { + return "update "+getTableName()+ + " set "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+getLastAccessTimeColumn()+" = ?, "+ + getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+getMaxIntervalColumn()+" = ? where "+getRowIdColumn()+" = ?"; + } + + public String getBoundedExpiredSessionsStatementAsString() + { + return "select * from "+getTableName()+" where "+getLastNodeColumn()+" = ? and "+getExpiryTimeColumn()+" >= ? and "+getExpiryTimeColumn()+" <= ?"; + } + + public String getSelectExpiredSessionsStatementAsString() + { + return "select * from "+getTableName()+" where "+getExpiryTimeColumn()+" >0 and "+getExpiryTimeColumn()+" <= ?"; + } + + public PreparedStatement getLoadStatement (Connection connection, String rowId, String contextPath, String virtualHosts) + throws SQLException + { + if (_dbAdaptor == null) + throw new IllegalStateException("No DB adaptor"); + + + if (contextPath == null || "".equals(contextPath)) + { + if (_dbAdaptor.isEmptyStringNull()) + { + PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+ + " where "+getIdColumn()+" = ? and "+ + getContextPathColumn()+" is null and "+ + getVirtualHostColumn()+" = ?"); + statement.setString(1, rowId); + statement.setString(2, virtualHosts); + + return statement; + } + } + + PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+ + " where "+getIdColumn()+" = ? and "+getContextPathColumn()+ + " = ? and "+getVirtualHostColumn()+" = ?"); + statement.setString(1, rowId); + statement.setString(2, contextPath); + statement.setString(3, virtualHosts); + + return statement; + } + } + + + + /** + * SessionIdTableSchema + * + */ + public static class SessionIdTableSchema + { + protected DatabaseAdaptor _dbAdaptor; + protected String _tableName = "JettySessionIds"; + protected String _idColumn = "id"; + + public void setDatabaseAdaptor(DatabaseAdaptor dbAdaptor) + { + _dbAdaptor = dbAdaptor; + } + public String getIdColumn() + { + return _idColumn; + } + + public void setIdColumn(String idColumn) + { + checkNotNull(idColumn); + _idColumn = idColumn; + } + + public String getTableName() + { + return _tableName; + } + + public void setTableName(String tableName) + { + checkNotNull(tableName); + _tableName = tableName; + } + + public String getInsertStatementAsString () + { + return "insert into "+_tableName+" ("+_idColumn+") values (?)"; + } + + public String getDeleteStatementAsString () + { + return "delete from "+_tableName+" where "+_idColumn+" = ?"; + } + + public String getSelectStatementAsString () + { + return "select * from "+_tableName+" where "+_idColumn+" = ?"; + } + + public String getCreateStatementAsString () + { + return "create table "+_tableName+" ("+_idColumn+" varchar(120), primary key("+_idColumn+"))"; + } + + private void checkNotNull(String s) + { + if (s == null) + throw new IllegalArgumentException(s); + } + } + + + /** + * 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 static class DatabaseAdaptor + { + String _dbName; + boolean _isLower; + boolean _isUpper; + + 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 + + + public DatabaseAdaptor () + { + } + + + public void adaptTo(DatabaseMetaData dbMeta) + throws SQLException + { + _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH); + LOG.debug ("Using database {}",_dbName); + _isLower = dbMeta.storesLowerCaseIdentifiers(); + _isUpper = dbMeta.storesUpperCaseIdentifiers(); + } + + + public void setBlobType(String blobType) + { + _blobType = blobType; + } + + public String getBlobType () + { + if (_blobType != null) + return _blobType; + + if (_dbName.startsWith("postgres")) + return "bytea"; + + return "blob"; + } + + + public void setLongType(String longType) + { + _longType = longType; + } + + + public String getLongType () + { + if (_longType != null) + return _longType; + + if (_dbName == null) + throw new IllegalStateException ("DbAdaptor missing metadata"); + + if (_dbName.startsWith("oracle")) + return "number(20)"; + + return "bigint"; + } + + + /** + * 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 (_dbName == null) + throw new IllegalStateException ("DbAdaptor missing metadata"); + + if (_isLower) + return identifier.toLowerCase(Locale.ENGLISH); + if (_isUpper) + return identifier.toUpperCase(Locale.ENGLISH); + + return identifier; + } + + public String getDBName () + { + return _dbName; + } + + + public InputStream getBlobInputStream (ResultSet result, String columnName) + throws SQLException + { + if (_dbName == null) + throw new IllegalStateException ("DbAdaptor missing metadata"); + + if (_dbName.startsWith("postgres")) + { + byte[] bytes = result.getBytes(columnName); + return new ByteArrayInputStream(bytes); + } + + Blob blob = result.getBlob(columnName); + return blob.getBinaryStream(); + } + + + public boolean isEmptyStringNull () + { + if (_dbName == null) + throw new IllegalStateException ("DbAdaptor missing metadata"); + + return (_dbName.startsWith("oracle")); + } + + /** + * rowId is a reserved word for Oracle, so change the name of this column + * @return true if db in use is oracle + */ + public boolean isRowIdReserved () + { + if (_dbName == null) + throw new IllegalStateException ("DbAdaptor missing metadata"); + + return (_dbName != null && _dbName.startsWith("oracle")); + } + } + + + /** + * Scavenger + * + */ + protected class Scavenger implements Runnable + { + + @Override + public void run() + { + try + { + scavenge(); + } + finally + { + if (_scheduler != null && _scheduler.isRunning()) + _scheduler.schedule(this, _scavengeIntervalMs, TimeUnit.MILLISECONDS); + } + } + } + + + 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; + } + + /** + * @param name + * @deprecated see DbAdaptor.setBlobType + */ + public void setBlobType (String name) + { + _dbAdaptor.setBlobType(name); + } + + public DatabaseAdaptor getDbAdaptor() + { + return _dbAdaptor; + } + + public void setDbAdaptor(DatabaseAdaptor dbAdaptor) + { + if (dbAdaptor == null) + throw new IllegalStateException ("DbAdaptor cannot be null"); + + _dbAdaptor = dbAdaptor; + } + + /** + * @return + * @deprecated see DbAdaptor.getBlobType + */ + public String getBlobType () + { + return _dbAdaptor.getBlobType(); + } + + /** + * @return + * @deprecated see DbAdaptor.getLogType + */ + public String getLongType() + { + return _dbAdaptor.getLongType(); + } + + /** + * @param longType + * @deprecated see DbAdaptor.setLongType + */ + public void setLongType(String longType) + { + _dbAdaptor.setLongType(longType); + } + + public SessionIdTableSchema getSessionIdTableSchema() + { + return _sessionIdTableSchema; + } + + public void setSessionIdTableSchema(SessionIdTableSchema sessionIdTableSchema) + { + if (sessionIdTableSchema == null) + throw new IllegalArgumentException("Null SessionIdTableSchema"); + + _sessionIdTableSchema = sessionIdTableSchema; + } + + public SessionTableSchema getSessionTableSchema() + { + return _sessionTableSchema; + } + + public void setSessionTableSchema(SessionTableSchema sessionTableSchema) + { + _sessionTableSchema = sessionTableSchema; + } + + public void setDeleteBlockSize (int bsize) + { + this._deleteBlockSize = bsize; + } + + public int getDeleteBlockSize () + { + return this._deleteBlockSize; + } + + 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)) + if (_scheduler != null && (period!=old_period || _task==null)) + { + synchronized (this) + { + if (_task!=null) + _task.cancel(); + if (_scavenger == null) + _scavenger = new Scavenger(); + _task = _scheduler.schedule(_scavenger,_scavengeIntervalMs,TimeUnit.MILLISECONDS); + } + } + } + + public long getScavengeInterval () + { + return _scavengeIntervalMs/1000; + } + + + @Override + 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 addSession(String id) + { + if (id == null) + return; + + synchronized (_sessionIds) + { + try + { + insert(id); + _sessionIds.add(id); + } + catch (Exception e) + { + LOG.warn("Problem storing session id="+id, e); + } + } + } + + + + @Override + 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 sessionid="+id); + try + { + _sessionIds.remove(id); + delete(id); + } + catch (Exception e) + { + LOG.warn("Problem removing session id="+id, e); + } + } + + } + + + @Override + 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) + */ + @Override + 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 0) + { + connection = getConnection(); + connection.setAutoCommit(true); + Set expiredSessionIds = new HashSet(); + + + //Pass 1: find sessions for which we were last managing node that have just expired since last pass + long lowerBound = (_lastScavengeTime - _scavengeIntervalMs); + long upperBound = _lastScavengeTime; + if (LOG.isDebugEnabled()) + LOG.debug (getWorkerName()+"- Pass 1: Searching for sessions expired between "+lowerBound + " and "+upperBound); + + try (PreparedStatement statement = connection.prepareStatement(_selectBoundedExpiredSessions)) + { + statement.setString(1, getWorkerName()); + statement.setLong(2, lowerBound); + statement.setLong(3, upperBound); + try (ResultSet result = statement.executeQuery()) + { + while (result.next()) + { + String sessionId = result.getString(_sessionTableSchema.getIdColumn()); + expiredSessionIds.add(sessionId); + if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId); + } + } + } + scavengeSessions(expiredSessionIds, false); + + + //Pass 2: find sessions that have expired a while ago for which this node was their last manager + try (PreparedStatement selectExpiredSessions = connection.prepareStatement(_selectExpiredSessions)) + { + expiredSessionIds.clear(); + upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs); + if (upperBound > 0) + { + if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 2: Searching for sessions expired before "+upperBound); + selectExpiredSessions.setLong(1, upperBound); + try (ResultSet result = selectExpiredSessions.executeQuery()) + { + while (result.next()) + { + String sessionId = result.getString(_sessionTableSchema.getIdColumn()); + String lastNode = result.getString(_sessionTableSchema.getLastNodeColumn()); + if ((getWorkerName() == null && lastNode == null) || (getWorkerName() != null && getWorkerName().equals(lastNode))) + expiredSessionIds.add(sessionId); + if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId+" last managed by "+getWorkerName()); + } + } + scavengeSessions(expiredSessionIds, false); + } + + + //Pass 3: + //find all sessions that have expired at least a couple of scanIntervals ago + //if we did not succeed in loading them (eg their related context no longer exists, can't be loaded etc) then + //they are simply deleted + upperBound = _lastScavengeTime - (3 * _scavengeIntervalMs); + expiredSessionIds.clear(); + if (upperBound > 0) + { + if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 3: searching for sessions expired before "+upperBound); + selectExpiredSessions.setLong(1, upperBound); + try (ResultSet result = selectExpiredSessions.executeQuery()) + { + while (result.next()) + { + String sessionId = result.getString(_sessionTableSchema.getIdColumn()); + expiredSessionIds.add(sessionId); + if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId); + } + } + scavengeSessions(expiredSessionIds, true); + } + } + } + } + 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(getWorkerName()+"- Scavenge sweep ended at "+_lastScavengeTime); + if (connection != null) + { + try + { + connection.close(); + } + catch (SQLException e) + { + LOG.warn(e); + } + } + } + } + + + /** + * @param expiredSessionIds + */ + private void scavengeSessions (Set expiredSessionIds, boolean forceDelete) + { + Set remainingIds = new HashSet(expiredSessionIds); + Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class); + for (int i=0; contexts!=null && i successfullyExpiredIds = ((JDBCSessionManager)manager).expire(expiredSessionIds); + if (successfullyExpiredIds != null) + remainingIds.removeAll(successfullyExpiredIds); + } + } + } + + //Any remaining ids are of those sessions that no context removed + if (!remainingIds.isEmpty() && forceDelete) + { + LOG.info("Forcibly deleting unrecoverable expired sessions {}", remainingIds); + try + { + //ensure they aren't in the local list of in-use session ids + synchronized (_sessionIds) + { + _sessionIds.removeAll(remainingIds); + } + + cleanExpiredSessionIds(remainingIds); + } + catch (Exception e) + { + LOG.warn("Error removing expired session ids", e); + } + } + } + + + + + private void cleanExpiredSessionIds (Set expiredIds) + throws Exception + { + if (expiredIds == null || expiredIds.isEmpty()) + return; + + String[] ids = expiredIds.toArray(new String[expiredIds.size()]); + try (Connection con = getConnection()) + { + con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); + con.setAutoCommit(false); + + int start = 0; + int end = 0; + int blocksize = _deleteBlockSize; + int block = 0; + + try (Statement statement = con.createStatement()) + { + while (end < ids.length) + { + start = block*blocksize; + if ((ids.length - start) >= blocksize) + end = start + blocksize; + else + end = ids.length; + + //take them out of the sessionIds table + statement.executeUpdate(fillInClause("delete from "+_sessionIdTableSchema.getTableName()+" where "+_sessionIdTableSchema.getIdColumn()+" in ", ids, start, end)); + //take them out of the sessions table + statement.executeUpdate(fillInClause("delete from "+_sessionTableSchema.getTableName()+" where "+_sessionTableSchema.getIdColumn()+" in ", ids, start, end)); + block++; + } + } + catch (Exception e) + { + con.rollback(); + throw e; + } + con.commit(); + } + } + + + + /** + * + * @param sql + * @param atoms + * @throws Exception + */ + private String fillInClause (String sql, String[] literals, int start, int end) + throws Exception + { + StringBuffer buff = new StringBuffer(); + buff.append(sql); + buff.append("("); + for (int i=start; i