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