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