View Javadoc

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