1 //
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.
8 //
9 //      The Eclipse Public License is available at
10 //      http://www.eclipse.org/legal/epl-v10.html
11 //
12 //      The Apache License v2.0 is available at
13 //      http://www.opensource.org/licenses/apache2.0.php
14 //
15 //  You may elect to redistribute this code under either of these licenses.
16 //  ========================================================================
17 //
18 
19 package org.eclipse.jetty.server.session;
20 
21 import java.io.ByteArrayInputStream;
22 import java.io.InputStream;
23 import java.sql.Blob;
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.ArrayList;
33 import java.util.Collection;
34 import java.util.HashSet;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Random;
39 import java.util.Timer;
40 import java.util.TimerTask;
41 
42 import javax.naming.InitialContext;
43 import javax.servlet.http.HttpServletRequest;
44 import javax.servlet.http.HttpSession;
45 import javax.sql.DataSource;
46 
47 import org.eclipse.jetty.server.Handler;
48 import org.eclipse.jetty.server.Server;
49 import org.eclipse.jetty.server.SessionManager;
50 import org.eclipse.jetty.server.handler.ContextHandler;
51 import org.eclipse.jetty.util.log.Logger;
52 
53 
54 
55 /**
56  * JDBCSessionIdManager
57  *
58  * SessionIdManager implementation that uses a database to store in-use session ids,
59  * to support distributed sessions.
60  *
61  */
62 public class JDBCSessionIdManager extends AbstractSessionIdManager
63 {
64     final static Logger LOG = SessionHandler.LOG;
65 
66     protected final HashSet<String> _sessionIds = new HashSet<String>();
67     protected Server _server;
68     protected Driver _driver;
69     protected String _driverClassName;
70     protected String _connectionUrl;
71     protected DataSource _datasource;
72     protected String _jndiName;
73     protected String _sessionIdTable = "JettySessionIds";
74     protected String _sessionTable = "JettySessions";
75     protected String _sessionTableRowId = "rowId";
76 
77     protected Timer _timer; //scavenge timer
78     protected TimerTask _task; //scavenge task
79     protected long _lastScavengeTime;
80     protected long _scavengeIntervalMs = 1000L * 60 * 10; //10mins
81     protected String _blobType; //if not set, is deduced from the type of the database at runtime
82     protected String _longType; //if not set, is deduced from the type of the database at runtime
83 
84     protected String _createSessionIdTable;
85     protected String _createSessionTable;
86 
87     protected String _selectBoundedExpiredSessions;
88     protected String _deleteOldExpiredSessions;
89 
90     protected String _insertId;
91     protected String _deleteId;
92     protected String _queryId;
93 
94     protected  String _insertSession;
95     protected  String _deleteSession;
96     protected  String _updateSession;
97     protected  String _updateSessionNode;
98     protected  String _updateSessionAccessTime;
99 
100     protected DatabaseAdaptor _dbAdaptor;
101 
102     private String _selectExpiredSessions;
103 
104 
105     /**
106      * DatabaseAdaptor
107      *
108      * Handles differences between databases.
109      *
110      * Postgres uses the getBytes and setBinaryStream methods to access
111      * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
112      * is happy to use the "blob" type and getBlob() methods instead.
113      *
114      * TODO if the differences become more major it would be worthwhile
115      * refactoring this class.
116      */
117     public class DatabaseAdaptor
118     {
119         String _dbName;
120         boolean _isLower;
121         boolean _isUpper;
122 
123 
124 
DatabaseAdaptor(DatabaseMetaData dbMeta)125         public DatabaseAdaptor (DatabaseMetaData dbMeta)
126         throws SQLException
127         {
128             _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH);
129             LOG.debug ("Using database {}",_dbName);
130             _isLower = dbMeta.storesLowerCaseIdentifiers();
131             _isUpper = dbMeta.storesUpperCaseIdentifiers();
132         }
133 
134         /**
135          * Convert a camel case identifier into either upper or lower
136          * depending on the way the db stores identifiers.
137          *
138          * @param identifier
139          * @return the converted identifier
140          */
convertIdentifier(String identifier)141         public String convertIdentifier (String identifier)
142         {
143             if (_isLower)
144                 return identifier.toLowerCase(Locale.ENGLISH);
145             if (_isUpper)
146                 return identifier.toUpperCase(Locale.ENGLISH);
147 
148             return identifier;
149         }
150 
getDBName()151         public String getDBName ()
152         {
153             return _dbName;
154         }
155 
getBlobType()156         public String getBlobType ()
157         {
158             if (_blobType != null)
159                 return _blobType;
160 
161             if (_dbName.startsWith("postgres"))
162                 return "bytea";
163 
164             return "blob";
165         }
166 
getLongType()167         public String getLongType ()
168         {
169             if (_longType != null)
170                 return _longType;
171 
172             if (_dbName.startsWith("oracle"))
173                 return "number(20)";
174 
175             return "bigint";
176         }
177 
getBlobInputStream(ResultSet result, String columnName)178         public InputStream getBlobInputStream (ResultSet result, String columnName)
179         throws SQLException
180         {
181             if (_dbName.startsWith("postgres"))
182             {
183                 byte[] bytes = result.getBytes(columnName);
184                 return new ByteArrayInputStream(bytes);
185             }
186 
187             Blob blob = result.getBlob(columnName);
188             return blob.getBinaryStream();
189         }
190 
191         /**
192          * rowId is a reserved word for Oracle, so change the name of this column
193          * @return
194          */
getRowIdColumnName()195         public String getRowIdColumnName ()
196         {
197             if (_dbName != null && _dbName.startsWith("oracle"))
198                 return "srowId";
199 
200             return "rowId";
201         }
202 
203 
isEmptyStringNull()204         public boolean isEmptyStringNull ()
205         {
206             return (_dbName.startsWith("oracle"));
207         }
208 
getLoadStatement(Connection connection, String rowId, String contextPath, String virtualHosts)209         public PreparedStatement getLoadStatement (Connection connection, String rowId, String contextPath, String virtualHosts)
210         throws SQLException
211         {
212             if (contextPath == null || "".equals(contextPath))
213             {
214                 if (isEmptyStringNull())
215                 {
216                     PreparedStatement statement = connection.prepareStatement("select * from "+_sessionTable+
217                     " where sessionId = ? and contextPath is null and virtualHost = ?");
218                     statement.setString(1, rowId);
219                     statement.setString(2, virtualHosts);
220 
221                     return statement;
222                 }
223             }
224 
225 
226 
227             PreparedStatement statement = connection.prepareStatement("select * from "+_sessionTable+
228             " where sessionId = ? and contextPath = ? and virtualHost = ?");
229             statement.setString(1, rowId);
230             statement.setString(2, contextPath);
231             statement.setString(3, virtualHosts);
232 
233             return statement;
234         }
235     }
236 
237 
238 
JDBCSessionIdManager(Server server)239     public JDBCSessionIdManager(Server server)
240     {
241         super();
242         _server=server;
243     }
244 
JDBCSessionIdManager(Server server, Random random)245     public JDBCSessionIdManager(Server server, Random random)
246     {
247        super(random);
248        _server=server;
249     }
250 
251     /**
252      * Configure jdbc connection information via a jdbc Driver
253      *
254      * @param driverClassName
255      * @param connectionUrl
256      */
setDriverInfo(String driverClassName, String connectionUrl)257     public void setDriverInfo (String driverClassName, String connectionUrl)
258     {
259         _driverClassName=driverClassName;
260         _connectionUrl=connectionUrl;
261     }
262 
263     /**
264      * Configure jdbc connection information via a jdbc Driver
265      *
266      * @param driverClass
267      * @param connectionUrl
268      */
setDriverInfo(Driver driverClass, String connectionUrl)269     public void setDriverInfo (Driver driverClass, String connectionUrl)
270     {
271         _driver=driverClass;
272         _connectionUrl=connectionUrl;
273     }
274 
275 
setDatasource(DataSource ds)276     public void setDatasource (DataSource ds)
277     {
278         _datasource = ds;
279     }
280 
getDataSource()281     public DataSource getDataSource ()
282     {
283         return _datasource;
284     }
285 
getDriverClassName()286     public String getDriverClassName()
287     {
288         return _driverClassName;
289     }
290 
getConnectionUrl()291     public String getConnectionUrl ()
292     {
293         return _connectionUrl;
294     }
295 
setDatasourceName(String jndi)296     public void setDatasourceName (String jndi)
297     {
298         _jndiName=jndi;
299     }
300 
getDatasourceName()301     public String getDatasourceName ()
302     {
303         return _jndiName;
304     }
305 
setBlobType(String name)306     public void setBlobType (String name)
307     {
308         _blobType = name;
309     }
310 
getBlobType()311     public String getBlobType ()
312     {
313         return _blobType;
314     }
315 
316 
317 
getLongType()318     public String getLongType()
319     {
320         return _longType;
321     }
322 
setLongType(String longType)323     public void setLongType(String longType)
324     {
325         this._longType = longType;
326     }
327 
setScavengeInterval(long sec)328     public void setScavengeInterval (long sec)
329     {
330         if (sec<=0)
331             sec=60;
332 
333         long old_period=_scavengeIntervalMs;
334         long period=sec*1000L;
335 
336         _scavengeIntervalMs=period;
337 
338         //add a bit of variability into the scavenge time so that not all
339         //nodes with the same scavenge time sync up
340         long tenPercent = _scavengeIntervalMs/10;
341         if ((System.currentTimeMillis()%2) == 0)
342             _scavengeIntervalMs += tenPercent;
343 
344         if (LOG.isDebugEnabled())
345             LOG.debug("Scavenging every "+_scavengeIntervalMs+" ms");
346         if (_timer!=null && (period!=old_period || _task==null))
347         {
348             synchronized (this)
349             {
350                 if (_task!=null)
351                     _task.cancel();
352                 _task = new TimerTask()
353                 {
354                     @Override
355                     public void run()
356                     {
357                         scavenge();
358                     }
359                 };
360                 _timer.schedule(_task,_scavengeIntervalMs,_scavengeIntervalMs);
361             }
362         }
363     }
364 
getScavengeInterval()365     public long getScavengeInterval ()
366     {
367         return _scavengeIntervalMs/1000;
368     }
369 
370 
addSession(HttpSession session)371     public void addSession(HttpSession session)
372     {
373         if (session == null)
374             return;
375 
376         synchronized (_sessionIds)
377         {
378             String id = ((JDBCSessionManager.Session)session).getClusterId();
379             try
380             {
381                 insert(id);
382                 _sessionIds.add(id);
383             }
384             catch (Exception e)
385             {
386                 LOG.warn("Problem storing session id="+id, e);
387             }
388         }
389     }
390 
removeSession(HttpSession session)391     public void removeSession(HttpSession session)
392     {
393         if (session == null)
394             return;
395 
396         removeSession(((JDBCSessionManager.Session)session).getClusterId());
397     }
398 
399 
400 
removeSession(String id)401     public void removeSession (String id)
402     {
403 
404         if (id == null)
405             return;
406 
407         synchronized (_sessionIds)
408         {
409             if (LOG.isDebugEnabled())
410                 LOG.debug("Removing session id="+id);
411             try
412             {
413                 _sessionIds.remove(id);
414                 delete(id);
415             }
416             catch (Exception e)
417             {
418                 LOG.warn("Problem removing session id="+id, e);
419             }
420         }
421 
422     }
423 
424 
425     /**
426      * Get the session id without any node identifier suffix.
427      *
428      * @see org.eclipse.jetty.server.SessionIdManager#getClusterId(java.lang.String)
429      */
getClusterId(String nodeId)430     public String getClusterId(String nodeId)
431     {
432         int dot=nodeId.lastIndexOf('.');
433         return (dot>0)?nodeId.substring(0,dot):nodeId;
434     }
435 
436 
437     /**
438      * Get the session id, including this node's id as a suffix.
439      *
440      * @see org.eclipse.jetty.server.SessionIdManager#getNodeId(java.lang.String, javax.servlet.http.HttpServletRequest)
441      */
getNodeId(String clusterId, HttpServletRequest request)442     public String getNodeId(String clusterId, HttpServletRequest request)
443     {
444         if (_workerName!=null)
445             return clusterId+'.'+_workerName;
446 
447         return clusterId;
448     }
449 
450 
idInUse(String id)451     public boolean idInUse(String id)
452     {
453         if (id == null)
454             return false;
455 
456         String clusterId = getClusterId(id);
457         boolean inUse = false;
458         synchronized (_sessionIds)
459         {
460             inUse = _sessionIds.contains(clusterId);
461         }
462 
463 
464         if (inUse)
465             return true; //optimisation - if this session is one we've been managing, we can check locally
466 
467         //otherwise, we need to go to the database to check
468         try
469         {
470             return exists(clusterId);
471         }
472         catch (Exception e)
473         {
474             LOG.warn("Problem checking inUse for id="+clusterId, e);
475             return false;
476         }
477     }
478 
479     /**
480      * Invalidate the session matching the id on all contexts.
481      *
482      * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
483      */
invalidateAll(String id)484     public void invalidateAll(String id)
485     {
486         //take the id out of the list of known sessionids for this node
487         removeSession(id);
488 
489         synchronized (_sessionIds)
490         {
491             //tell all contexts that may have a session object with this id to
492             //get rid of them
493             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
494             for (int i=0; contexts!=null && i<contexts.length; i++)
495             {
496                 SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
497                 if (sessionHandler != null)
498                 {
499                     SessionManager manager = sessionHandler.getSessionManager();
500 
501                     if (manager != null && manager instanceof JDBCSessionManager)
502                     {
503                         ((JDBCSessionManager)manager).invalidateSession(id);
504                     }
505                 }
506             }
507         }
508     }
509 
510 
511     /**
512      * Start up the id manager.
513      *
514      * Makes necessary database tables and starts a Session
515      * scavenger thread.
516      */
517     @Override
doStart()518     public void doStart()
519     throws Exception
520     {
521         initializeDatabase();
522         prepareTables();
523         cleanExpiredSessions();
524         super.doStart();
525         if (LOG.isDebugEnabled())
526             LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
527         _timer=new Timer("JDBCSessionScavenger", true);
528         setScavengeInterval(getScavengeInterval());
529     }
530 
531     /**
532      * Stop the scavenger.
533      */
534     @Override
doStop()535     public void doStop ()
536     throws Exception
537     {
538         synchronized(this)
539         {
540             if (_task!=null)
541                 _task.cancel();
542             if (_timer!=null)
543                 _timer.cancel();
544             _timer=null;
545         }
546         _sessionIds.clear();
547         super.doStop();
548     }
549 
550     /**
551      * Get a connection from the driver or datasource.
552      *
553      * @return the connection for the datasource
554      * @throws SQLException
555      */
getConnection()556     protected Connection getConnection ()
557     throws SQLException
558     {
559         if (_datasource != null)
560             return _datasource.getConnection();
561         else
562             return DriverManager.getConnection(_connectionUrl);
563     }
564 
565 
566 
567 
568 
569     /**
570      * Set up the tables in the database
571      * @throws SQLException
572      */
prepareTables()573     private void prepareTables()
574     throws SQLException
575     {
576         _createSessionIdTable = "create table "+_sessionIdTable+" (id varchar(120), primary key(id))";
577         _selectBoundedExpiredSessions = "select * from "+_sessionTable+" where expiryTime >= ? and expiryTime <= ?";
578         _selectExpiredSessions = "select * from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
579         _deleteOldExpiredSessions = "delete from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
580 
581         _insertId = "insert into "+_sessionIdTable+" (id)  values (?)";
582         _deleteId = "delete from "+_sessionIdTable+" where id = ?";
583         _queryId = "select * from "+_sessionIdTable+" where id = ?";
584 
585         Connection connection = null;
586         try
587         {
588             //make the id table
589             connection = getConnection();
590             connection.setAutoCommit(true);
591             DatabaseMetaData metaData = connection.getMetaData();
592             _dbAdaptor = new DatabaseAdaptor(metaData);
593             _sessionTableRowId = _dbAdaptor.getRowIdColumnName();
594 
595             //checking for table existence is case-sensitive, but table creation is not
596             String tableName = _dbAdaptor.convertIdentifier(_sessionIdTable);
597             ResultSet result = metaData.getTables(null, null, tableName, null);
598             if (!result.next())
599             {
600                 //table does not exist, so create it
601                 connection.createStatement().executeUpdate(_createSessionIdTable);
602             }
603 
604             //make the session table if necessary
605             tableName = _dbAdaptor.convertIdentifier(_sessionTable);
606             result = metaData.getTables(null, null, tableName, null);
607             if (!result.next())
608             {
609                 //table does not exist, so create it
610                 String blobType = _dbAdaptor.getBlobType();
611                 String longType = _dbAdaptor.getLongType();
612                 _createSessionTable = "create table "+_sessionTable+" ("+_sessionTableRowId+" varchar(120), sessionId varchar(120), "+
613                                            " contextPath varchar(60), virtualHost varchar(60), lastNode varchar(60), accessTime "+longType+", "+
614                                            " lastAccessTime "+longType+", createTime "+longType+", cookieTime "+longType+", "+
615                                            " lastSavedTime "+longType+", expiryTime "+longType+", map "+blobType+", primary key("+_sessionTableRowId+"))";
616                 connection.createStatement().executeUpdate(_createSessionTable);
617             }
618 
619             //make some indexes on the JettySessions table
620             String index1 = "idx_"+_sessionTable+"_expiry";
621             String index2 = "idx_"+_sessionTable+"_session";
622 
623             result = metaData.getIndexInfo(null, null, tableName, false, false);
624             boolean index1Exists = false;
625             boolean index2Exists = false;
626             while (result.next())
627             {
628                 String idxName = result.getString("INDEX_NAME");
629                 if (index1.equalsIgnoreCase(idxName))
630                     index1Exists = true;
631                 else if (index2.equalsIgnoreCase(idxName))
632                     index2Exists = true;
633             }
634             if (!(index1Exists && index2Exists))
635             {
636                 Statement statement = connection.createStatement();
637                 try
638                 {
639                     if (!index1Exists)
640                         statement.executeUpdate("create index "+index1+" on "+_sessionTable+" (expiryTime)");
641                     if (!index2Exists)
642                         statement.executeUpdate("create index "+index2+" on "+_sessionTable+" (sessionId, contextPath)");
643                 }
644                 finally
645                 {
646                     if (statement!=null)
647                     {
648                         try { statement.close(); }
649                         catch(Exception e) { LOG.warn(e); }
650                     }
651                 }
652             }
653 
654             //set up some strings representing the statements for session manipulation
655             _insertSession = "insert into "+_sessionTable+
656             " ("+_sessionTableRowId+", sessionId, contextPath, virtualHost, lastNode, accessTime, lastAccessTime, createTime, cookieTime, lastSavedTime, expiryTime, map) "+
657             " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
658 
659             _deleteSession = "delete from "+_sessionTable+
660             " where "+_sessionTableRowId+" = ?";
661 
662             _updateSession = "update "+_sessionTable+
663             " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ?, map = ? where "+_sessionTableRowId+" = ?";
664 
665             _updateSessionNode = "update "+_sessionTable+
666             " set lastNode = ? where "+_sessionTableRowId+" = ?";
667 
668             _updateSessionAccessTime = "update "+_sessionTable+
669             " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ? where "+_sessionTableRowId+" = ?";
670 
671 
672         }
673         finally
674         {
675             if (connection != null)
676                 connection.close();
677         }
678     }
679 
680     /**
681      * Insert a new used session id into the table.
682      *
683      * @param id
684      * @throws SQLException
685      */
insert(String id)686     private void insert (String id)
687     throws SQLException
688     {
689         Connection connection = null;
690         PreparedStatement statement = null;
691         PreparedStatement query = null;
692         try
693         {
694             connection = getConnection();
695             connection.setAutoCommit(true);
696             query = connection.prepareStatement(_queryId);
697             query.setString(1, id);
698             ResultSet result = query.executeQuery();
699             //only insert the id if it isn't in the db already
700             if (!result.next())
701             {
702                 statement = connection.prepareStatement(_insertId);
703                 statement.setString(1, id);
704                 statement.executeUpdate();
705             }
706         }
707         finally
708         {
709             if (query!=null)
710             {
711                 try { query.close(); }
712                 catch(Exception e) { LOG.warn(e); }
713             }
714 
715             if (statement!=null)
716             {
717                 try { statement.close(); }
718                 catch(Exception e) { LOG.warn(e); }
719             }
720 
721             if (connection != null)
722                 connection.close();
723         }
724     }
725 
726     /**
727      * Remove a session id from the table.
728      *
729      * @param id
730      * @throws SQLException
731      */
delete(String id)732     private void delete (String id)
733     throws SQLException
734     {
735         Connection connection = null;
736         PreparedStatement statement = null;
737         try
738         {
739             connection = getConnection();
740             connection.setAutoCommit(true);
741             statement = connection.prepareStatement(_deleteId);
742             statement.setString(1, id);
743             statement.executeUpdate();
744         }
745         finally
746         {
747             if (statement!=null)
748             {
749                 try { statement.close(); }
750                 catch(Exception e) { LOG.warn(e); }
751             }
752 
753             if (connection != null)
754                 connection.close();
755         }
756     }
757 
758 
759     /**
760      * Check if a session id exists.
761      *
762      * @param id
763      * @return
764      * @throws SQLException
765      */
exists(String id)766     private boolean exists (String id)
767     throws SQLException
768     {
769         Connection connection = null;
770         PreparedStatement statement = null;
771         try
772         {
773             connection = getConnection();
774             connection.setAutoCommit(true);
775             statement = connection.prepareStatement(_queryId);
776             statement.setString(1, id);
777             ResultSet result = statement.executeQuery();
778             return result.next();
779         }
780         finally
781         {
782             if (statement!=null)
783             {
784                 try { statement.close(); }
785                 catch(Exception e) { LOG.warn(e); }
786             }
787 
788             if (connection != null)
789                 connection.close();
790         }
791     }
792 
793     /**
794      * Look for sessions in the database that have expired.
795      *
796      * We do this in the SessionIdManager and not the SessionManager so
797      * that we only have 1 scavenger, otherwise if there are n SessionManagers
798      * there would be n scavengers, all contending for the database.
799      *
800      * We look first for sessions that expired in the previous interval, then
801      * for sessions that expired previously - these are old sessions that no
802      * node is managing any more and have become stuck in the database.
803      */
scavenge()804     private void scavenge ()
805     {
806         Connection connection = null;
807         PreparedStatement statement = null;
808         List<String> expiredSessionIds = new ArrayList<String>();
809         try
810         {
811             if (LOG.isDebugEnabled())
812                 LOG.debug("Scavenge sweep started at "+System.currentTimeMillis());
813             if (_lastScavengeTime > 0)
814             {
815                 connection = getConnection();
816                 connection.setAutoCommit(true);
817                 //"select sessionId from JettySessions where expiryTime > (lastScavengeTime - scanInterval) and expiryTime < lastScavengeTime";
818                 statement = connection.prepareStatement(_selectBoundedExpiredSessions);
819                 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
820                 long upperBound = _lastScavengeTime;
821                 if (LOG.isDebugEnabled())
822                     LOG.debug (" Searching for sessions expired between "+lowerBound + " and "+upperBound);
823 
824                 statement.setLong(1, lowerBound);
825                 statement.setLong(2, upperBound);
826                 ResultSet result = statement.executeQuery();
827                 while (result.next())
828                 {
829                     String sessionId = result.getString("sessionId");
830                     expiredSessionIds.add(sessionId);
831                     if (LOG.isDebugEnabled()) LOG.debug (" Found expired sessionId="+sessionId);
832                 }
833 
834                 //tell the SessionManagers to expire any sessions with a matching sessionId in memory
835                 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
836                 for (int i=0; contexts!=null && i<contexts.length; i++)
837                 {
838 
839                     SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
840                     if (sessionHandler != null)
841                     {
842                         SessionManager manager = sessionHandler.getSessionManager();
843                         if (manager != null && manager instanceof JDBCSessionManager)
844                         {
845                             ((JDBCSessionManager)manager).expire(expiredSessionIds);
846                         }
847                     }
848                 }
849 
850                 //find all sessions that have expired at least a couple of scanIntervals ago and just delete them
851                 upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
852                 if (upperBound > 0)
853                 {
854                     if (LOG.isDebugEnabled()) LOG.debug("Deleting old expired sessions expired before "+upperBound);
855                     try
856                     {
857                         statement = connection.prepareStatement(_deleteOldExpiredSessions);
858                         statement.setLong(1, upperBound);
859                         int rows = statement.executeUpdate();
860                         if (LOG.isDebugEnabled()) LOG.debug("Deleted "+rows+" rows of old sessions expired before "+upperBound);
861                     }
862                     finally
863                     {
864                         if (statement!=null)
865                         {
866                             try { statement.close(); }
867                             catch(Exception e) { LOG.warn(e); }
868                         }
869                     }
870                 }
871             }
872         }
873         catch (Exception e)
874         {
875             if (isRunning())
876                 LOG.warn("Problem selecting expired sessions", e);
877             else
878                 LOG.ignore(e);
879         }
880         finally
881         {
882             _lastScavengeTime=System.currentTimeMillis();
883             if (LOG.isDebugEnabled()) LOG.debug("Scavenge sweep ended at "+_lastScavengeTime);
884             if (connection != null)
885             {
886                 try
887                 {
888                 connection.close();
889                 }
890                 catch (SQLException e)
891                 {
892                     LOG.warn(e);
893                 }
894             }
895         }
896     }
897 
898     /**
899      * Get rid of sessions and sessionids from sessions that have already expired
900      * @throws Exception
901      */
cleanExpiredSessions()902     private void cleanExpiredSessions ()
903     {
904         Connection connection = null;
905         PreparedStatement statement = null;
906         Statement sessionsTableStatement = null;
907         Statement sessionIdsTableStatement = null;
908         List<String> expiredSessionIds = new ArrayList<String>();
909         try
910         {
911             connection = getConnection();
912             connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
913             connection.setAutoCommit(false);
914 
915             statement = connection.prepareStatement(_selectExpiredSessions);
916             long now = System.currentTimeMillis();
917             if (LOG.isDebugEnabled()) LOG.debug ("Searching for sessions expired before {}", now);
918 
919             statement.setLong(1, now);
920             ResultSet result = statement.executeQuery();
921             while (result.next())
922             {
923                 String sessionId = result.getString("sessionId");
924                 expiredSessionIds.add(sessionId);
925                 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId={}", sessionId);
926             }
927 
928             sessionsTableStatement = null;
929             sessionIdsTableStatement = null;
930 
931             if (!expiredSessionIds.isEmpty())
932             {
933                 sessionsTableStatement = connection.createStatement();
934                 sessionsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionTable+" where sessionId in ", expiredSessionIds));
935                 sessionIdsTableStatement = connection.createStatement();
936                 sessionIdsTableStatement.executeUpdate(createCleanExpiredSessionsSql("delete from "+_sessionIdTable+" where id in ", expiredSessionIds));
937             }
938             connection.commit();
939 
940             synchronized (_sessionIds)
941             {
942                 _sessionIds.removeAll(expiredSessionIds); //in case they were in our local cache of session ids
943             }
944         }
945         catch (Exception e)
946         {
947             if (connection != null)
948             {
949                 try
950                 {
951                     LOG.warn("Rolling back clean of expired sessions", e);
952                     connection.rollback();
953                 }
954                 catch (Exception x) { LOG.warn("Rollback of expired sessions failed", x);}
955             }
956         }
957         finally
958         {
959             if (sessionIdsTableStatement!=null)
960             {
961                 try { sessionIdsTableStatement.close(); }
962                 catch(Exception e) { LOG.warn(e); }
963             }
964 
965             if (sessionsTableStatement!=null)
966             {
967                 try { sessionsTableStatement.close(); }
968                 catch(Exception e) { LOG.warn(e); }
969             }
970 
971             if (statement!=null)
972             {
973                 try { statement.close(); }
974                 catch(Exception e) { LOG.warn(e); }
975             }
976 
977             try
978             {
979                 if (connection != null)
980                     connection.close();
981             }
982             catch (SQLException e)
983             {
984                 LOG.warn(e);
985             }
986         }
987     }
988 
989 
990     /**
991      *
992      * @param sql
993      * @param connection
994      * @param expiredSessionIds
995      * @throws Exception
996      */
createCleanExpiredSessionsSql(String sql,Collection<String> expiredSessionIds)997     private String createCleanExpiredSessionsSql (String sql,Collection<String> expiredSessionIds)
998     throws Exception
999     {
1000         StringBuffer buff = new StringBuffer();
1001         buff.append(sql);
1002         buff.append("(");
1003         Iterator<String> itor = expiredSessionIds.iterator();
1004         while (itor.hasNext())
1005         {
1006             buff.append("'"+(itor.next())+"'");
1007             if (itor.hasNext())
1008                 buff.append(",");
1009         }
1010         buff.append(")");
1011 
1012         if (LOG.isDebugEnabled()) LOG.debug("Cleaning expired sessions with: {}", buff);
1013         return buff.toString();
1014     }
1015 
initializeDatabase()1016     private void initializeDatabase ()
1017     throws Exception
1018     {
1019         if (_datasource != null)
1020             return; //already set up
1021 
1022         if (_jndiName!=null)
1023         {
1024             InitialContext ic = new InitialContext();
1025             _datasource = (DataSource)ic.lookup(_jndiName);
1026         }
1027         else if ( _driver != null && _connectionUrl != null )
1028         {
1029             DriverManager.registerDriver(_driver);
1030         }
1031         else if (_driverClassName != null && _connectionUrl != null)
1032         {
1033             Class.forName(_driverClassName);
1034         }
1035         else
1036             throw new IllegalStateException("No database configured for sessions");
1037     }
1038 
1039 
1040 }
1041