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