View Javadoc

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.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  import java.util.concurrent.TimeUnit;
39  
40  import javax.naming.InitialContext;
41  import javax.servlet.http.HttpServletRequest;
42  import javax.servlet.http.HttpSession;
43  import javax.sql.DataSource;
44  
45  import org.eclipse.jetty.server.Handler;
46  import org.eclipse.jetty.server.Server;
47  import org.eclipse.jetty.server.SessionManager;
48  import org.eclipse.jetty.server.handler.ContextHandler;
49  import org.eclipse.jetty.util.log.Logger;
50  import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
51  import org.eclipse.jetty.util.thread.Scheduler;
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      public final static int MAX_INTERVAL_NOT_SET = -999;
66  
67      protected final HashSet<String> _sessionIds = new HashSet<String>();
68      protected Server _server;
69      protected Driver _driver;
70      protected String _driverClassName;
71      protected String _connectionUrl;
72      protected DataSource _datasource;
73      protected String _jndiName;
74  
75      protected int _deleteBlockSize = 10; //number of ids to include in where 'in' clause
76  
77      protected Scheduler.Task _task; //scavenge task
78      protected Scheduler _scheduler;
79      protected Scavenger _scavenger;
80      protected boolean _ownScheduler;
81      protected long _lastScavengeTime;
82      protected long _scavengeIntervalMs = 1000L * 60 * 10; //10mins
83  
84  
85      protected String _createSessionIdTable;
86      protected String _createSessionTable;
87  
88      protected String _selectBoundedExpiredSessions;
89      private String _selectExpiredSessions;
90      
91      protected String _insertId;
92      protected String _deleteId;
93      protected String _queryId;
94  
95      protected  String _insertSession;
96      protected  String _deleteSession;
97      protected  String _updateSession;
98      protected  String _updateSessionNode;
99      protected  String _updateSessionAccessTime;
100 
101     protected DatabaseAdaptor _dbAdaptor = new DatabaseAdaptor();
102     protected SessionIdTableSchema _sessionIdTableSchema = new SessionIdTableSchema();
103     protected SessionTableSchema _sessionTableSchema = new SessionTableSchema();
104     
105   
106 
107  
108     /**
109      * SessionTableSchema
110      *
111      */
112     public static class SessionTableSchema
113     {        
114         protected DatabaseAdaptor _dbAdaptor;
115         protected String _tableName = "JettySessions";
116         protected String _rowIdColumn = "rowId";
117         protected String _idColumn = "sessionId";
118         protected String _contextPathColumn = "contextPath";
119         protected String _virtualHostColumn = "virtualHost"; 
120         protected String _lastNodeColumn = "lastNode";
121         protected String _accessTimeColumn = "accessTime"; 
122         protected String _lastAccessTimeColumn = "lastAccessTime";
123         protected String _createTimeColumn = "createTime";
124         protected String _cookieTimeColumn = "cookieTime";
125         protected String _lastSavedTimeColumn = "lastSavedTime";
126         protected String _expiryTimeColumn = "expiryTime";
127         protected String _maxIntervalColumn = "maxInterval";
128         protected String _mapColumn = "map";
129         
130         
131         protected void setDatabaseAdaptor(DatabaseAdaptor dbadaptor)
132         {
133             _dbAdaptor = dbadaptor;
134         }
135         
136         
137         public String getTableName()
138         {
139             return _tableName;
140         }
141         public void setTableName(String tableName)
142         {
143             checkNotNull(tableName);
144             _tableName = tableName;
145         }
146         public String getRowIdColumn()
147         {       
148             if ("rowId".equals(_rowIdColumn) && _dbAdaptor.isRowIdReserved())
149                 _rowIdColumn = "srowId";
150             return _rowIdColumn;
151         }
152         public void setRowIdColumn(String rowIdColumn)
153         {
154             checkNotNull(rowIdColumn);
155             if (_dbAdaptor == null)
156                 throw new IllegalStateException ("DbAdaptor is null");
157             
158             if (_dbAdaptor.isRowIdReserved() && "rowId".equals(rowIdColumn))
159                 throw new IllegalArgumentException("rowId is reserved word for Oracle");
160             
161             _rowIdColumn = rowIdColumn;
162         }
163         public String getIdColumn()
164         {
165             return _idColumn;
166         }
167         public void setIdColumn(String idColumn)
168         {
169             checkNotNull(idColumn);
170             _idColumn = idColumn;
171         }
172         public String getContextPathColumn()
173         {
174             return _contextPathColumn;
175         }
176         public void setContextPathColumn(String contextPathColumn)
177         {
178             checkNotNull(contextPathColumn);
179             _contextPathColumn = contextPathColumn;
180         }
181         public String getVirtualHostColumn()
182         {
183             return _virtualHostColumn;
184         }
185         public void setVirtualHostColumn(String virtualHostColumn)
186         {
187             checkNotNull(virtualHostColumn);
188             _virtualHostColumn = virtualHostColumn;
189         }
190         public String getLastNodeColumn()
191         {
192             return _lastNodeColumn;
193         }
194         public void setLastNodeColumn(String lastNodeColumn)
195         {
196             checkNotNull(lastNodeColumn);
197             _lastNodeColumn = lastNodeColumn;
198         }
199         public String getAccessTimeColumn()
200         {
201             return _accessTimeColumn;
202         }
203         public void setAccessTimeColumn(String accessTimeColumn)
204         {
205             checkNotNull(accessTimeColumn);
206             _accessTimeColumn = accessTimeColumn;
207         }
208         public String getLastAccessTimeColumn()
209         {
210             return _lastAccessTimeColumn;
211         }
212         public void setLastAccessTimeColumn(String lastAccessTimeColumn)
213         {
214             checkNotNull(lastAccessTimeColumn);
215             _lastAccessTimeColumn = lastAccessTimeColumn;
216         }
217         public String getCreateTimeColumn()
218         {
219             return _createTimeColumn;
220         }
221         public void setCreateTimeColumn(String createTimeColumn)
222         {
223             checkNotNull(createTimeColumn);
224             _createTimeColumn = createTimeColumn;
225         }
226         public String getCookieTimeColumn()
227         {
228             return _cookieTimeColumn;
229         }
230         public void setCookieTimeColumn(String cookieTimeColumn)
231         {
232             checkNotNull(cookieTimeColumn);
233             _cookieTimeColumn = cookieTimeColumn;
234         }
235         public String getLastSavedTimeColumn()
236         {
237             return _lastSavedTimeColumn;
238         }
239         public void setLastSavedTimeColumn(String lastSavedTimeColumn)
240         {
241             checkNotNull(lastSavedTimeColumn);
242             _lastSavedTimeColumn = lastSavedTimeColumn;
243         }
244         public String getExpiryTimeColumn()
245         {
246             return _expiryTimeColumn;
247         }
248         public void setExpiryTimeColumn(String expiryTimeColumn)
249         {
250             checkNotNull(expiryTimeColumn);
251             _expiryTimeColumn = expiryTimeColumn;
252         }
253         public String getMaxIntervalColumn()
254         {
255             return _maxIntervalColumn;
256         }
257         public void setMaxIntervalColumn(String maxIntervalColumn)
258         {
259             checkNotNull(maxIntervalColumn);
260             _maxIntervalColumn = maxIntervalColumn;
261         }
262         public String getMapColumn()
263         {
264             return _mapColumn;
265         }
266         public void setMapColumn(String mapColumn)
267         {
268             checkNotNull(mapColumn);
269             _mapColumn = mapColumn;
270         }
271         
272         public String getCreateStatementAsString ()
273         {
274             if (_dbAdaptor == null)
275                 throw new IllegalStateException ("No DBAdaptor");
276             
277             String blobType = _dbAdaptor.getBlobType();
278             String longType = _dbAdaptor.getLongType();
279             
280             return "create table "+_tableName+" ("+getRowIdColumn()+" varchar(120), "+_idColumn+" varchar(120), "+
281                     _contextPathColumn+" varchar(60), "+_virtualHostColumn+" varchar(60), "+_lastNodeColumn+" varchar(60), "+_accessTimeColumn+" "+longType+", "+
282                     _lastAccessTimeColumn+" "+longType+", "+_createTimeColumn+" "+longType+", "+_cookieTimeColumn+" "+longType+", "+
283                     _lastSavedTimeColumn+" "+longType+", "+_expiryTimeColumn+" "+longType+", "+_maxIntervalColumn+" "+longType+", "+
284                     _mapColumn+" "+blobType+", primary key("+getRowIdColumn()+"))";
285         }
286         
287         public String getCreateIndexOverExpiryStatementAsString (String indexName)
288         {
289             return "create index "+indexName+" on "+getTableName()+" ("+getExpiryTimeColumn()+")";
290         }
291         
292         public String getCreateIndexOverSessionStatementAsString (String indexName)
293         {
294             return "create index "+indexName+" on "+getTableName()+" ("+getIdColumn()+", "+getContextPathColumn()+")";
295         }
296         
297         public String getAlterTableForMaxIntervalAsString ()
298         {
299             if (_dbAdaptor == null)
300                 throw new IllegalStateException ("No DBAdaptor");
301             String longType = _dbAdaptor.getLongType();
302             return "alter table "+getTableName()+" add "+longType+" not null default "+MAX_INTERVAL_NOT_SET;
303         }
304         
305         private void checkNotNull(String s)
306         {
307             if (s == null)
308                 throw new IllegalArgumentException(s);
309         }
310         public String getInsertSessionStatementAsString()
311         {
312            return "insert into "+getTableName()+
313             " ("+getRowIdColumn()+", "+getIdColumn()+", "+getContextPathColumn()+", "+getVirtualHostColumn()+", "+getLastNodeColumn()+
314             ", "+getAccessTimeColumn()+", "+getLastAccessTimeColumn()+", "+getCreateTimeColumn()+", "+getCookieTimeColumn()+
315             ", "+getLastSavedTimeColumn()+", "+getExpiryTimeColumn()+", "+getMaxIntervalColumn()+", "+getMapColumn()+") "+
316             " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
317         }
318         public String getDeleteSessionStatementAsString()
319         {
320             return "delete from "+getTableName()+
321             " where "+getRowIdColumn()+" = ?";
322         }
323         public String getUpdateSessionStatementAsString()
324         {
325             return "update "+getTableName()+
326                     " set "+getIdColumn()+" = ?, "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+
327                     getLastAccessTimeColumn()+" = ?, "+getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+
328                     getMaxIntervalColumn()+" = ?, "+getMapColumn()+" = ? where "+getRowIdColumn()+" = ?";
329         }
330         public String getUpdateSessionNodeStatementAsString()
331         {
332             return "update "+getTableName()+
333                     " set "+getLastNodeColumn()+" = ? where "+getRowIdColumn()+" = ?";
334         }
335         public String getUpdateSessionAccessTimeStatementAsString()
336         {
337            return "update "+getTableName()+
338             " set "+getLastNodeColumn()+" = ?, "+getAccessTimeColumn()+" = ?, "+getLastAccessTimeColumn()+" = ?, "+
339                    getLastSavedTimeColumn()+" = ?, "+getExpiryTimeColumn()+" = ?, "+getMaxIntervalColumn()+" = ? where "+getRowIdColumn()+" = ?";
340         }
341         
342         public String getBoundedExpiredSessionsStatementAsString()
343         {
344             return "select * from "+getTableName()+" where "+getLastNodeColumn()+" = ? and "+getExpiryTimeColumn()+" >= ? and "+getExpiryTimeColumn()+" <= ?";
345         }
346         
347         public String getSelectExpiredSessionsStatementAsString()
348         {
349             return "select * from "+getTableName()+" where "+getExpiryTimeColumn()+" >0 and "+getExpiryTimeColumn()+" <= ?";
350         }
351      
352         public PreparedStatement getLoadStatement (Connection connection, String rowId, String contextPath, String virtualHosts)
353         throws SQLException
354         { 
355             if (_dbAdaptor == null)
356                 throw new IllegalStateException("No DB adaptor");
357 
358 
359             if (contextPath == null || "".equals(contextPath))
360             {
361                 if (_dbAdaptor.isEmptyStringNull())
362                 {
363                     PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+
364                                                                               " where "+getIdColumn()+" = ? and "+
365                                                                               getContextPathColumn()+" is null and "+
366                                                                               getVirtualHostColumn()+" = ?");
367                     statement.setString(1, rowId);
368                     statement.setString(2, virtualHosts);
369 
370                     return statement;
371                 }
372             }
373 
374             PreparedStatement statement = connection.prepareStatement("select * from "+getTableName()+
375                                                                       " where "+getIdColumn()+" = ? and "+getContextPathColumn()+
376                                                                       " = ? and "+getVirtualHostColumn()+" = ?");
377             statement.setString(1, rowId);
378             statement.setString(2, contextPath);
379             statement.setString(3, virtualHosts);
380 
381             return statement;
382         }
383     }
384     
385     
386     
387     /**
388      * SessionIdTableSchema
389      *
390      */
391     public static class SessionIdTableSchema
392     {
393         protected DatabaseAdaptor _dbAdaptor;
394         protected String _tableName = "JettySessionIds";
395         protected String _idColumn = "id";
396 
397         public void setDatabaseAdaptor(DatabaseAdaptor dbAdaptor)
398         {
399             _dbAdaptor = dbAdaptor;
400         }
401         public String getIdColumn()
402         {
403             return _idColumn;
404         }
405 
406         public void setIdColumn(String idColumn)
407         {
408             checkNotNull(idColumn);
409             _idColumn = idColumn;
410         }
411 
412         public String getTableName()
413         {
414             return _tableName;
415         }
416 
417         public void setTableName(String tableName)
418         {
419             checkNotNull(tableName);
420             _tableName = tableName;
421         }
422 
423         public String getInsertStatementAsString ()
424         {
425             return "insert into "+_tableName+" ("+_idColumn+")  values (?)";
426         }
427 
428         public String getDeleteStatementAsString ()
429         {
430             return "delete from "+_tableName+" where "+_idColumn+" = ?";
431         }
432 
433         public String getSelectStatementAsString ()
434         {
435             return  "select * from "+_tableName+" where "+_idColumn+" = ?";
436         }
437         
438         public String getCreateStatementAsString ()
439         {
440             return "create table "+_tableName+" ("+_idColumn+" varchar(120), primary key("+_idColumn+"))";
441         }
442         
443         private void checkNotNull(String s)
444         {
445             if (s == null)
446                 throw new IllegalArgumentException(s);
447         }
448     }
449 
450 
451     /**
452      * DatabaseAdaptor
453      *
454      * Handles differences between databases.
455      *
456      * Postgres uses the getBytes and setBinaryStream methods to access
457      * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
458      * is happy to use the "blob" type and getBlob() methods instead.
459      *
460      * TODO if the differences become more major it would be worthwhile
461      * refactoring this class.
462      */
463     public static class DatabaseAdaptor
464     {
465         String _dbName;
466         boolean _isLower;
467         boolean _isUpper;
468         
469         protected String _blobType; //if not set, is deduced from the type of the database at runtime
470         protected String _longType; //if not set, is deduced from the type of the database at runtime
471 
472 
473         public DatabaseAdaptor ()
474         {           
475         }
476         
477         
478         public void adaptTo(DatabaseMetaData dbMeta)  
479         throws SQLException
480         {
481             _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH);
482             LOG.debug ("Using database {}",_dbName);
483             _isLower = dbMeta.storesLowerCaseIdentifiers();
484             _isUpper = dbMeta.storesUpperCaseIdentifiers(); 
485         }
486         
487        
488         public void setBlobType(String blobType)
489         {
490             _blobType = blobType;
491         }
492         
493         public String getBlobType ()
494         {
495             if (_blobType != null)
496                 return _blobType;
497 
498             if (_dbName.startsWith("postgres"))
499                 return "bytea";
500 
501             return "blob";
502         }
503         
504 
505         public void setLongType(String longType)
506         {
507             _longType = longType;
508         }
509         
510 
511         public String getLongType ()
512         {
513             if (_longType != null)
514                 return _longType;
515 
516             if (_dbName == null)
517                 throw new IllegalStateException ("DbAdaptor missing metadata");
518             
519             if (_dbName.startsWith("oracle"))
520                 return "number(20)";
521 
522             return "bigint";
523         }
524         
525 
526         /**
527          * Convert a camel case identifier into either upper or lower
528          * depending on the way the db stores identifiers.
529          *
530          * @param identifier
531          * @return the converted identifier
532          */
533         public String convertIdentifier (String identifier)
534         {
535             if (_dbName == null)
536                 throw new IllegalStateException ("DbAdaptor missing metadata");
537             
538             if (_isLower)
539                 return identifier.toLowerCase(Locale.ENGLISH);
540             if (_isUpper)
541                 return identifier.toUpperCase(Locale.ENGLISH);
542 
543             return identifier;
544         }
545 
546         public String getDBName ()
547         {
548             return _dbName;
549         }
550 
551 
552         public InputStream getBlobInputStream (ResultSet result, String columnName)
553         throws SQLException
554         {
555             if (_dbName == null)
556                 throw new IllegalStateException ("DbAdaptor missing metadata");
557             
558             if (_dbName.startsWith("postgres"))
559             {
560                 byte[] bytes = result.getBytes(columnName);
561                 return new ByteArrayInputStream(bytes);
562             }
563 
564             Blob blob = result.getBlob(columnName);
565             return blob.getBinaryStream();
566         }
567 
568 
569         public boolean isEmptyStringNull ()
570         {
571             if (_dbName == null)
572                 throw new IllegalStateException ("DbAdaptor missing metadata");
573             
574             return (_dbName.startsWith("oracle"));
575         }
576         
577         /**
578          * rowId is a reserved word for Oracle, so change the name of this column
579          * @return true if db in use is oracle
580          */
581         public boolean isRowIdReserved ()
582         {
583             if (_dbName == null)
584                 throw new IllegalStateException ("DbAdaptor missing metadata");
585             
586             return (_dbName != null && _dbName.startsWith("oracle"));
587         }
588     }
589 
590     
591     /**
592      * Scavenger
593      *
594      */
595     protected class Scavenger implements Runnable
596     {
597 
598         @Override
599         public void run()
600         {
601            try
602            {
603                scavenge();
604            }
605            finally
606            {
607                if (_scheduler != null && _scheduler.isRunning())
608                    _scheduler.schedule(this, _scavengeIntervalMs, TimeUnit.MILLISECONDS);
609            }
610         }
611     }
612 
613 
614     public JDBCSessionIdManager(Server server)
615     {
616         super();
617         _server=server;
618     }
619 
620     public JDBCSessionIdManager(Server server, Random random)
621     {
622        super(random);
623        _server=server;
624     }
625 
626     /**
627      * Configure jdbc connection information via a jdbc Driver
628      *
629      * @param driverClassName
630      * @param connectionUrl
631      */
632     public void setDriverInfo (String driverClassName, String connectionUrl)
633     {
634         _driverClassName=driverClassName;
635         _connectionUrl=connectionUrl;
636     }
637 
638     /**
639      * Configure jdbc connection information via a jdbc Driver
640      *
641      * @param driverClass
642      * @param connectionUrl
643      */
644     public void setDriverInfo (Driver driverClass, String connectionUrl)
645     {
646         _driver=driverClass;
647         _connectionUrl=connectionUrl;
648     }
649 
650 
651     public void setDatasource (DataSource ds)
652     {
653         _datasource = ds;
654     }
655 
656     public DataSource getDataSource ()
657     {
658         return _datasource;
659     }
660 
661     public String getDriverClassName()
662     {
663         return _driverClassName;
664     }
665 
666     public String getConnectionUrl ()
667     {
668         return _connectionUrl;
669     }
670 
671     public void setDatasourceName (String jndi)
672     {
673         _jndiName=jndi;
674     }
675 
676     public String getDatasourceName ()
677     {
678         return _jndiName;
679     }
680 
681     /**
682      * @param name
683      * @deprecated see DbAdaptor.setBlobType
684      */
685     public void setBlobType (String name)
686     {
687         _dbAdaptor.setBlobType(name);
688     }
689 
690     public DatabaseAdaptor getDbAdaptor()
691     {
692         return _dbAdaptor;
693     }
694 
695     public void setDbAdaptor(DatabaseAdaptor dbAdaptor)
696     {
697         if (dbAdaptor == null)
698             throw new IllegalStateException ("DbAdaptor cannot be null");
699         
700         _dbAdaptor = dbAdaptor;
701     }
702 
703     /**
704      * @return
705      * @deprecated see DbAdaptor.getBlobType
706      */
707     public String getBlobType ()
708     {
709         return _dbAdaptor.getBlobType();
710     }
711 
712     /**
713      * @return
714      * @deprecated see DbAdaptor.getLogType
715      */
716     public String getLongType()
717     {
718         return _dbAdaptor.getLongType();
719     }
720 
721     /**
722      * @param longType
723      * @deprecated see DbAdaptor.setLongType
724      */
725     public void setLongType(String longType)
726     {
727        _dbAdaptor.setLongType(longType);
728     }
729     
730     public SessionIdTableSchema getSessionIdTableSchema()
731     {
732         return _sessionIdTableSchema;
733     }
734 
735     public void setSessionIdTableSchema(SessionIdTableSchema sessionIdTableSchema)
736     {
737         if (sessionIdTableSchema == null)
738             throw new IllegalArgumentException("Null SessionIdTableSchema");
739         
740         _sessionIdTableSchema = sessionIdTableSchema;
741     }
742 
743     public SessionTableSchema getSessionTableSchema()
744     {
745         return _sessionTableSchema;
746     }
747 
748     public void setSessionTableSchema(SessionTableSchema sessionTableSchema)
749     {
750         _sessionTableSchema = sessionTableSchema;
751     }
752 
753     public void setDeleteBlockSize (int bsize)
754     {
755         this._deleteBlockSize = bsize;
756     }
757 
758     public int getDeleteBlockSize ()
759     {
760         return this._deleteBlockSize;
761     }
762     
763     public void setScavengeInterval (long sec)
764     {
765         if (sec<=0)
766             sec=60;
767 
768         long old_period=_scavengeIntervalMs;
769         long period=sec*1000L;
770 
771         _scavengeIntervalMs=period;
772 
773         //add a bit of variability into the scavenge time so that not all
774         //nodes with the same scavenge time sync up
775         long tenPercent = _scavengeIntervalMs/10;
776         if ((System.currentTimeMillis()%2) == 0)
777             _scavengeIntervalMs += tenPercent;
778 
779         if (LOG.isDebugEnabled())
780             LOG.debug("Scavenging every "+_scavengeIntervalMs+" ms");
781         
782         //if (_timer!=null && (period!=old_period || _task==null))
783         if (_scheduler != null && (period!=old_period || _task==null))
784         {
785             synchronized (this)
786             {
787                 if (_task!=null)
788                     _task.cancel();
789                 if (_scavenger == null)
790                     _scavenger = new Scavenger();
791                 _task = _scheduler.schedule(_scavenger,_scavengeIntervalMs,TimeUnit.MILLISECONDS);
792             }
793         }
794     }
795 
796     public long getScavengeInterval ()
797     {
798         return _scavengeIntervalMs/1000;
799     }
800 
801 
802     @Override
803     public void addSession(HttpSession session)
804     {
805         if (session == null)
806             return;
807 
808         synchronized (_sessionIds)
809         {
810             String id = ((JDBCSessionManager.Session)session).getClusterId();
811             try
812             {
813                 insert(id);
814                 _sessionIds.add(id);
815             }
816             catch (Exception e)
817             {
818                 LOG.warn("Problem storing session id="+id, e);
819             }
820         }
821     }
822     
823   
824     public void addSession(String id)
825     {
826         if (id == null)
827             return;
828 
829         synchronized (_sessionIds)
830         {           
831             try
832             {
833                 insert(id);
834                 _sessionIds.add(id);
835             }
836             catch (Exception e)
837             {
838                 LOG.warn("Problem storing session id="+id, e);
839             }
840         }
841     }
842 
843 
844 
845     @Override
846     public void removeSession(HttpSession session)
847     {
848         if (session == null)
849             return;
850 
851         removeSession(((JDBCSessionManager.Session)session).getClusterId());
852     }
853 
854 
855 
856     public void removeSession (String id)
857     {
858 
859         if (id == null)
860             return;
861 
862         synchronized (_sessionIds)
863         {
864             if (LOG.isDebugEnabled())
865                 LOG.debug("Removing sessionid="+id);
866             try
867             {
868                 _sessionIds.remove(id);
869                 delete(id);
870             }
871             catch (Exception e)
872             {
873                 LOG.warn("Problem removing session id="+id, e);
874             }
875         }
876 
877     }
878 
879 
880     @Override
881     public boolean idInUse(String id)
882     {
883         if (id == null)
884             return false;
885 
886         String clusterId = getClusterId(id);
887         boolean inUse = false;
888         synchronized (_sessionIds)
889         {
890             inUse = _sessionIds.contains(clusterId);
891         }
892 
893         
894         if (inUse)
895             return true; //optimisation - if this session is one we've been managing, we can check locally
896 
897         //otherwise, we need to go to the database to check
898         try
899         {
900             return exists(clusterId);
901         }
902         catch (Exception e)
903         {
904             LOG.warn("Problem checking inUse for id="+clusterId, e);
905             return false;
906         }
907     }
908 
909     /**
910      * Invalidate the session matching the id on all contexts.
911      *
912      * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
913      */
914     @Override
915     public void invalidateAll(String id)
916     {
917         //take the id out of the list of known sessionids for this node
918         removeSession(id);
919 
920         synchronized (_sessionIds)
921         {
922             //tell all contexts that may have a session object with this id to
923             //get rid of them
924             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
925             for (int i=0; contexts!=null && i<contexts.length; i++)
926             {
927                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
928                 if (sessionHandler != null)
929                 {
930                     SessionManager manager = sessionHandler.getSessionManager();
931 
932                     if (manager != null && manager instanceof JDBCSessionManager)
933                     {
934                         ((JDBCSessionManager)manager).invalidateSession(id);
935                     }
936                 }
937             }
938         }
939     }
940 
941 
942     @Override
943     public void renewSessionId (String oldClusterId, String oldNodeId, HttpServletRequest request)
944     {
945         //generate a new id
946         String newClusterId = newSessionId(request.hashCode());
947 
948         synchronized (_sessionIds)
949         {
950             removeSession(oldClusterId);//remove the old one from the list (and database)
951             addSession(newClusterId); //add in the new session id to the list (and database)
952 
953             //tell all contexts to update the id 
954             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
955             for (int i=0; contexts!=null && i<contexts.length; i++)
956             {
957                 SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
958                 if (sessionHandler != null) 
959                 {
960                     SessionManager manager = sessionHandler.getSessionManager();
961 
962                     if (manager != null && manager instanceof JDBCSessionManager)
963                     {
964                         ((JDBCSessionManager)manager).renewSessionId(oldClusterId, oldNodeId, newClusterId, getNodeId(newClusterId, request));
965                     }
966                 }
967             }
968         }
969     }
970 
971 
972     /**
973      * Start up the id manager.
974      *
975      * Makes necessary database tables and starts a Session
976      * scavenger thread.
977      */
978     @Override
979     public void doStart()
980     throws Exception
981     {           
982         initializeDatabase();
983         prepareTables();   
984         super.doStart();
985         if (LOG.isDebugEnabled()) 
986             LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
987         
988          //try and use a common scheduler, fallback to own
989          _scheduler =_server.getBean(Scheduler.class);
990          if (_scheduler == null)
991          {
992              _scheduler = new ScheduledExecutorScheduler();
993              _ownScheduler = true;
994              _scheduler.start();
995          }
996   
997         setScavengeInterval(getScavengeInterval());
998     }
999 
1000     /**
1001      * Stop the scavenger.
1002      */
1003     @Override
1004     public void doStop ()
1005     throws Exception
1006     {
1007         synchronized(this)
1008         {
1009             if (_task!=null)
1010                 _task.cancel();
1011             _task=null;
1012             if (_ownScheduler && _scheduler !=null)
1013                 _scheduler.stop();
1014             _scheduler=null;
1015         }
1016         _sessionIds.clear();
1017         super.doStop();
1018     }
1019 
1020     /**
1021      * Get a connection from the driver or datasource.
1022      *
1023      * @return the connection for the datasource
1024      * @throws SQLException
1025      */
1026     protected Connection getConnection ()
1027     throws SQLException
1028     {
1029         if (_datasource != null)
1030             return _datasource.getConnection();
1031         else
1032             return DriverManager.getConnection(_connectionUrl);
1033     }
1034     
1035 
1036 
1037 
1038 
1039 
1040     /**
1041      * Set up the tables in the database
1042      * @throws SQLException
1043      */
1044     /**
1045      * @throws SQLException
1046      */
1047     private void prepareTables()
1048     throws SQLException
1049     {
1050         if (_sessionIdTableSchema == null)
1051             throw new IllegalStateException ("No SessionIdTableSchema");
1052         
1053         if (_sessionTableSchema == null)
1054             throw new IllegalStateException ("No SessionTableSchema");
1055         
1056         try (Connection connection = getConnection();
1057              Statement statement = connection.createStatement())
1058         {
1059             //make the id table
1060             connection.setAutoCommit(true);
1061             DatabaseMetaData metaData = connection.getMetaData();
1062             _dbAdaptor.adaptTo(metaData);
1063             _sessionTableSchema.setDatabaseAdaptor(_dbAdaptor);
1064             _sessionIdTableSchema.setDatabaseAdaptor(_dbAdaptor);
1065             
1066             _createSessionIdTable = _sessionIdTableSchema.getCreateStatementAsString();
1067             _insertId = _sessionIdTableSchema.getInsertStatementAsString();
1068             _deleteId =  _sessionIdTableSchema.getDeleteStatementAsString();
1069             _queryId = _sessionIdTableSchema.getSelectStatementAsString();
1070             
1071             //checking for table existence is case-sensitive, but table creation is not
1072             String tableName = _dbAdaptor.convertIdentifier(_sessionIdTableSchema.getTableName());
1073             try (ResultSet result = metaData.getTables(null, null, tableName, null))
1074             {
1075                 if (!result.next())
1076                 {
1077                     //table does not exist, so create it
1078                     statement.executeUpdate(_createSessionIdTable);
1079                 }
1080             }         
1081             
1082             //make the session table if necessary
1083             tableName = _dbAdaptor.convertIdentifier(_sessionTableSchema.getTableName());
1084             try (ResultSet result = metaData.getTables(null, null, tableName, null))
1085             {
1086                 if (!result.next())
1087                 {
1088                     //table does not exist, so create it
1089                     _createSessionTable = _sessionTableSchema.getCreateStatementAsString();
1090                     statement.executeUpdate(_createSessionTable);
1091                 }
1092                 else
1093                 {
1094                     //session table exists, check it has maxinterval column
1095                     ResultSet colResult = null;
1096                     try
1097                     {
1098                         colResult = metaData.getColumns(null, null,
1099                                                         _dbAdaptor.convertIdentifier(_sessionTableSchema.getTableName()), 
1100                                                         _dbAdaptor.convertIdentifier(_sessionTableSchema.getMaxIntervalColumn()));
1101                     }
1102                     catch (SQLException s)
1103                     {
1104                         LOG.warn("Problem checking if "+_sessionTableSchema.getTableName()+
1105                                  " table contains "+_sessionTableSchema.getMaxIntervalColumn()+" column. Ensure table contains column definition: \""
1106                                 +_sessionTableSchema.getMaxIntervalColumn()+" long not null default -999\"");
1107                         throw s;
1108                     }
1109                     try
1110                     {
1111                         if (!colResult.next())
1112                         {
1113                             try
1114                             {
1115                                 //add the maxinterval column
1116                                 statement.executeUpdate(_sessionTableSchema.getAlterTableForMaxIntervalAsString());
1117                             }
1118                             catch (SQLException s)
1119                             {
1120                                 LOG.warn("Problem adding "+_sessionTableSchema.getMaxIntervalColumn()+
1121                                          " column. Ensure table contains column definition: \""+_sessionTableSchema.getMaxIntervalColumn()+
1122                                          " long not null default -999\"");
1123                                 throw s;
1124                             }
1125                         }
1126                     }
1127                     finally
1128                     {
1129                         colResult.close();
1130                     }
1131                 }
1132             }
1133             //make some indexes on the JettySessions table
1134             String index1 = "idx_"+_sessionTableSchema.getTableName()+"_expiry";
1135             String index2 = "idx_"+_sessionTableSchema.getTableName()+"_session";
1136 
1137             boolean index1Exists = false;
1138             boolean index2Exists = false;
1139             try (ResultSet result = metaData.getIndexInfo(null, null, tableName, false, false))
1140             {
1141                 while (result.next())
1142                 {
1143                     String idxName = result.getString("INDEX_NAME");
1144                     if (index1.equalsIgnoreCase(idxName))
1145                         index1Exists = true;
1146                     else if (index2.equalsIgnoreCase(idxName))
1147                         index2Exists = true;
1148                 }
1149             }
1150             if (!index1Exists)
1151                 statement.executeUpdate(_sessionTableSchema.getCreateIndexOverExpiryStatementAsString(index1));
1152             if (!index2Exists)
1153                 statement.executeUpdate(_sessionTableSchema.getCreateIndexOverSessionStatementAsString(index2));
1154 
1155             //set up some strings representing the statements for session manipulation
1156             _insertSession = _sessionTableSchema.getInsertSessionStatementAsString();
1157             _deleteSession = _sessionTableSchema.getDeleteSessionStatementAsString();
1158             _updateSession = _sessionTableSchema.getUpdateSessionStatementAsString();
1159             _updateSessionNode = _sessionTableSchema.getUpdateSessionNodeStatementAsString();
1160             _updateSessionAccessTime = _sessionTableSchema.getUpdateSessionAccessTimeStatementAsString();
1161             _selectBoundedExpiredSessions = _sessionTableSchema.getBoundedExpiredSessionsStatementAsString();
1162             _selectExpiredSessions = _sessionTableSchema.getSelectExpiredSessionsStatementAsString();
1163         }
1164     }
1165 
1166     /**
1167      * Insert a new used session id into the table.
1168      *
1169      * @param id
1170      * @throws SQLException
1171      */
1172     private void insert (String id)
1173     throws SQLException
1174     {
1175         try (Connection connection = getConnection();
1176                 PreparedStatement query = connection.prepareStatement(_queryId))
1177         {
1178             connection.setAutoCommit(true);
1179             query.setString(1, id);
1180             try (ResultSet result = query.executeQuery())
1181             {
1182                 //only insert the id if it isn't in the db already
1183                 if (!result.next())
1184                 {
1185                     try (PreparedStatement statement = connection.prepareStatement(_insertId))
1186                     {
1187                         statement.setString(1, id);
1188                         statement.executeUpdate();
1189                     }
1190                 }
1191             }
1192         }
1193     }
1194 
1195     /**
1196      * Remove a session id from the table.
1197      *
1198      * @param id
1199      * @throws SQLException
1200      */
1201     private void delete (String id)
1202     throws SQLException
1203     {
1204         try (Connection connection = getConnection();
1205                 PreparedStatement statement = connection.prepareStatement(_deleteId))
1206         {
1207             connection.setAutoCommit(true);
1208             statement.setString(1, id);
1209             statement.executeUpdate();
1210         }
1211     }
1212 
1213 
1214     /**
1215      * Check if a session id exists.
1216      *
1217      * @param id
1218      * @return
1219      * @throws SQLException
1220      */
1221     private boolean exists (String id)
1222     throws SQLException
1223     {
1224         try (Connection connection = getConnection();
1225                 PreparedStatement statement = connection.prepareStatement(_queryId))
1226         {
1227             connection.setAutoCommit(true);
1228             statement.setString(1, id);
1229             try (ResultSet result = statement.executeQuery())
1230             {
1231                 return result.next();
1232             }
1233         }
1234     }
1235 
1236     /**
1237      * Look for sessions in the database that have expired.
1238      *
1239      * We do this in the SessionIdManager and not the SessionManager so
1240      * that we only have 1 scavenger, otherwise if there are n SessionManagers
1241      * there would be n scavengers, all contending for the database.
1242      *
1243      * We look first for sessions that expired in the previous interval, then
1244      * for sessions that expired previously - these are old sessions that no
1245      * node is managing any more and have become stuck in the database.
1246      */
1247     private void scavenge ()
1248     {
1249         Connection connection = null;
1250         try
1251         {
1252             if (LOG.isDebugEnabled())
1253                 LOG.debug(getWorkerName()+"- Scavenge sweep started at "+System.currentTimeMillis());
1254             if (_lastScavengeTime > 0)
1255             {
1256                 connection = getConnection();
1257                 connection.setAutoCommit(true);
1258                 Set<String> expiredSessionIds = new HashSet<String>();
1259                 
1260                 
1261                 //Pass 1: find sessions for which we were last managing node that have just expired since last pass
1262                 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
1263                 long upperBound = _lastScavengeTime;
1264                 if (LOG.isDebugEnabled())
1265                     LOG.debug (getWorkerName()+"- Pass 1: Searching for sessions expired between "+lowerBound + " and "+upperBound);
1266 
1267                 try (PreparedStatement statement = connection.prepareStatement(_selectBoundedExpiredSessions))
1268                 {
1269                     statement.setString(1, getWorkerName());
1270                     statement.setLong(2, lowerBound);
1271                     statement.setLong(3, upperBound);
1272                     try (ResultSet result = statement.executeQuery())
1273                     {
1274                         while (result.next())
1275                         {
1276                             String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1277                             expiredSessionIds.add(sessionId);
1278                             if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId);
1279                         }
1280                     }
1281                 }
1282                 scavengeSessions(expiredSessionIds, false);
1283 
1284 
1285                 //Pass 2: find sessions that have expired a while ago for which this node was their last manager
1286                 try (PreparedStatement selectExpiredSessions = connection.prepareStatement(_selectExpiredSessions))
1287                 {
1288                     expiredSessionIds.clear();
1289                     upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
1290                     if (upperBound > 0)
1291                     {
1292                         if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 2: Searching for sessions expired before "+upperBound);
1293                         selectExpiredSessions.setLong(1, upperBound);
1294                         try (ResultSet result = selectExpiredSessions.executeQuery())
1295                         {
1296                             while (result.next())
1297                             {
1298                                 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1299                                 String lastNode = result.getString(_sessionTableSchema.getLastNodeColumn());
1300                                 if ((getWorkerName() == null && lastNode == null) || (getWorkerName() != null && getWorkerName().equals(lastNode)))
1301                                     expiredSessionIds.add(sessionId);
1302                                 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId+" last managed by "+getWorkerName());
1303                             }
1304                         }
1305                         scavengeSessions(expiredSessionIds, false);
1306                     }
1307 
1308 
1309                     //Pass 3:
1310                     //find all sessions that have expired at least a couple of scanIntervals ago
1311                     //if we did not succeed in loading them (eg their related context no longer exists, can't be loaded etc) then
1312                     //they are simply deleted
1313                     upperBound = _lastScavengeTime - (3 * _scavengeIntervalMs);
1314                     expiredSessionIds.clear();
1315                     if (upperBound > 0)
1316                     {
1317                         if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Pass 3: searching for sessions expired before "+upperBound);
1318                         selectExpiredSessions.setLong(1, upperBound);
1319                         try (ResultSet result = selectExpiredSessions.executeQuery())
1320                         {
1321                             while (result.next())
1322                             {
1323                                 String sessionId = result.getString(_sessionTableSchema.getIdColumn());
1324                                 expiredSessionIds.add(sessionId);
1325                                 if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId);
1326                             }
1327                         }
1328                         scavengeSessions(expiredSessionIds, true);
1329                     }
1330                 }
1331             }
1332         }
1333         catch (Exception e)
1334         {
1335             if (isRunning())
1336                 LOG.warn("Problem selecting expired sessions", e);
1337             else
1338                 LOG.ignore(e);
1339         }
1340         finally
1341         {
1342             _lastScavengeTime=System.currentTimeMillis();
1343             if (LOG.isDebugEnabled()) LOG.debug(getWorkerName()+"- Scavenge sweep ended at "+_lastScavengeTime);
1344             if (connection != null)
1345             {
1346                 try
1347                 {
1348                     connection.close();
1349                 }
1350                 catch (SQLException e)
1351                 {
1352                     LOG.warn(e);
1353                 }
1354             }
1355         }
1356     }
1357     
1358     
1359     /**
1360      * @param expiredSessionIds
1361      */
1362     private void scavengeSessions (Set<String> expiredSessionIds, boolean forceDelete)
1363     {       
1364         Set<String> remainingIds = new HashSet<String>(expiredSessionIds);
1365         Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
1366         for (int i=0; contexts!=null && i<contexts.length; i++)
1367         {
1368             SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
1369             if (sessionHandler != null)
1370             {
1371                 SessionManager manager = sessionHandler.getSessionManager();
1372                 if (manager != null && manager instanceof JDBCSessionManager)
1373                 {
1374                     Set<String> successfullyExpiredIds = ((JDBCSessionManager)manager).expire(expiredSessionIds);
1375                     if (successfullyExpiredIds != null)
1376                         remainingIds.removeAll(successfullyExpiredIds);
1377                 }
1378             }
1379         }
1380 
1381         //Any remaining ids are of those sessions that no context removed
1382         if (!remainingIds.isEmpty() && forceDelete)
1383         {
1384             LOG.info("Forcibly deleting unrecoverable expired sessions {}", remainingIds);
1385             try
1386             {
1387                 //ensure they aren't in the local list of in-use session ids
1388                 synchronized (_sessionIds)
1389                 {
1390                     _sessionIds.removeAll(remainingIds);
1391                 }
1392                 
1393                 cleanExpiredSessionIds(remainingIds);
1394             }
1395             catch (Exception e)
1396             {
1397                 LOG.warn("Error removing expired session ids", e);
1398             }
1399         }
1400     }
1401 
1402 
1403    
1404     
1405     private void cleanExpiredSessionIds (Set<String> expiredIds)
1406     throws Exception
1407     {
1408         if (expiredIds == null || expiredIds.isEmpty())
1409             return;
1410 
1411         String[] ids = expiredIds.toArray(new String[expiredIds.size()]);
1412         try (Connection con = getConnection())
1413         {
1414             con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
1415             con.setAutoCommit(false);
1416 
1417             int start = 0;
1418             int end = 0;
1419             int blocksize = _deleteBlockSize;
1420             int block = 0;
1421        
1422             try (Statement statement = con.createStatement())
1423             {
1424                 while (end < ids.length)
1425                 {
1426                     start = block*blocksize;
1427                     if ((ids.length -  start)  >= blocksize)
1428                         end = start + blocksize;
1429                     else
1430                         end = ids.length;
1431 
1432                     //take them out of the sessionIds table
1433                     statement.executeUpdate(fillInClause("delete from "+_sessionIdTableSchema.getTableName()+" where "+_sessionIdTableSchema.getIdColumn()+" in ", ids, start, end));
1434                     //take them out of the sessions table
1435                     statement.executeUpdate(fillInClause("delete from "+_sessionTableSchema.getTableName()+" where "+_sessionTableSchema.getIdColumn()+" in ", ids, start, end));
1436                     block++;
1437                 }
1438             }
1439             catch (Exception e)
1440             {
1441                 con.rollback();
1442                 throw e;
1443             }
1444             con.commit();
1445         }
1446     }
1447 
1448     
1449     
1450     /**
1451      * 
1452      * @param sql
1453      * @param atoms
1454      * @throws Exception
1455      */
1456     private String fillInClause (String sql, String[] literals, int start, int end)
1457     throws Exception
1458     {
1459         StringBuffer buff = new StringBuffer();
1460         buff.append(sql);
1461         buff.append("(");
1462         for (int i=start; i<end; i++)
1463         {
1464             buff.append("'"+(literals[i])+"'");
1465             if (i+1<end)
1466                 buff.append(",");
1467         }
1468         buff.append(")");
1469         return buff.toString();
1470     }
1471     
1472     
1473     
1474     private void initializeDatabase ()
1475     throws Exception
1476     {
1477         if (_datasource != null)
1478             return; //already set up
1479         
1480         if (_jndiName!=null)
1481         {
1482             InitialContext ic = new InitialContext();
1483             _datasource = (DataSource)ic.lookup(_jndiName);
1484         }
1485         else if ( _driver != null && _connectionUrl != null )
1486         {
1487             DriverManager.registerDriver(_driver);
1488         }
1489         else if (_driverClassName != null && _connectionUrl != null)
1490         {
1491             Class.forName(_driverClassName);
1492         }
1493         else
1494             throw new IllegalStateException("No database configured for sessions");
1495     }
1496     
1497    
1498 }