2 // ========================================================================
3 // Copyright (c) 1995-2016 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 String stem = "alter table "+getTableName()+" add "+getMaxIntervalColumn()+" "+longType;
301 if (_dbAdaptor.getDBName().contains("oracle"))
302 return stem + " default "+ MAX_INTERVAL_NOT_SET + " not null";
304 return stem +" not null default "+ MAX_INTERVAL_NOT_SET;
307 private void checkNotNull(String s)
310 throw new IllegalArgumentException(s);
312 public String getInsertSessionStatementAsString()
314 return "insert into "+getTableName()+
315 " ("+getRowIdColumn()+", "+getIdColumn()+", "+getContextPathColumn()+", "+getVirtualHostColumn()+", "+getLastNodeColumn()+
316 ", "+getAccessTimeColumn()+", "+getLastAccessTimeColumn()+", "+getCreateTimeColumn()+", "+getCookieTimeColumn()+
317 ", "+getLastSavedTimeColumn()+", "+getExpiryTimeColumn()+", "+getMaxIntervalColumn()+", "+getMapColumn()+") "+
318 " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
320 public String getDeleteSessionStatementAsString()
322 return "delete from "+getTableName()+
323 " where "+getRowIdColumn()+" = ?";
325 public String getUpdateSessionStatementAsString()
327 return "update "+getTableName()+
328 " set "+getIdColumn()+" = ?, "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+
329 getLastAccessTimeColumn()+" = ?, "+getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+
330 getMaxIntervalColumn()+" = ?, "+getMapColumn()+" = ? where "+getRowIdColumn()+" = ?";
332 public String getUpdateSessionNodeStatementAsString()
334 return "update "+getTableName()+
335 " set "+getLastNodeColumn()+" = ? where "+getRowIdColumn()+" = ?";
337 public String getUpdateSessionAccessTimeStatementAsString()
339 return "update "+getTableName()+
340 " set "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+getLastAccessTimeColumn()+" = ?, "+
341 getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+getMaxIntervalColumn()+" = ? where "+getRowIdColumn()+" = ?";
344 public String getBoundedExpiredSessionsStatementAsString()
346 return "select * from "+getTableName()+" where "+getLastNodeColumn()+" = ? and "+getExpiryTimeColumn()+" >= ? and "+getExpiryTimeColumn()+" <= ?";
349 public String getSelectExpiredSessionsStatementAsString()
351 return "select * from "+getTableName()+" where "+getExpiryTimeColumn()+" >0 and "+getExpiryTimeColumn()+" <= ?";
354 public PreparedStatement getLoadStatement (Connection connection, String rowId, String contextPath, String virtualHosts)
357 if (_dbAdaptor == null)
358 throw new IllegalStateException("No DB adaptor");
361 if (contextPath == null || "".equals(contextPath))
363 if (_dbAdaptor.isEmptyStringNull())
365 PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+
366 " where "+getIdColumn()+" = ? and "+
367 getContextPathColumn()+" is null and "+
368 getVirtualHostColumn()+" = ?");
369 statement.setString(1, rowId);
370 statement.setString(2, virtualHosts);
376 PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+
377 " where "+getIdColumn()+" = ? and "+getContextPathColumn()+
378 " = ? and "+getVirtualHostColumn()+" = ?");
379 statement.setString(1, rowId);
380 statement.setString(2, contextPath);
381 statement.setString(3, virtualHosts);
390 * SessionIdTableSchema
393 public static class SessionIdTableSchema
395 protected DatabaseAdaptor _dbAdaptor;
396 protected String _tableName = "JettySessionIds";
397 protected String _idColumn = "id";
399 public void setDatabaseAdaptor(DatabaseAdaptor dbAdaptor)
401 _dbAdaptor = dbAdaptor;
403 public String getIdColumn()
408 public void setIdColumn(String idColumn)
410 checkNotNull(idColumn);
411 _idColumn = idColumn;
414 public String getTableName()
419 public void setTableName(String tableName)
421 checkNotNull(tableName);
422 _tableName = tableName;
425 public String getInsertStatementAsString ()
427 return "insert into "+_tableName+" ("+_idColumn+") values (?)";
430 public String getDeleteStatementAsString ()
432 return "delete from "+_tableName+" where "+_idColumn+" = ?";
435 public String getSelectStatementAsString ()
437 return "select * from "+_tableName+" where "+_idColumn+" = ?";
440 public String getCreateStatementAsString ()
442 return "create table "+_tableName+" ("+_idColumn+" varchar(120), primary key("+_idColumn+"))";
445 private void checkNotNull(String s)
448 throw new IllegalArgumentException(s);
456 * Handles differences between databases.
458 * Postgres uses the getBytes and setBinaryStream methods to access
459 * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
460 * is happy to use the "blob" type and getBlob() methods instead.
462 * TODO if the differences become more major it would be worthwhile
463 * refactoring this class.
465 public static class DatabaseAdaptor
471 protected String _blobType; //if not set, is deduced from the type of the database at runtime
472 protected String _longType; //if not set, is deduced from the type of the database at runtime
475 public DatabaseAdaptor ()
480 public void adaptTo(DatabaseMetaData dbMeta)
483 _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH);
484 if (LOG.isDebugEnabled())
485 LOG.debug ("Using database {}",_dbName);
486 _isLower = dbMeta.storesLowerCaseIdentifiers();
487 _isUpper = dbMeta.storesUpperCaseIdentifiers();
491 public void setBlobType(String blobType)
493 _blobType = blobType;
496 public String getBlobType ()
498 if (_blobType != null)
501 if (_dbName.startsWith("postgres"))
508 public void setLongType(String longType)
510 _longType = longType;
514 public String getLongType ()
516 if (_longType != null)
520 throw new IllegalStateException ("DbAdaptor missing metadata");
522 if (_dbName.startsWith("oracle"))
530 * Convert a camel case identifier into either upper or lower
531 * depending on the way the db stores identifiers.
534 * @return the converted identifier
536 public String convertIdentifier (String identifier)
539 throw new IllegalStateException ("DbAdaptor missing metadata");
542 return identifier.toLowerCase(Locale.ENGLISH);
544 return identifier.toUpperCase(Locale.ENGLISH);
549 public String getDBName ()
555 public InputStream getBlobInputStream (ResultSet result, String columnName)
559 throw new IllegalStateException ("DbAdaptor missing metadata");
561 if (_dbName.startsWith("postgres"))
563 byte[] bytes = result.getBytes(columnName);
564 return new ByteArrayInputStream(bytes);
567 Blob blob = result.getBlob(columnName);
568 return blob.getBinaryStream();
572 public boolean isEmptyStringNull ()
575 throw new IllegalStateException ("DbAdaptor missing metadata");
577 return (_dbName.startsWith("oracle"));
581 * rowId is a reserved word for Oracle, so change the name of this column
582 * @return true if db in use is oracle
584 public boolean isRowIdReserved ()
587 throw new IllegalStateException ("DbAdaptor missing metadata");
589 return (_dbName != null && _dbName.startsWith("oracle"));
598 protected class Scavenger implements Runnable
610 if (_scheduler != null && _scheduler.isRunning())
611 _task = _scheduler.schedule(this, _scavengeIntervalMs, TimeUnit.MILLISECONDS);
617 public JDBCSessionIdManager(Server server)
623 public JDBCSessionIdManager(Server server, Random random)
630 * Configure jdbc connection information via a jdbc Driver
632 * @param driverClassName
633 * @param connectionUrl
635 public void setDriverInfo (String driverClassName, String connectionUrl)
637 _driverClassName=driverClassName;
638 _connectionUrl=connectionUrl;
642 * Configure jdbc connection information via a jdbc Driver
645 * @param connectionUrl
647 public void setDriverInfo (Driver driverClass, String connectionUrl)
650 _connectionUrl=connectionUrl;
654 public void setDatasource (DataSource ds)
659 public DataSource getDataSource ()
664 public String getDriverClassName()
666 return _driverClassName;
669 public String getConnectionUrl ()
671 return _connectionUrl;
674 public void setDatasourceName (String jndi)
679 public String getDatasourceName ()
686 * @deprecated see DbAdaptor.setBlobType
688 public void setBlobType (String name)
690 _dbAdaptor.setBlobType(name);
693 public DatabaseAdaptor getDbAdaptor()
698 public void setDbAdaptor(DatabaseAdaptor dbAdaptor)
700 if (dbAdaptor == null)
701 throw new IllegalStateException ("DbAdaptor cannot be null");
703 _dbAdaptor = dbAdaptor;
708 * @deprecated see DbAdaptor.getBlobType
710 public String getBlobType ()
712 return _dbAdaptor.getBlobType();
717 * @deprecated see DbAdaptor.getLogType
719 public String getLongType()
721 return _dbAdaptor.getLongType();
726 * @deprecated see DbAdaptor.setLongType
728 public void setLongType(String longType)
730 _dbAdaptor.setLongType(longType);
733 public SessionIdTableSchema getSessionIdTableSchema()
735 return _sessionIdTableSchema;
738 public void setSessionIdTableSchema(SessionIdTableSchema sessionIdTableSchema)
740 if (sessionIdTableSchema == null)
741 throw new IllegalArgumentException("Null SessionIdTableSchema");
743 _sessionIdTableSchema = sessionIdTableSchema;
746 public SessionTableSchema getSessionTableSchema()
748 return _sessionTableSchema;
751 public void setSessionTableSchema(SessionTableSchema sessionTableSchema)
753 _sessionTableSchema = sessionTableSchema;
756 public void setDeleteBlockSize (int bsize)
758 this._deleteBlockSize = bsize;
761 public int getDeleteBlockSize ()
763 return this._deleteBlockSize;
766 public void setScavengeInterval (long sec)
771 long old_period=_scavengeIntervalMs;
772 long period=sec*1000L;
774 _scavengeIntervalMs=period;
776 //add a bit of variability into the scavenge time so that not all
777 //nodes with the same scavenge time sync up
778 long tenPercent = _scavengeIntervalMs/10;
779 if ((System.currentTimeMillis()%2) == 0)
780 _scavengeIntervalMs += tenPercent;
782 if (LOG.isDebugEnabled())
783 LOG.debug("Scavenging every "+_scavengeIntervalMs+" ms");
787 //if (_timer!=null && (period!=old_period || _task==null))
788 if (_scheduler != null && (period!=old_period || _task==null))
792 if (_scavenger == null)
793 _scavenger = new Scavenger();
794 _task = _scheduler.schedule(_scavenger,_scavengeIntervalMs,TimeUnit.MILLISECONDS);
799 public long getScavengeInterval ()
801 return _scavengeIntervalMs/1000;
806 public void addSession(HttpSession session)
811 synchronized (_sessionIds)
813 String id = ((JDBCSessionManager.Session)session).getClusterId();
821 LOG.warn("Problem storing session id="+id, e);
827 public void addSession(String id)
832 synchronized (_sessionIds)
841 LOG.warn("Problem storing session id="+id, e);
849 public void removeSession(HttpSession session)
854 removeSession(((JDBCSessionManager.Session)session).getClusterId());
859 public void removeSession (String id)
865 synchronized (_sessionIds)
867 if (LOG.isDebugEnabled())
868 LOG.debug("Removing sessionid="+id);
871 _sessionIds.remove(id);
876 LOG.warn("Problem removing session id="+id, e);
884 public boolean idInUse(String id)
889 String clusterId = getClusterId(id);
890 boolean inUse = false;
891 synchronized (_sessionIds)
893 inUse = _sessionIds.contains(clusterId);
898 return true; //optimisation - if this session is one we've been managing, we can check locally
900 //otherwise, we need to go to the database to check
903 return exists(clusterId);
907 LOG.warn("Problem checking inUse for id="+clusterId, e);
913 * Invalidate the session matching the id on all contexts.
915 * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
918 public void invalidateAll(String id)
920 //take the id out of the list of known sessionids for this node
923 synchronized (_sessionIds)
925 //tell all contexts that may have a session object with this id to
927 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
928 for (int i=0; contexts!=null && i<contexts.length; i++)
930 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
931 if (sessionHandler != null)
933 SessionManager manager = sessionHandler.getSessionManager();
935 if (manager != null && manager instanceof JDBCSessionManager)
937 ((JDBCSessionManager)manager).invalidateSession(id);
946 public void renewSessionId (String oldClusterId, String oldNodeId, HttpServletRequest request)
949 String newClusterId = newSessionId(request.hashCode());
951 synchronized (_sessionIds)
953 removeSession(oldClusterId);//remove the old one from the list (and database)
954 addSession(newClusterId); //add in the new session id to the list (and database)
956 //tell all contexts to update the id
957 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
958 for (int i=0; contexts!=null && i<contexts.length; i++)
960 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
961 if (sessionHandler != null)
963 SessionManager manager = sessionHandler.getSessionManager();
965 if (manager != null && manager instanceof JDBCSessionManager)
967 ((JDBCSessionManager)manager).renewSessionId(oldClusterId, oldNodeId, newClusterId, getNodeId(newClusterId, request));
976 * Start up the id manager.
978 * Makes necessary database tables and starts a Session
982 public void doStart()
985 initializeDatabase();
988 if (LOG.isDebugEnabled())
989 LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
991 //try and use a common scheduler, fallback to own
992 _scheduler =_server.getBean(Scheduler.class);
993 if (_scheduler == null)
995 _scheduler = new ScheduledExecutorScheduler();
996 _ownScheduler = true;
999 else if (!_scheduler.isStarted())
1000 throw new IllegalStateException("Shared scheduler not started");
1002 setScavengeInterval(getScavengeInterval());
1006 * Stop the scavenger.
1009 public void doStop ()
1017 if (_ownScheduler && _scheduler !=null)
1021 _sessionIds.clear();
1026 * Get a connection from the driver or datasource.
1028 * @return the connection for the datasource
1029 * @throws SQLException
1031 protected Connection getConnection ()
1034 if (_datasource != null)
1035 return _datasource.getConnection();
1037 return DriverManager.getConnection(_connectionUrl);
1046 * Set up the tables in the database
1047 * @throws SQLException
1050 * @throws SQLException
1052 private void prepareTables()
1055 if (_sessionIdTableSchema == null)
1056 throw new IllegalStateException ("No SessionIdTableSchema");
1058 if (_sessionTableSchema == null)
1059 throw new IllegalStateException ("No SessionTableSchema");
1061 try (Connection connection = getConnection();
1062 Statement statement = connection.createStatement())
1065 connection.setAutoCommit(true);
1066 DatabaseMetaData metaData = connection.getMetaData();
1067 _dbAdaptor.adaptTo(metaData);
1068 _sessionTableSchema.setDatabaseAdaptor(_dbAdaptor);
1069 _sessionIdTableSchema.setDatabaseAdaptor(_dbAdaptor);
1071 _createSessionIdTable = _sessionIdTableSchema.getCreateStatementAsString();
1072 _insertId = _sessionIdTableSchema.getInsertStatementAsString();
1073 _deleteId = _sessionIdTableSchema.getDeleteStatementAsString();
1074 _queryId = _sessionIdTableSchema.getSelectStatementAsString();
1076 //checking for table existence is case-sensitive, but table creation is not
1077 String tableName = _dbAdaptor.convertIdentifier(_sessionIdTableSchema.getTableName());
1078 try (ResultSet result = metaData.getTables(null, null, tableName, null))
1082 //table does not exist, so create it
1083 statement.executeUpdate(_createSessionIdTable);
1087 //make the session table if necessary
1088 tableName = _dbAdaptor.convertIdentifier(_sessionTableSchema.getTableName());
1089 try (ResultSet result = metaData.getTables(null, null, tableName, null))
1093 //table does not exist, so create it
1094 _createSessionTable = _sessionTableSchema.getCreateStatementAsString();
1095 statement.executeUpdate(_createSessionTable);
1099 //session table exists, check it has maxinterval column
1100 ResultSet colResult = null;
1103 colResult = metaData.getColumns(null, null,
1104 _dbAdaptor.convertIdentifier(_sessionTableSchema.getTableName()),
1105 _dbAdaptor.convertIdentifier(_sessionTableSchema.getMaxIntervalColumn()));
1107 catch (SQLException s)
1109 LOG.warn("Problem checking if "+_sessionTableSchema.getTableName()+
1110 " table contains "+_sessionTableSchema.getMaxIntervalColumn()+" column. Ensure table contains column definition: \""
1111 +_sessionTableSchema.getMaxIntervalColumn()+" long not null default -999\"");
1116 if (!colResult.next())
1120 //add the maxinterval column
1121 statement.executeUpdate(_sessionTableSchema.getAlterTableForMaxIntervalAsString());
1123 catch (SQLException s)
1125 LOG.warn("Problem adding "+_sessionTableSchema.getMaxIntervalColumn()+
1126 " column. Ensure table contains column definition: \""+_sessionTableSchema.getMaxIntervalColumn()+
1127 " long not null default -999\"");
1138 //make some indexes on the JettySessions table
1139 String index1 = "idx_"+_sessionTableSchema.getTableName()+"_expiry";
1140 String index2 = "idx_"+_sessionTableSchema.getTableName()+"_session";
1142 boolean index1Exists = false;
1143 boolean index2Exists = false;
1144 try (ResultSet result = metaData.getIndexInfo(null, null, tableName, false, false))
1146 while (result.next())
1148 String idxName = result.getString("INDEX_NAME");
1149 if (index1.equalsIgnoreCase(idxName))
1150 index1Exists = true;
1151 else if (index2.equalsIgnoreCase(idxName))
1152 index2Exists = true;
1156 statement.executeUpdate(_sessionTableSchema.getCreateIndexOverExpiryStatementAsString(index1));
1158 statement.executeUpdate(_sessionTableSchema.getCreateIndexOverSessionStatementAsString(index2));
1160 //set up some strings representing the statements for session manipulation
1161 _insertSession = _sessionTableSchema.getInsertSessionStatementAsString();
1162 _deleteSession = _sessionTableSchema.getDeleteSessionStatementAsString();
1163 _updateSession = _sessionTableSchema.getUpdateSessionStatementAsString();
1164 _updateSessionNode = _sessionTableSchema.getUpdateSessionNodeStatementAsString();
1165 _updateSessionAccessTime = _sessionTableSchema.getUpdateSessionAccessTimeStatementAsString();
1166 _selectBoundedExpiredSessions = _sessionTableSchema.getBoundedExpiredSessionsStatementAsString();
1167 _selectExpiredSessions = _sessionTableSchema.getSelectExpiredSessionsStatementAsString();
1172 * Insert a new used session id into the table.
1175 * @throws SQLException
1177 private void insert (String id)
1180 try (Connection connection = getConnection();
1181 PreparedStatement query = connection.prepareStatement(_queryId))
1183 connection.setAutoCommit(true);
1184 query.setString(1, id);
1185 try (ResultSet result = query.executeQuery())
1187 //only insert the id if it isn't in the db already
1190 try (PreparedStatement statement = connection.prepareStatement(_insertId))
1192 statement.setString(1, id);
1193 statement.executeUpdate();
1201 * Remove a session id from the table.
1204 * @throws SQLException
1206 private void delete (String id)
1209 try (Connection connection = getConnection();
1210 PreparedStatement statement = connection.prepareStatement(_deleteId))
1212 connection.setAutoCommit(true);
1213 statement.setString(1, id);
1214 statement.executeUpdate();
1220 * Check if a session id exists.
1224 * @throws SQLException
1226 private boolean exists (String id)
1229 try (Connection connection = getConnection();
1230 PreparedStatement statement = connection.prepareStatement(_queryId))
1232 connection.setAutoCommit(true);
1233 statement.setString(1, id);
1234 try (ResultSet result = statement.executeQuery())
1236 return result.next();
1242 * Look for sessions in the database that have expired.
1244 * We do this in the SessionIdManager and not the SessionManager so
1245 * that we only have 1 scavenger, otherwise if there are n SessionManagers
1246 * there would be n scavengers, all contending for the database.
1248 * We look first for sessions that expired in the previous interval, then
1249 * for sessions that expired previously - these are old sessions that no
1250 * node is managing any more and have become stuck in the database.
1252 private void scavenge ()
1254 Connection connection = null;
1257 if (LOG.isDebugEnabled())
1258 LOG.debug(getWorkerName()+"- Scavenge sweep started at "+System.currentTimeMillis());
1259 if (_lastScavengeTime > 0)
1261 connection = getConnection();
1262 connection.setAutoCommit(true);
1263 Set<String> expiredSessionIds = new HashSet<String>();
1266 //Pass 1: find sessions for which we were last managing node that have just expired since last pass
1267 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
1268 long upperBound = _lastScavengeTime;
1269 if (LOG.isDebugEnabled())
1270 LOG.debug (getWorkerName()+"- Pass 1: Searching for sessions expired between "+lowerBound + " and "+upperBound);
1272 try (PreparedStatement statement = connection.prepareStatement(_selectBoundedExpiredSessions))
1274 statement.setString(1, getWorkerName());
1275 statement.setLong(2, lowerBound);
1276 statement.setLong(3, upperBound);
1277 try (ResultSet result = statement.executeQuery())
1279 while (result.next())
1281 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1282 expiredSessionIds.add(sessionId);
1283 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId);
1287 scavengeSessions(expiredSessionIds, false);
1290 //Pass 2: find sessions that have expired a while ago for which this node was their last manager
1291 try (PreparedStatement selectExpiredSessions = connection.prepareStatement(_selectExpiredSessions))
1293 expiredSessionIds.clear();
1294 upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
1297 if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 2: Searching for sessions expired before "+upperBound);
1298 selectExpiredSessions.setLong(1, upperBound);
1299 try (ResultSet result = selectExpiredSessions.executeQuery())
1301 while (result.next())
1303 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1304 String lastNode = result.getString(_sessionTableSchema.getLastNodeColumn());
1305 if ((getWorkerName() == null && lastNode == null) || (getWorkerName() != null && getWorkerName().equals(lastNode)))
1306 expiredSessionIds.add(sessionId);
1307 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId+" last managed by "+getWorkerName());
1310 scavengeSessions(expiredSessionIds, false);
1315 //find all sessions that have expired at least a couple of scanIntervals ago
1316 //if we did not succeed in loading them (eg their related context no longer exists, can't be loaded etc) then
1317 //they are simply deleted
1318 upperBound = _lastScavengeTime - (3 * _scavengeIntervalMs);
1319 expiredSessionIds.clear();
1322 if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 3: searching for sessions expired before "+upperBound);
1323 selectExpiredSessions.setLong(1, upperBound);
1324 try (ResultSet result = selectExpiredSessions.executeQuery())
1326 while (result.next())
1328 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1329 expiredSessionIds.add(sessionId);
1330 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId);
1333 scavengeSessions(expiredSessionIds, true);
1341 LOG.warn("Problem selecting expired sessions", e);
1347 _lastScavengeTime=System.currentTimeMillis();
1348 if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Scavenge sweep ended at "+_lastScavengeTime);
1349 if (connection != null)
1355 catch (SQLException e)
1365 * @param expiredSessionIds
1367 private void scavengeSessions (Set<String> expiredSessionIds, boolean forceDelete)
1369 Set<String> remainingIds = new HashSet<String>(expiredSessionIds);
1370 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
1371 for (int i=0; contexts!=null && i<contexts.length; i++)
1373 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
1374 if (sessionHandler != null)
1376 SessionManager manager = sessionHandler.getSessionManager();
1377 if (manager != null && manager instanceof JDBCSessionManager)
1379 Set<String> successfullyExpiredIds = ((JDBCSessionManager)manager).expire(expiredSessionIds);
1380 if (successfullyExpiredIds != null)
1381 remainingIds.removeAll(successfullyExpiredIds);
1386 //Any remaining ids are of those sessions that no context removed
1387 if (!remainingIds.isEmpty() && forceDelete)
1389 LOG.info("Forcibly deleting unrecoverable expired sessions {}", remainingIds);
1392 //ensure they aren't in the local list of in-use session ids
1393 synchronized (_sessionIds)
1395 _sessionIds.removeAll(remainingIds);
1398 cleanExpiredSessionIds(remainingIds);
1402 LOG.warn("Error removing expired session ids", e);
1410 private void cleanExpiredSessionIds (Set<String> expiredIds)
1413 if (expiredIds == null || expiredIds.isEmpty())
1416 String[] ids = expiredIds.toArray(new String[expiredIds.size()]);
1417 try (Connection con = getConnection())
1419 con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
1420 con.setAutoCommit(false);
1424 int blocksize = _deleteBlockSize;
1427 try (Statement statement = con.createStatement())
1429 while (end < ids.length)
1431 start = block*blocksize;
1432 if ((ids.length - start) >= blocksize)
1433 end = start + blocksize;
1437 //take them out of the sessionIds table
1438 statement.executeUpdate(fillInClause("delete from "+_sessionIdTableSchema.getTableName()+" where "+_sessionIdTableSchema.getIdColumn()+" in ", ids, start, end));
1439 //take them out of the sessions table
1440 statement.executeUpdate(fillInClause("delete from "+_sessionTableSchema.getTableName()+" where "+_sessionTableSchema.getIdColumn()+" in ", ids, start, end));
1461 private String fillInClause (String sql, String[] literals, int start, int end)
1464 StringBuffer buff = new StringBuffer();
1467 for (int i=start; i<end; i++)
1469 buff.append("'"+(literals[i])+"'");
1474 return buff.toString();
1479 private void initializeDatabase ()
1482 if (_datasource != null)
1483 return; //already set up
1485 if (_jndiName!=null)
1487 InitialContext ic = new InitialContext();
1488 _datasource = (DataSource)ic.lookup(_jndiName);
1490 else if ( _driver != null && _connectionUrl != null )
1492 DriverManager.registerDriver(_driver);
1494 else if (_driverClassName != null && _connectionUrl != null)
1496 Class.forName(_driverClassName);
1499 throw new IllegalStateException("No database configured for sessions");