2 // ========================================================================
3 // Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4 // ------------------------------------------------------------------------
5 // All rights reserved. This program and the accompanying materials
6 // are made available under the terms of the Eclipse Public License v1.0
7 // and Apache License v2.0 which accompanies this distribution.
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
19 package org.eclipse.jetty.server.session;
21 import java.io.ByteArrayInputStream;
22 import java.io.InputStream;
24 import java.sql.Connection;
25 import java.sql.DatabaseMetaData;
26 import java.sql.Driver;
27 import java.sql.DriverManager;
28 import java.sql.PreparedStatement;
29 import java.sql.ResultSet;
30 import java.sql.SQLException;
31 import java.sql.Statement;
32 import java.util.HashSet;
33 import java.util.Locale;
34 import java.util.Random;
36 import java.util.concurrent.TimeUnit;
38 import javax.naming.InitialContext;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpSession;
41 import javax.sql.DataSource;
43 import org.eclipse.jetty.server.Handler;
44 import org.eclipse.jetty.server.Server;
45 import org.eclipse.jetty.server.SessionManager;
46 import org.eclipse.jetty.server.handler.ContextHandler;
47 import org.eclipse.jetty.util.log.Logger;
48 import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
49 import org.eclipse.jetty.util.thread.Scheduler;
54 * JDBCSessionIdManager
56 * SessionIdManager implementation that uses a database to store in-use session ids,
57 * to support distributed sessions.
60 public class JDBCSessionIdManager extends AbstractSessionIdManager
62 final static Logger LOG = SessionHandler.LOG;
63 public final static int MAX_INTERVAL_NOT_SET = -999;
65 protected final HashSet<String> _sessionIds = new HashSet<String>();
66 protected Server _server;
67 protected Driver _driver;
68 protected String _driverClassName;
69 protected String _connectionUrl;
70 protected DataSource _datasource;
71 protected String _jndiName;
73 protected int _deleteBlockSize = 10; //number of ids to include in where 'in' clause
75 protected Scheduler.Task _task; //scavenge task
76 protected Scheduler _scheduler;
77 protected Scavenger _scavenger;
78 protected boolean _ownScheduler;
79 protected long _lastScavengeTime;
80 protected long _scavengeIntervalMs = 1000L * 60 * 10; //10mins
83 protected String _createSessionIdTable;
84 protected String _createSessionTable;
86 protected String _selectBoundedExpiredSessions;
87 private String _selectExpiredSessions;
89 protected String _insertId;
90 protected String _deleteId;
91 protected String _queryId;
93 protected String _insertSession;
94 protected String _deleteSession;
95 protected String _updateSession;
96 protected String _updateSessionNode;
97 protected String _updateSessionAccessTime;
99 protected DatabaseAdaptor _dbAdaptor = new DatabaseAdaptor();
100 protected SessionIdTableSchema _sessionIdTableSchema = new SessionIdTableSchema();
101 protected SessionTableSchema _sessionTableSchema = new SessionTableSchema();
110 public static class SessionTableSchema
112 protected DatabaseAdaptor _dbAdaptor;
113 protected String _tableName = "JettySessions";
114 protected String _rowIdColumn = "rowId";
115 protected String _idColumn = "sessionId";
116 protected String _contextPathColumn = "contextPath";
117 protected String _virtualHostColumn = "virtualHost";
118 protected String _lastNodeColumn = "lastNode";
119 protected String _accessTimeColumn = "accessTime";
120 protected String _lastAccessTimeColumn = "lastAccessTime";
121 protected String _createTimeColumn = "createTime";
122 protected String _cookieTimeColumn = "cookieTime";
123 protected String _lastSavedTimeColumn = "lastSavedTime";
124 protected String _expiryTimeColumn = "expiryTime";
125 protected String _maxIntervalColumn = "maxInterval";
126 protected String _mapColumn = "map";
129 protected void setDatabaseAdaptor(DatabaseAdaptor dbadaptor)
131 _dbAdaptor = dbadaptor;
135 public String getTableName()
139 public void setTableName(String tableName)
141 checkNotNull(tableName);
142 _tableName = tableName;
144 public String getRowIdColumn()
146 if ("rowId".equals(_rowIdColumn) && _dbAdaptor.isRowIdReserved())
147 _rowIdColumn = "srowId";
150 public void setRowIdColumn(String rowIdColumn)
152 checkNotNull(rowIdColumn);
153 if (_dbAdaptor == null)
154 throw new IllegalStateException ("DbAdaptor is null");
156 if (_dbAdaptor.isRowIdReserved() && "rowId".equals(rowIdColumn))
157 throw new IllegalArgumentException("rowId is reserved word for Oracle");
159 _rowIdColumn = rowIdColumn;
161 public String getIdColumn()
165 public void setIdColumn(String idColumn)
167 checkNotNull(idColumn);
168 _idColumn = idColumn;
170 public String getContextPathColumn()
172 return _contextPathColumn;
174 public void setContextPathColumn(String contextPathColumn)
176 checkNotNull(contextPathColumn);
177 _contextPathColumn = contextPathColumn;
179 public String getVirtualHostColumn()
181 return _virtualHostColumn;
183 public void setVirtualHostColumn(String virtualHostColumn)
185 checkNotNull(virtualHostColumn);
186 _virtualHostColumn = virtualHostColumn;
188 public String getLastNodeColumn()
190 return _lastNodeColumn;
192 public void setLastNodeColumn(String lastNodeColumn)
194 checkNotNull(lastNodeColumn);
195 _lastNodeColumn = lastNodeColumn;
197 public String getAccessTimeColumn()
199 return _accessTimeColumn;
201 public void setAccessTimeColumn(String accessTimeColumn)
203 checkNotNull(accessTimeColumn);
204 _accessTimeColumn = accessTimeColumn;
206 public String getLastAccessTimeColumn()
208 return _lastAccessTimeColumn;
210 public void setLastAccessTimeColumn(String lastAccessTimeColumn)
212 checkNotNull(lastAccessTimeColumn);
213 _lastAccessTimeColumn = lastAccessTimeColumn;
215 public String getCreateTimeColumn()
217 return _createTimeColumn;
219 public void setCreateTimeColumn(String createTimeColumn)
221 checkNotNull(createTimeColumn);
222 _createTimeColumn = createTimeColumn;
224 public String getCookieTimeColumn()
226 return _cookieTimeColumn;
228 public void setCookieTimeColumn(String cookieTimeColumn)
230 checkNotNull(cookieTimeColumn);
231 _cookieTimeColumn = cookieTimeColumn;
233 public String getLastSavedTimeColumn()
235 return _lastSavedTimeColumn;
237 public void setLastSavedTimeColumn(String lastSavedTimeColumn)
239 checkNotNull(lastSavedTimeColumn);
240 _lastSavedTimeColumn = lastSavedTimeColumn;
242 public String getExpiryTimeColumn()
244 return _expiryTimeColumn;
246 public void setExpiryTimeColumn(String expiryTimeColumn)
248 checkNotNull(expiryTimeColumn);
249 _expiryTimeColumn = expiryTimeColumn;
251 public String getMaxIntervalColumn()
253 return _maxIntervalColumn;
255 public void setMaxIntervalColumn(String maxIntervalColumn)
257 checkNotNull(maxIntervalColumn);
258 _maxIntervalColumn = maxIntervalColumn;
260 public String getMapColumn()
264 public void setMapColumn(String mapColumn)
266 checkNotNull(mapColumn);
267 _mapColumn = mapColumn;
270 public String getCreateStatementAsString ()
272 if (_dbAdaptor == null)
273 throw new IllegalStateException ("No DBAdaptor");
275 String blobType = _dbAdaptor.getBlobType();
276 String longType = _dbAdaptor.getLongType();
278 return "create table "+_tableName+" ("+getRowIdColumn()+" varchar(120), "+_idColumn+" varchar(120), "+
279 _contextPathColumn+" varchar(60), "+_virtualHostColumn+" varchar(60), "+_lastNodeColumn+" varchar(60), "+_accessTimeColumn+" "+longType+", "+
280 _lastAccessTimeColumn+" "+longType+", "+_createTimeColumn+" "+longType+", "+_cookieTimeColumn+" "+longType+", "+
281 _lastSavedTimeColumn+" "+longType+", "+_expiryTimeColumn+" "+longType+", "+_maxIntervalColumn+" "+longType+", "+
282 _mapColumn+" "+blobType+", primary key("+getRowIdColumn()+"))";
285 public String getCreateIndexOverExpiryStatementAsString (String indexName)
287 return "create index "+indexName+" on "+getTableName()+" ("+getExpiryTimeColumn()+")";
290 public String getCreateIndexOverSessionStatementAsString (String indexName)
292 return "create index "+indexName+" on "+getTableName()+" ("+getIdColumn()+", "+getContextPathColumn()+")";
295 public String getAlterTableForMaxIntervalAsString ()
297 if (_dbAdaptor == null)
298 throw new IllegalStateException ("No DBAdaptor");
299 String longType = _dbAdaptor.getLongType();
300 return "alter table "+getTableName()+" add "+getMaxIntervalColumn()+" "+longType+" not null default "+MAX_INTERVAL_NOT_SET;
303 private void checkNotNull(String s)
306 throw new IllegalArgumentException(s);
308 public String getInsertSessionStatementAsString()
310 return "insert into "+getTableName()+
311 " ("+getRowIdColumn()+", "+getIdColumn()+", "+getContextPathColumn()+", "+getVirtualHostColumn()+", "+getLastNodeColumn()+
312 ", "+getAccessTimeColumn()+", "+getLastAccessTimeColumn()+", "+getCreateTimeColumn()+", "+getCookieTimeColumn()+
313 ", "+getLastSavedTimeColumn()+", "+getExpiryTimeColumn()+", "+getMaxIntervalColumn()+", "+getMapColumn()+") "+
314 " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
316 public String getDeleteSessionStatementAsString()
318 return "delete from "+getTableName()+
319 " where "+getRowIdColumn()+" = ?";
321 public String getUpdateSessionStatementAsString()
323 return "update "+getTableName()+
324 " set "+getIdColumn()+" = ?, "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+
325 getLastAccessTimeColumn()+" = ?, "+getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+
326 getMaxIntervalColumn()+" = ?, "+getMapColumn()+" = ? where "+getRowIdColumn()+" = ?";
328 public String getUpdateSessionNodeStatementAsString()
330 return "update "+getTableName()+
331 " set "+getLastNodeColumn()+" = ? where "+getRowIdColumn()+" = ?";
333 public String getUpdateSessionAccessTimeStatementAsString()
335 return "update "+getTableName()+
336 " set "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+getLastAccessTimeColumn()+" = ?, "+
337 getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+getMaxIntervalColumn()+" = ? where "+getRowIdColumn()+" = ?";
340 public String getBoundedExpiredSessionsStatementAsString()
342 return "select * from "+getTableName()+" where "+getLastNodeColumn()+" = ? and "+getExpiryTimeColumn()+" >= ? and "+getExpiryTimeColumn()+" <= ?";
345 public String getSelectExpiredSessionsStatementAsString()
347 return "select * from "+getTableName()+" where "+getExpiryTimeColumn()+" >0 and "+getExpiryTimeColumn()+" <= ?";
350 public PreparedStatement getLoadStatement (Connection connection, String rowId, String contextPath, String virtualHosts)
353 if (_dbAdaptor == null)
354 throw new IllegalStateException("No DB adaptor");
357 if (contextPath == null || "".equals(contextPath))
359 if (_dbAdaptor.isEmptyStringNull())
361 PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+
362 " where "+getIdColumn()+" = ? and "+
363 getContextPathColumn()+" is null and "+
364 getVirtualHostColumn()+" = ?");
365 statement.setString(1, rowId);
366 statement.setString(2, virtualHosts);
372 PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+
373 " where "+getIdColumn()+" = ? and "+getContextPathColumn()+
374 " = ? and "+getVirtualHostColumn()+" = ?");
375 statement.setString(1, rowId);
376 statement.setString(2, contextPath);
377 statement.setString(3, virtualHosts);
386 * SessionIdTableSchema
389 public static class SessionIdTableSchema
391 protected DatabaseAdaptor _dbAdaptor;
392 protected String _tableName = "JettySessionIds";
393 protected String _idColumn = "id";
395 public void setDatabaseAdaptor(DatabaseAdaptor dbAdaptor)
397 _dbAdaptor = dbAdaptor;
399 public String getIdColumn()
404 public void setIdColumn(String idColumn)
406 checkNotNull(idColumn);
407 _idColumn = idColumn;
410 public String getTableName()
415 public void setTableName(String tableName)
417 checkNotNull(tableName);
418 _tableName = tableName;
421 public String getInsertStatementAsString ()
423 return "insert into "+_tableName+" ("+_idColumn+") values (?)";
426 public String getDeleteStatementAsString ()
428 return "delete from "+_tableName+" where "+_idColumn+" = ?";
431 public String getSelectStatementAsString ()
433 return "select * from "+_tableName+" where "+_idColumn+" = ?";
436 public String getCreateStatementAsString ()
438 return "create table "+_tableName+" ("+_idColumn+" varchar(120), primary key("+_idColumn+"))";
441 private void checkNotNull(String s)
444 throw new IllegalArgumentException(s);
452 * Handles differences between databases.
454 * Postgres uses the getBytes and setBinaryStream methods to access
455 * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
456 * is happy to use the "blob" type and getBlob() methods instead.
458 * TODO if the differences become more major it would be worthwhile
459 * refactoring this class.
461 public static class DatabaseAdaptor
467 protected String _blobType; //if not set, is deduced from the type of the database at runtime
468 protected String _longType; //if not set, is deduced from the type of the database at runtime
471 public DatabaseAdaptor ()
476 public void adaptTo(DatabaseMetaData dbMeta)
479 _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH);
480 LOG.debug ("Using database {}",_dbName);
481 _isLower = dbMeta.storesLowerCaseIdentifiers();
482 _isUpper = dbMeta.storesUpperCaseIdentifiers();
486 public void setBlobType(String blobType)
488 _blobType = blobType;
491 public String getBlobType ()
493 if (_blobType != null)
496 if (_dbName.startsWith("postgres"))
503 public void setLongType(String longType)
505 _longType = longType;
509 public String getLongType ()
511 if (_longType != null)
515 throw new IllegalStateException ("DbAdaptor missing metadata");
517 if (_dbName.startsWith("oracle"))
525 * Convert a camel case identifier into either upper or lower
526 * depending on the way the db stores identifiers.
529 * @return the converted identifier
531 public String convertIdentifier (String identifier)
534 throw new IllegalStateException ("DbAdaptor missing metadata");
537 return identifier.toLowerCase(Locale.ENGLISH);
539 return identifier.toUpperCase(Locale.ENGLISH);
544 public String getDBName ()
550 public InputStream getBlobInputStream (ResultSet result, String columnName)
554 throw new IllegalStateException ("DbAdaptor missing metadata");
556 if (_dbName.startsWith("postgres"))
558 byte[] bytes = result.getBytes(columnName);
559 return new ByteArrayInputStream(bytes);
562 Blob blob = result.getBlob(columnName);
563 return blob.getBinaryStream();
567 public boolean isEmptyStringNull ()
570 throw new IllegalStateException ("DbAdaptor missing metadata");
572 return (_dbName.startsWith("oracle"));
576 * rowId is a reserved word for Oracle, so change the name of this column
577 * @return true if db in use is oracle
579 public boolean isRowIdReserved ()
582 throw new IllegalStateException ("DbAdaptor missing metadata");
584 return (_dbName != null && _dbName.startsWith("oracle"));
593 protected class Scavenger implements Runnable
605 if (_scheduler != null && _scheduler.isRunning())
606 _scheduler.schedule(this, _scavengeIntervalMs, TimeUnit.MILLISECONDS);
612 public JDBCSessionIdManager(Server server)
618 public JDBCSessionIdManager(Server server, Random random)
625 * Configure jdbc connection information via a jdbc Driver
627 * @param driverClassName
628 * @param connectionUrl
630 public void setDriverInfo (String driverClassName, String connectionUrl)
632 _driverClassName=driverClassName;
633 _connectionUrl=connectionUrl;
637 * Configure jdbc connection information via a jdbc Driver
640 * @param connectionUrl
642 public void setDriverInfo (Driver driverClass, String connectionUrl)
645 _connectionUrl=connectionUrl;
649 public void setDatasource (DataSource ds)
654 public DataSource getDataSource ()
659 public String getDriverClassName()
661 return _driverClassName;
664 public String getConnectionUrl ()
666 return _connectionUrl;
669 public void setDatasourceName (String jndi)
674 public String getDatasourceName ()
681 * @deprecated see DbAdaptor.setBlobType
683 public void setBlobType (String name)
685 _dbAdaptor.setBlobType(name);
688 public DatabaseAdaptor getDbAdaptor()
693 public void setDbAdaptor(DatabaseAdaptor dbAdaptor)
695 if (dbAdaptor == null)
696 throw new IllegalStateException ("DbAdaptor cannot be null");
698 _dbAdaptor = dbAdaptor;
703 * @deprecated see DbAdaptor.getBlobType
705 public String getBlobType ()
707 return _dbAdaptor.getBlobType();
712 * @deprecated see DbAdaptor.getLogType
714 public String getLongType()
716 return _dbAdaptor.getLongType();
721 * @deprecated see DbAdaptor.setLongType
723 public void setLongType(String longType)
725 _dbAdaptor.setLongType(longType);
728 public SessionIdTableSchema getSessionIdTableSchema()
730 return _sessionIdTableSchema;
733 public void setSessionIdTableSchema(SessionIdTableSchema sessionIdTableSchema)
735 if (sessionIdTableSchema == null)
736 throw new IllegalArgumentException("Null SessionIdTableSchema");
738 _sessionIdTableSchema = sessionIdTableSchema;
741 public SessionTableSchema getSessionTableSchema()
743 return _sessionTableSchema;
746 public void setSessionTableSchema(SessionTableSchema sessionTableSchema)
748 _sessionTableSchema = sessionTableSchema;
751 public void setDeleteBlockSize (int bsize)
753 this._deleteBlockSize = bsize;
756 public int getDeleteBlockSize ()
758 return this._deleteBlockSize;
761 public void setScavengeInterval (long sec)
766 long old_period=_scavengeIntervalMs;
767 long period=sec*1000L;
769 _scavengeIntervalMs=period;
771 //add a bit of variability into the scavenge time so that not all
772 //nodes with the same scavenge time sync up
773 long tenPercent = _scavengeIntervalMs/10;
774 if ((System.currentTimeMillis()%2) == 0)
775 _scavengeIntervalMs += tenPercent;
777 if (LOG.isDebugEnabled())
778 LOG.debug("Scavenging every "+_scavengeIntervalMs+" ms");
780 //if (_timer!=null && (period!=old_period || _task==null))
781 if (_scheduler != null && (period!=old_period || _task==null))
787 if (_scavenger == null)
788 _scavenger = new Scavenger();
789 _task = _scheduler.schedule(_scavenger,_scavengeIntervalMs,TimeUnit.MILLISECONDS);
794 public long getScavengeInterval ()
796 return _scavengeIntervalMs/1000;
801 public void addSession(HttpSession session)
806 synchronized (_sessionIds)
808 String id = ((JDBCSessionManager.Session)session).getClusterId();
816 LOG.warn("Problem storing session id="+id, e);
822 public void addSession(String id)
827 synchronized (_sessionIds)
836 LOG.warn("Problem storing session id="+id, e);
844 public void removeSession(HttpSession session)
849 removeSession(((JDBCSessionManager.Session)session).getClusterId());
854 public void removeSession (String id)
860 synchronized (_sessionIds)
862 if (LOG.isDebugEnabled())
863 LOG.debug("Removing sessionid="+id);
866 _sessionIds.remove(id);
871 LOG.warn("Problem removing session id="+id, e);
879 public boolean idInUse(String id)
884 String clusterId = getClusterId(id);
885 boolean inUse = false;
886 synchronized (_sessionIds)
888 inUse = _sessionIds.contains(clusterId);
893 return true; //optimisation - if this session is one we've been managing, we can check locally
895 //otherwise, we need to go to the database to check
898 return exists(clusterId);
902 LOG.warn("Problem checking inUse for id="+clusterId, e);
908 * Invalidate the session matching the id on all contexts.
910 * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
913 public void invalidateAll(String id)
915 //take the id out of the list of known sessionids for this node
918 synchronized (_sessionIds)
920 //tell all contexts that may have a session object with this id to
922 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
923 for (int i=0; contexts!=null && i<contexts.length; i++)
925 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
926 if (sessionHandler != null)
928 SessionManager manager = sessionHandler.getSessionManager();
930 if (manager != null && manager instanceof JDBCSessionManager)
932 ((JDBCSessionManager)manager).invalidateSession(id);
941 public void renewSessionId (String oldClusterId, String oldNodeId, HttpServletRequest request)
944 String newClusterId = newSessionId(request.hashCode());
946 synchronized (_sessionIds)
948 removeSession(oldClusterId);//remove the old one from the list (and database)
949 addSession(newClusterId); //add in the new session id to the list (and database)
951 //tell all contexts to update the id
952 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
953 for (int i=0; contexts!=null && i<contexts.length; i++)
955 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
956 if (sessionHandler != null)
958 SessionManager manager = sessionHandler.getSessionManager();
960 if (manager != null && manager instanceof JDBCSessionManager)
962 ((JDBCSessionManager)manager).renewSessionId(oldClusterId, oldNodeId, newClusterId, getNodeId(newClusterId, request));
971 * Start up the id manager.
973 * Makes necessary database tables and starts a Session
977 public void doStart()
980 initializeDatabase();
983 if (LOG.isDebugEnabled())
984 LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
986 //try and use a common scheduler, fallback to own
987 _scheduler =_server.getBean(Scheduler.class);
988 if (_scheduler == null)
990 _scheduler = new ScheduledExecutorScheduler();
991 _ownScheduler = true;
995 setScavengeInterval(getScavengeInterval());
999 * Stop the scavenger.
1002 public void doStop ()
1010 if (_ownScheduler && _scheduler !=null)
1014 _sessionIds.clear();
1019 * Get a connection from the driver or datasource.
1021 * @return the connection for the datasource
1022 * @throws SQLException
1024 protected Connection getConnection ()
1027 if (_datasource != null)
1028 return _datasource.getConnection();
1030 return DriverManager.getConnection(_connectionUrl);
1039 * Set up the tables in the database
1040 * @throws SQLException
1043 * @throws SQLException
1045 private void prepareTables()
1048 if (_sessionIdTableSchema == null)
1049 throw new IllegalStateException ("No SessionIdTableSchema");
1051 if (_sessionTableSchema == null)
1052 throw new IllegalStateException ("No SessionTableSchema");
1054 try (Connection connection = getConnection();
1055 Statement statement = connection.createStatement())
1058 connection.setAutoCommit(true);
1059 DatabaseMetaData metaData = connection.getMetaData();
1060 _dbAdaptor.adaptTo(metaData);
1061 _sessionTableSchema.setDatabaseAdaptor(_dbAdaptor);
1062 _sessionIdTableSchema.setDatabaseAdaptor(_dbAdaptor);
1064 _createSessionIdTable = _sessionIdTableSchema.getCreateStatementAsString();
1065 _insertId = _sessionIdTableSchema.getInsertStatementAsString();
1066 _deleteId = _sessionIdTableSchema.getDeleteStatementAsString();
1067 _queryId = _sessionIdTableSchema.getSelectStatementAsString();
1069 //checking for table existence is case-sensitive, but table creation is not
1070 String tableName = _dbAdaptor.convertIdentifier(_sessionIdTableSchema.getTableName());
1071 try (ResultSet result = metaData.getTables(null, null, tableName, null))
1075 //table does not exist, so create it
1076 statement.executeUpdate(_createSessionIdTable);
1080 //make the session table if necessary
1081 tableName = _dbAdaptor.convertIdentifier(_sessionTableSchema.getTableName());
1082 try (ResultSet result = metaData.getTables(null, null, tableName, null))
1086 //table does not exist, so create it
1087 _createSessionTable = _sessionTableSchema.getCreateStatementAsString();
1088 statement.executeUpdate(_createSessionTable);
1092 //session table exists, check it has maxinterval column
1093 ResultSet colResult = null;
1096 colResult = metaData.getColumns(null, null,
1097 _dbAdaptor.convertIdentifier(_sessionTableSchema.getTableName()),
1098 _dbAdaptor.convertIdentifier(_sessionTableSchema.getMaxIntervalColumn()));
1100 catch (SQLException s)
1102 LOG.warn("Problem checking if "+_sessionTableSchema.getTableName()+
1103 " table contains "+_sessionTableSchema.getMaxIntervalColumn()+" column. Ensure table contains column definition: \""
1104 +_sessionTableSchema.getMaxIntervalColumn()+" long not null default -999\"");
1109 if (!colResult.next())
1113 //add the maxinterval column
1114 statement.executeUpdate(_sessionTableSchema.getAlterTableForMaxIntervalAsString());
1116 catch (SQLException s)
1118 LOG.warn("Problem adding "+_sessionTableSchema.getMaxIntervalColumn()+
1119 " column. Ensure table contains column definition: \""+_sessionTableSchema.getMaxIntervalColumn()+
1120 " long not null default -999\"");
1131 //make some indexes on the JettySessions table
1132 String index1 = "idx_"+_sessionTableSchema.getTableName()+"_expiry";
1133 String index2 = "idx_"+_sessionTableSchema.getTableName()+"_session";
1135 boolean index1Exists = false;
1136 boolean index2Exists = false;
1137 try (ResultSet result = metaData.getIndexInfo(null, null, tableName, false, false))
1139 while (result.next())
1141 String idxName = result.getString("INDEX_NAME");
1142 if (index1.equalsIgnoreCase(idxName))
1143 index1Exists = true;
1144 else if (index2.equalsIgnoreCase(idxName))
1145 index2Exists = true;
1149 statement.executeUpdate(_sessionTableSchema.getCreateIndexOverExpiryStatementAsString(index1));
1151 statement.executeUpdate(_sessionTableSchema.getCreateIndexOverSessionStatementAsString(index2));
1153 //set up some strings representing the statements for session manipulation
1154 _insertSession = _sessionTableSchema.getInsertSessionStatementAsString();
1155 _deleteSession = _sessionTableSchema.getDeleteSessionStatementAsString();
1156 _updateSession = _sessionTableSchema.getUpdateSessionStatementAsString();
1157 _updateSessionNode = _sessionTableSchema.getUpdateSessionNodeStatementAsString();
1158 _updateSessionAccessTime = _sessionTableSchema.getUpdateSessionAccessTimeStatementAsString();
1159 _selectBoundedExpiredSessions = _sessionTableSchema.getBoundedExpiredSessionsStatementAsString();
1160 _selectExpiredSessions = _sessionTableSchema.getSelectExpiredSessionsStatementAsString();
1165 * Insert a new used session id into the table.
1168 * @throws SQLException
1170 private void insert (String id)
1173 try (Connection connection = getConnection();
1174 PreparedStatement query = connection.prepareStatement(_queryId))
1176 connection.setAutoCommit(true);
1177 query.setString(1, id);
1178 try (ResultSet result = query.executeQuery())
1180 //only insert the id if it isn't in the db already
1183 try (PreparedStatement statement = connection.prepareStatement(_insertId))
1185 statement.setString(1, id);
1186 statement.executeUpdate();
1194 * Remove a session id from the table.
1197 * @throws SQLException
1199 private void delete (String id)
1202 try (Connection connection = getConnection();
1203 PreparedStatement statement = connection.prepareStatement(_deleteId))
1205 connection.setAutoCommit(true);
1206 statement.setString(1, id);
1207 statement.executeUpdate();
1213 * Check if a session id exists.
1217 * @throws SQLException
1219 private boolean exists (String id)
1222 try (Connection connection = getConnection();
1223 PreparedStatement statement = connection.prepareStatement(_queryId))
1225 connection.setAutoCommit(true);
1226 statement.setString(1, id);
1227 try (ResultSet result = statement.executeQuery())
1229 return result.next();
1235 * Look for sessions in the database that have expired.
1237 * We do this in the SessionIdManager and not the SessionManager so
1238 * that we only have 1 scavenger, otherwise if there are n SessionManagers
1239 * there would be n scavengers, all contending for the database.
1241 * We look first for sessions that expired in the previous interval, then
1242 * for sessions that expired previously - these are old sessions that no
1243 * node is managing any more and have become stuck in the database.
1245 private void scavenge ()
1247 Connection connection = null;
1250 if (LOG.isDebugEnabled())
1251 LOG.debug(getWorkerName()+"- Scavenge sweep started at "+System.currentTimeMillis());
1252 if (_lastScavengeTime > 0)
1254 connection = getConnection();
1255 connection.setAutoCommit(true);
1256 Set<String> expiredSessionIds = new HashSet<String>();
1259 //Pass 1: find sessions for which we were last managing node that have just expired since last pass
1260 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
1261 long upperBound = _lastScavengeTime;
1262 if (LOG.isDebugEnabled())
1263 LOG.debug (getWorkerName()+"- Pass 1: Searching for sessions expired between "+lowerBound + " and "+upperBound);
1265 try (PreparedStatement statement = connection.prepareStatement(_selectBoundedExpiredSessions))
1267 statement.setString(1, getWorkerName());
1268 statement.setLong(2, lowerBound);
1269 statement.setLong(3, upperBound);
1270 try (ResultSet result = statement.executeQuery())
1272 while (result.next())
1274 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1275 expiredSessionIds.add(sessionId);
1276 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId);
1280 scavengeSessions(expiredSessionIds, false);
1283 //Pass 2: find sessions that have expired a while ago for which this node was their last manager
1284 try (PreparedStatement selectExpiredSessions = connection.prepareStatement(_selectExpiredSessions))
1286 expiredSessionIds.clear();
1287 upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
1290 if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 2: Searching for sessions expired before "+upperBound);
1291 selectExpiredSessions.setLong(1, upperBound);
1292 try (ResultSet result = selectExpiredSessions.executeQuery())
1294 while (result.next())
1296 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1297 String lastNode = result.getString(_sessionTableSchema.getLastNodeColumn());
1298 if ((getWorkerName() == null && lastNode == null) || (getWorkerName() != null && getWorkerName().equals(lastNode)))
1299 expiredSessionIds.add(sessionId);
1300 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId+" last managed by "+getWorkerName());
1303 scavengeSessions(expiredSessionIds, false);
1308 //find all sessions that have expired at least a couple of scanIntervals ago
1309 //if we did not succeed in loading them (eg their related context no longer exists, can't be loaded etc) then
1310 //they are simply deleted
1311 upperBound = _lastScavengeTime - (3 * _scavengeIntervalMs);
1312 expiredSessionIds.clear();
1315 if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 3: searching for sessions expired before "+upperBound);
1316 selectExpiredSessions.setLong(1, upperBound);
1317 try (ResultSet result = selectExpiredSessions.executeQuery())
1319 while (result.next())
1321 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1322 expiredSessionIds.add(sessionId);
1323 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId);
1326 scavengeSessions(expiredSessionIds, true);
1334 LOG.warn("Problem selecting expired sessions", e);
1340 _lastScavengeTime=System.currentTimeMillis();
1341 if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Scavenge sweep ended at "+_lastScavengeTime);
1342 if (connection != null)
1348 catch (SQLException e)
1358 * @param expiredSessionIds
1360 private void scavengeSessions (Set<String> expiredSessionIds, boolean forceDelete)
1362 Set<String> remainingIds = new HashSet<String>(expiredSessionIds);
1363 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
1364 for (int i=0; contexts!=null && i<contexts.length; i++)
1366 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
1367 if (sessionHandler != null)
1369 SessionManager manager = sessionHandler.getSessionManager();
1370 if (manager != null && manager instanceof JDBCSessionManager)
1372 Set<String> successfullyExpiredIds = ((JDBCSessionManager)manager).expire(expiredSessionIds);
1373 if (successfullyExpiredIds != null)
1374 remainingIds.removeAll(successfullyExpiredIds);
1379 //Any remaining ids are of those sessions that no context removed
1380 if (!remainingIds.isEmpty() && forceDelete)
1382 LOG.info("Forcibly deleting unrecoverable expired sessions {}", remainingIds);
1385 //ensure they aren't in the local list of in-use session ids
1386 synchronized (_sessionIds)
1388 _sessionIds.removeAll(remainingIds);
1391 cleanExpiredSessionIds(remainingIds);
1395 LOG.warn("Error removing expired session ids", e);
1403 private void cleanExpiredSessionIds (Set<String> expiredIds)
1406 if (expiredIds == null || expiredIds.isEmpty())
1409 String[] ids = expiredIds.toArray(new String[expiredIds.size()]);
1410 try (Connection con = getConnection())
1412 con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
1413 con.setAutoCommit(false);
1417 int blocksize = _deleteBlockSize;
1420 try (Statement statement = con.createStatement())
1422 while (end < ids.length)
1424 start = block*blocksize;
1425 if ((ids.length - start) >= blocksize)
1426 end = start + blocksize;
1430 //take them out of the sessionIds table
1431 statement.executeUpdate(fillInClause("delete from "+_sessionIdTableSchema.getTableName()+" where "+_sessionIdTableSchema.getIdColumn()+" in ", ids, start, end));
1432 //take them out of the sessions table
1433 statement.executeUpdate(fillInClause("delete from "+_sessionTableSchema.getTableName()+" where "+_sessionTableSchema.getIdColumn()+" in ", ids, start, end));
1454 private String fillInClause (String sql, String[] literals, int start, int end)
1457 StringBuffer buff = new StringBuffer();
1460 for (int i=start; i<end; i++)
1462 buff.append("'"+(literals[i])+"'");
1467 return buff.toString();
1472 private void initializeDatabase ()
1475 if (_datasource != null)
1476 return; //already set up
1478 if (_jndiName!=null)
1480 InitialContext ic = new InitialContext();
1481 _datasource = (DataSource)ic.lookup(_jndiName);
1483 else if ( _driver != null && _connectionUrl != null )
1485 DriverManager.registerDriver(_driver);
1487 else if (_driverClassName != null && _connectionUrl != null)
1489 Class.forName(_driverClassName);
1492 throw new IllegalStateException("No database configured for sessions");