View Javadoc

1   // ========================================================================
2   // Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  
14  package org.eclipse.jetty.server.session;
15  
16  import java.io.ByteArrayInputStream;
17  import java.io.InputStream;
18  import java.sql.Blob;
19  import java.sql.Connection;
20  import java.sql.DatabaseMetaData;
21  import java.sql.Driver;
22  import java.sql.DriverManager;
23  import java.sql.PreparedStatement;
24  import java.sql.ResultSet;
25  import java.sql.SQLException;
26  import java.sql.Statement;
27  import java.util.ArrayList;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Random;
31  import java.util.Timer;
32  import java.util.TimerTask;
33  
34  import javax.naming.InitialContext;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpSession;
37  import javax.sql.DataSource;
38  
39  import org.eclipse.jetty.server.Handler;
40  import org.eclipse.jetty.server.Server;
41  import org.eclipse.jetty.server.SessionManager;
42  import org.eclipse.jetty.server.handler.ContextHandler;
43  import org.eclipse.jetty.util.log.Logger;
44  
45  
46  
47  /**
48   * JDBCSessionIdManager
49   *
50   * SessionIdManager implementation that uses a database to store in-use session ids, 
51   * to support distributed sessions.
52   * 
53   */
54  public class JDBCSessionIdManager extends AbstractSessionIdManager
55  {    
56      final static Logger LOG = SessionHandler.LOG;
57      
58      protected final HashSet<String> _sessionIds = new HashSet<String>();
59      protected Server _server;
60      protected Driver _driver;
61      protected String _driverClassName;
62      protected String _connectionUrl;
63      protected DataSource _datasource;
64      protected String _jndiName;
65      protected String _sessionIdTable = "JettySessionIds";
66      protected String _sessionTable = "JettySessions";
67      protected String _sessionTableRowId = "rowId";
68      
69      protected Timer _timer; //scavenge timer
70      protected TimerTask _task; //scavenge task
71      protected long _lastScavengeTime;
72      protected long _scavengeIntervalMs = 1000 * 60 * 10; //10mins
73      protected String _blobType; //if not set, is deduced from the type of the database at runtime
74      
75      protected String _createSessionIdTable;
76      protected String _createSessionTable;
77                                              
78      protected String _selectExpiredSessions;
79      protected String _deleteOldExpiredSessions;
80  
81      protected String _insertId;
82      protected String _deleteId;
83      protected String _queryId;
84      
85      protected DatabaseAdaptor _dbAdaptor;
86  
87      
88      /**
89       * DatabaseAdaptor
90       *
91       * Handles differences between databases.
92       * 
93       * Postgres uses the getBytes and setBinaryStream methods to access
94       * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL
95       * is happy to use the "blob" type and getBlob() methods instead.
96       * 
97       * TODO if the differences become more major it would be worthwhile
98       * refactoring this class.
99       */
100     public class DatabaseAdaptor 
101     {
102         String _dbName;
103         boolean _isLower;
104         boolean _isUpper;
105         
106         
107         public DatabaseAdaptor (DatabaseMetaData dbMeta)
108         throws SQLException
109         {
110             _dbName = dbMeta.getDatabaseProductName().toLowerCase(); 
111             LOG.debug ("Using database {}",_dbName);
112             _isLower = dbMeta.storesLowerCaseIdentifiers();
113             _isUpper = dbMeta.storesUpperCaseIdentifiers();
114         }
115         
116         /**
117          * Convert a camel case identifier into either upper or lower
118          * depending on the way the db stores identifiers.
119          * 
120          * @param identifier
121          * @return the converted identifier
122          */
123         public String convertIdentifier (String identifier)
124         {
125             if (_isLower)
126                 return identifier.toLowerCase();
127             if (_isUpper)
128                 return identifier.toUpperCase();
129             
130             return identifier;
131         }
132         
133         public String getDBName ()
134         {
135             return _dbName;
136         }
137         
138         public String getBlobType ()
139         {
140             if (_blobType != null)
141                 return _blobType;
142             
143             if (_dbName.startsWith("postgres"))
144                 return "bytea";
145             
146             return "blob";
147         }
148         
149         public InputStream getBlobInputStream (ResultSet result, String columnName)
150         throws SQLException
151         {
152             if (_dbName.startsWith("postgres"))
153             {
154                 byte[] bytes = result.getBytes(columnName);
155                 return new ByteArrayInputStream(bytes);
156             }
157             
158             Blob blob = result.getBlob(columnName);
159             return blob.getBinaryStream();
160         }
161     }
162     
163     
164     
165     public JDBCSessionIdManager(Server server)
166     {
167         super();
168         _server=server;
169     }
170     
171     public JDBCSessionIdManager(Server server, Random random)
172     {
173        super(random);
174        _server=server;
175     }
176 
177     /**
178      * Configure jdbc connection information via a jdbc Driver
179      * 
180      * @param driverClassName
181      * @param connectionUrl
182      */
183     public void setDriverInfo (String driverClassName, String connectionUrl)
184     {
185         _driverClassName=driverClassName;
186         _connectionUrl=connectionUrl;
187     }
188     
189     /**
190      * Configure jdbc connection information via a jdbc Driver
191      * 
192      * @param driverClass
193      * @param connectionUrl
194      */
195     public void setDriverInfo (Driver driverClass, String connectionUrl)
196     {
197         _driver=driverClass;
198         _connectionUrl=connectionUrl;
199     }
200     
201     
202     public void setDatasource (DataSource ds)
203     {
204         _datasource = ds;
205     }
206     
207     public DataSource getDataSource ()
208     {
209         return _datasource;
210     }
211     
212     public String getDriverClassName()
213     {
214         return _driverClassName;
215     }
216     
217     public String getConnectionUrl ()
218     {
219         return _connectionUrl;
220     }
221     
222     public void setDatasourceName (String jndi)
223     {
224         _jndiName=jndi;
225     }
226     
227     public String getDatasourceName ()
228     {
229         return _jndiName;
230     }
231    
232     public void setBlobType (String name)
233     {
234         _blobType = name;
235     }
236     
237     public String getBlobType ()
238     {
239         return _blobType;
240     }
241     
242     public void setScavengeInterval (long sec)
243     {
244         if (sec<=0)
245             sec=60;
246 
247         long old_period=_scavengeIntervalMs;
248         long period=sec*1000;
249       
250         _scavengeIntervalMs=period;
251         
252         //add a bit of variability into the scavenge time so that not all
253         //nodes with the same scavenge time sync up
254         long tenPercent = _scavengeIntervalMs/10;
255         if ((System.currentTimeMillis()%2) == 0)
256             _scavengeIntervalMs += tenPercent;
257         
258         if (LOG.isDebugEnabled()) 
259             LOG.debug("Scavenging every "+_scavengeIntervalMs+" ms");
260         if (_timer!=null && (period!=old_period || _task==null))
261         {
262             synchronized (this)
263             {
264                 if (_task!=null)
265                     _task.cancel();
266                 _task = new TimerTask()
267                 {
268                     @Override
269                     public void run()
270                     {
271                         scavenge();
272                     }   
273                 };
274                 _timer.schedule(_task,_scavengeIntervalMs,_scavengeIntervalMs);
275             }
276         }  
277     }
278     
279     public long getScavengeInterval ()
280     {
281         return _scavengeIntervalMs/1000;
282     }
283     
284     
285     public void addSession(HttpSession session)
286     {
287         if (session == null)
288             return;
289         
290         synchronized (_sessionIds)
291         {
292             String id = ((JDBCSessionManager.Session)session).getClusterId();            
293             try
294             {
295                 insert(id);
296                 _sessionIds.add(id);
297             }
298             catch (Exception e)
299             {
300                 LOG.warn("Problem storing session id="+id, e);
301             }
302         }
303     }
304     
305     public void removeSession(HttpSession session)
306     {
307         if (session == null)
308             return;
309         
310         removeSession(((JDBCSessionManager.Session)session).getClusterId());
311     }
312     
313     
314     
315     public void removeSession (String id)
316     {
317 
318         if (id == null)
319             return;
320         
321         synchronized (_sessionIds)
322         {  
323             if (LOG.isDebugEnabled())
324                 LOG.debug("Removing session id="+id);
325             try
326             {               
327                 _sessionIds.remove(id);
328                 delete(id);
329             }
330             catch (Exception e)
331             {
332                 LOG.warn("Problem removing session id="+id, e);
333             }
334         }
335         
336     }
337     
338 
339     /** 
340      * Get the session id without any node identifier suffix.
341      * 
342      * @see org.eclipse.jetty.server.SessionIdManager#getClusterId(java.lang.String)
343      */
344     public String getClusterId(String nodeId)
345     {
346         int dot=nodeId.lastIndexOf('.');
347         return (dot>0)?nodeId.substring(0,dot):nodeId;
348     }
349     
350 
351     /** 
352      * Get the session id, including this node's id as a suffix.
353      * 
354      * @see org.eclipse.jetty.server.SessionIdManager#getNodeId(java.lang.String, javax.servlet.http.HttpServletRequest)
355      */
356     public String getNodeId(String clusterId, HttpServletRequest request)
357     {
358         if (_workerName!=null)
359             return clusterId+'.'+_workerName;
360 
361         return clusterId;
362     }
363 
364 
365     public boolean idInUse(String id)
366     {
367         if (id == null)
368             return false;
369         
370         String clusterId = getClusterId(id);
371         boolean inUse = false;
372         synchronized (_sessionIds)
373         {
374             inUse = _sessionIds.contains(clusterId);
375         }
376         
377         if (inUse)
378             return true; //optimisation - if this session is one we've been managing, we can check locally
379 
380         //otherwise, we need to go to the database to check
381         try
382         {
383             return exists(clusterId);
384         }
385         catch (Exception e)
386         {
387             LOG.warn("Problem checking inUse for id="+clusterId, e);
388             return false;
389         }
390     }
391 
392     /** 
393      * Invalidate the session matching the id on all contexts.
394      * 
395      * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String)
396      */
397     public void invalidateAll(String id)
398     {            
399         //take the id out of the list of known sessionids for this node
400         removeSession(id);
401         
402         synchronized (_sessionIds)
403         {
404             //tell all contexts that may have a session object with this id to
405             //get rid of them
406             Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
407             for (int i=0; contexts!=null && i<contexts.length; i++)
408             {
409                 SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
410                 if (sessionHandler != null) 
411                 {
412                     SessionManager manager = sessionHandler.getSessionManager();
413 
414                     if (manager != null && manager instanceof JDBCSessionManager)
415                     {
416                         ((JDBCSessionManager)manager).invalidateSession(id);
417                     }
418                 }
419             }
420         }
421     }
422 
423 
424     /** 
425      * Start up the id manager.
426      * 
427      * Makes necessary database tables and starts a Session
428      * scavenger thread.
429      */
430     @Override
431     public void doStart()
432     {
433         try
434         {            
435             initializeDatabase();
436             prepareTables();        
437             super.doStart();
438             if (LOG.isDebugEnabled()) 
439                 LOG.debug("Scavenging interval = "+getScavengeInterval()+" sec");
440             _timer=new Timer("JDBCSessionScavenger", true);
441             setScavengeInterval(getScavengeInterval());
442         }
443         catch (Exception e)
444         {
445             LOG.warn("Problem initialising JettySessionIds table", e);
446         }
447     }
448     
449     /** 
450      * Stop the scavenger.
451      */
452     @Override
453     public void doStop () 
454     throws Exception
455     {
456         synchronized(this)
457         {
458             if (_task!=null)
459                 _task.cancel();
460             if (_timer!=null)
461                 _timer.cancel();
462             _timer=null;
463         }
464         super.doStop();
465     }
466   
467     /**
468      * Get a connection from the driver or datasource.
469      * 
470      * @return the connection for the datasource
471      * @throws SQLException
472      */
473     protected Connection getConnection ()
474     throws SQLException
475     {
476         if (_datasource != null)
477             return _datasource.getConnection();
478         else
479             return DriverManager.getConnection(_connectionUrl);
480     }
481 
482     
483     private void initializeDatabase ()
484     throws Exception
485     {
486         if (_datasource != null)
487             return; //already set up
488         
489         if (_jndiName!=null)
490         {
491             InitialContext ic = new InitialContext();
492             _datasource = (DataSource)ic.lookup(_jndiName);
493         }
494         else if ( _driver != null && _connectionUrl != null )
495         {
496             DriverManager.registerDriver(_driver);
497         }
498         else if (_driverClassName != null && _connectionUrl != null)
499         {
500             Class.forName(_driverClassName);
501         }
502         else
503             throw new IllegalStateException("No database configured for sessions");
504     }
505     
506     
507     
508     /**
509      * Set up the tables in the database
510      * @throws SQLException
511      */
512     private void prepareTables()
513     throws SQLException
514     {
515         _createSessionIdTable = "create table "+_sessionIdTable+" (id varchar(120), primary key(id))";
516         _selectExpiredSessions = "select * from "+_sessionTable+" where expiryTime >= ? and expiryTime <= ?";
517         _deleteOldExpiredSessions = "delete from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?";
518 
519         _insertId = "insert into "+_sessionIdTable+" (id)  values (?)";
520         _deleteId = "delete from "+_sessionIdTable+" where id = ?";
521         _queryId = "select * from "+_sessionIdTable+" where id = ?";
522 
523         Connection connection = null;
524         try
525         {
526             //make the id table
527             connection = getConnection();
528             connection.setAutoCommit(true);
529             DatabaseMetaData metaData = connection.getMetaData();
530             _dbAdaptor = new DatabaseAdaptor(metaData);
531             _sessionTableRowId = (_dbAdaptor.getDBName() != null && _dbAdaptor.getDBName().contains("oracle") ? "srowId":_sessionTableRowId);
532 
533             //checking for table existence is case-sensitive, but table creation is not
534             String tableName = _dbAdaptor.convertIdentifier(_sessionIdTable);
535             ResultSet result = metaData.getTables(null, null, tableName, null);
536             if (!result.next())
537             {
538                 //table does not exist, so create it
539                 connection.createStatement().executeUpdate(_createSessionIdTable);
540             }
541             
542             //make the session table if necessary
543             tableName = _dbAdaptor.convertIdentifier(_sessionTable);   
544             result = metaData.getTables(null, null, tableName, null);
545             if (!result.next())
546             {
547                 //table does not exist, so create it
548                 String blobType = _dbAdaptor.getBlobType();
549                 _createSessionTable = "create table "+_sessionTable+" ("+_sessionTableRowId+" varchar(120), sessionId varchar(120), "+
550                                            " contextPath varchar(60), virtualHost varchar(60), lastNode varchar(60), accessTime bigint, "+
551                                            " lastAccessTime bigint, createTime bigint, cookieTime bigint, "+
552                                            " lastSavedTime bigint, expiryTime bigint, map "+blobType+", primary key("+_sessionTableRowId+"))";
553                 connection.createStatement().executeUpdate(_createSessionTable);
554             }
555             
556             //make some indexes on the JettySessions table
557             String index1 = "idx_"+_sessionTable+"_expiry";
558             String index2 = "idx_"+_sessionTable+"_session";
559             
560             result = metaData.getIndexInfo(null, null, tableName, false, false);
561             boolean index1Exists = false;
562             boolean index2Exists = false;
563             while (result.next())
564             {
565                 String idxName = result.getString("INDEX_NAME");
566                 if (index1.equalsIgnoreCase(idxName))
567                     index1Exists = true;
568                 else if (index2.equalsIgnoreCase(idxName))
569                     index2Exists = true;
570             }
571             if (!(index1Exists && index2Exists))
572             {
573                 Statement statement = connection.createStatement();
574                 if (!index1Exists)
575                     statement.executeUpdate("create index "+index1+" on "+_sessionTable+" (expiryTime)");
576                 if (!index2Exists)
577                     statement.executeUpdate("create index "+index2+" on "+_sessionTable+" (sessionId, contextPath)");
578             }
579         }
580         finally
581         {
582             if (connection != null)
583                 connection.close();
584         }
585     }
586     
587     /**
588      * Insert a new used session id into the table.
589      * 
590      * @param id
591      * @throws SQLException
592      */
593     private void insert (String id)
594     throws SQLException 
595     {
596         Connection connection = null;
597         try
598         {
599             connection = getConnection();
600             connection.setAutoCommit(true);            
601             PreparedStatement query = connection.prepareStatement(_queryId);
602             query.setString(1, id);
603             ResultSet result = query.executeQuery();
604             //only insert the id if it isn't in the db already 
605             if (!result.next())
606             {
607                 PreparedStatement statement = connection.prepareStatement(_insertId);
608                 statement.setString(1, id);
609                 statement.executeUpdate();
610             }
611         }
612         finally
613         {
614             if (connection != null)
615                 connection.close();
616         }
617     }
618     
619     /**
620      * Remove a session id from the table.
621      * 
622      * @param id
623      * @throws SQLException
624      */
625     private void delete (String id)
626     throws SQLException
627     {
628         Connection connection = null;
629         try
630         {
631             connection = getConnection();
632             connection.setAutoCommit(true);
633             PreparedStatement statement = connection.prepareStatement(_deleteId);
634             statement.setString(1, id);
635             statement.executeUpdate();
636         }
637         finally
638         {
639             if (connection != null)
640                 connection.close();
641         }
642     }
643     
644     
645     /**
646      * Check if a session id exists.
647      * 
648      * @param id
649      * @return
650      * @throws SQLException
651      */
652     private boolean exists (String id)
653     throws SQLException
654     {
655         Connection connection = null;
656         try
657         {
658             connection = getConnection();
659             connection.setAutoCommit(true);
660             PreparedStatement statement = connection.prepareStatement(_queryId);
661             statement.setString(1, id);
662             ResultSet result = statement.executeQuery();
663             return result.next();
664         }
665         finally
666         {
667             if (connection != null)
668                 connection.close();
669         }
670     }
671     
672     /**
673      * Look for sessions in the database that have expired.
674      * 
675      * We do this in the SessionIdManager and not the SessionManager so
676      * that we only have 1 scavenger, otherwise if there are n SessionManagers
677      * there would be n scavengers, all contending for the database.
678      * 
679      * We look first for sessions that expired in the previous interval, then
680      * for sessions that expired previously - these are old sessions that no
681      * node is managing any more and have become stuck in the database.
682      */
683     private void scavenge ()
684     {
685         Connection connection = null;
686         List<String> expiredSessionIds = new ArrayList<String>();
687         try
688         {            
689             if (LOG.isDebugEnabled()) 
690                 LOG.debug("Scavenge sweep started at "+System.currentTimeMillis());
691             if (_lastScavengeTime > 0)
692             {
693                 connection = getConnection();
694                 connection.setAutoCommit(true);
695                 //"select sessionId from JettySessions where expiryTime > (lastScavengeTime - scanInterval) and expiryTime < lastScavengeTime";
696                 PreparedStatement statement = connection.prepareStatement(_selectExpiredSessions);
697                 long lowerBound = (_lastScavengeTime - _scavengeIntervalMs);
698                 long upperBound = _lastScavengeTime;
699                 if (LOG.isDebugEnabled()) 
700                     LOG.debug (" Searching for sessions expired between "+lowerBound + " and "+upperBound);
701                 
702                 statement.setLong(1, lowerBound);
703                 statement.setLong(2, upperBound);
704                 ResultSet result = statement.executeQuery();
705                 while (result.next())
706                 {
707                     String sessionId = result.getString("sessionId");
708                     expiredSessionIds.add(sessionId);
709                     if (LOG.isDebugEnabled()) LOG.debug (" Found expired sessionId="+sessionId); 
710                 }
711 
712                 //tell the SessionManagers to expire any sessions with a matching sessionId in memory
713                 Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class);
714                 for (int i=0; contexts!=null && i<contexts.length; i++)
715                 {
716 
717                     SessionHandler sessionHandler = (SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class);
718                     if (sessionHandler != null) 
719                     { 
720                         SessionManager manager = sessionHandler.getSessionManager();
721                         if (manager != null && manager instanceof JDBCSessionManager)
722                         {
723                             ((JDBCSessionManager)manager).expire(expiredSessionIds);
724                         }
725                     }
726                 }
727 
728                 //find all sessions that have expired at least a couple of scanIntervals ago and just delete them
729                 upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs);
730                 if (upperBound > 0)
731                 {
732                     if (LOG.isDebugEnabled()) LOG.debug("Deleting old expired sessions expired before "+upperBound);
733                     statement = connection.prepareStatement(_deleteOldExpiredSessions);
734                     statement.setLong(1, upperBound);
735                     statement.executeUpdate();
736                 }
737             }
738         }
739         catch (Exception e)
740         {
741             if (isRunning())    
742                 LOG.warn("Problem selecting expired sessions", e);
743             else
744                 LOG.ignore(e);
745         }
746         finally
747         {           
748             _lastScavengeTime=System.currentTimeMillis();
749             if (LOG.isDebugEnabled()) LOG.debug("Scavenge sweep ended at "+_lastScavengeTime);
750             if (connection != null)
751             {
752                 try
753                 {
754                 connection.close();
755                 }
756                 catch (SQLException e)
757                 {
758                     LOG.warn(e);
759                 }
760             }
761         }
762     }
763 }