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 19package org.eclipse.jetty.server.session; 20 21import java.io.DataInputStream; 22import java.io.File; 23import java.io.FileInputStream; 24import java.io.IOException; 25import java.io.InputStream; 26import java.io.ObjectInputStream; 27import java.net.URI; 28import java.util.ArrayList; 29import java.util.Iterator; 30import java.util.Map; 31import java.util.Set; 32import java.util.Timer; 33import java.util.TimerTask; 34import java.util.concurrent.ConcurrentHashMap; 35import java.util.concurrent.ConcurrentMap; 36 37import javax.servlet.ServletContext; 38import javax.servlet.http.HttpServletRequest; 39 40import org.eclipse.jetty.server.handler.ContextHandler; 41import org.eclipse.jetty.util.IO; 42import org.eclipse.jetty.util.log.Logger; 43 44 45/* ------------------------------------------------------------ */ 46/** 47 * HashSessionManager 48 * 49 * An in-memory implementation of SessionManager. 50 * <p> 51 * This manager supports saving sessions to disk, either periodically or at shutdown. 52 * Sessions can also have their content idle saved to disk to reduce the memory overheads of large idle sessions. 53 * <p> 54 * This manager will create it's own Timer instance to scavenge threads, unless it discovers a shared Timer instance 55 * set as the "org.eclipse.jetty.server.session.timer" attribute of the ContextHandler. 56 * 57 */ 58public class HashSessionManager extends AbstractSessionManager 59{ 60 final static Logger __log = SessionHandler.LOG; 61 62 protected final ConcurrentMap<String,HashedSession> _sessions=new ConcurrentHashMap<String,HashedSession>(); 63 private static int __id; 64 private Timer _timer; 65 private boolean _timerStop=false; 66 private TimerTask _task; 67 long _scavengePeriodMs=30000; 68 long _savePeriodMs=0; //don't do period saves by default 69 long _idleSavePeriodMs = 0; // don't idle save sessions by default. 70 private TimerTask _saveTask; 71 File _storeDir; 72 private boolean _lazyLoad=false; 73 private volatile boolean _sessionsLoaded=false; 74 private boolean _deleteUnrestorableSessions=false; 75 76 77 78 79 /* ------------------------------------------------------------ */ 80 public HashSessionManager() 81 { 82 super(); 83 } 84 85 /* ------------------------------------------------------------ */ 86 /** 87 * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStart() 88 */ 89 @Override 90 public void doStart() throws Exception 91 { 92 super.doStart(); 93 94 _timerStop=false; 95 ServletContext context = ContextHandler.getCurrentContext(); 96 if (context!=null) 97 _timer=(Timer)context.getAttribute("org.eclipse.jetty.server.session.timer"); 98 if (_timer==null) 99 { 100 _timerStop=true; 101 _timer=new Timer("HashSessionScavenger-"+__id++, true); 102 } 103 104 setScavengePeriod(getScavengePeriod()); 105 106 if (_storeDir!=null) 107 { 108 if (!_storeDir.exists()) 109 _storeDir.mkdirs(); 110 111 if (!_lazyLoad) 112 restoreSessions(); 113 } 114 115 setSavePeriod(getSavePeriod()); 116 } 117 118 /* ------------------------------------------------------------ */ 119 /** 120 * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStop() 121 */ 122 @Override 123 public void doStop() throws Exception 124 { 125 // stop the scavengers 126 synchronized(this) 127 { 128 if (_saveTask!=null) 129 _saveTask.cancel(); 130 _saveTask=null; 131 if (_task!=null) 132 _task.cancel(); 133 _task=null; 134 if (_timer!=null && _timerStop) 135 _timer.cancel(); 136 _timer=null; 137 } 138 139 // This will callback invalidate sessions - where we decide if we will save 140 super.doStop(); 141 142 _sessions.clear(); 143 144 } 145 146 /* ------------------------------------------------------------ */ 147 /** 148 * @return the period in seconds at which a check is made for sessions to be invalidated. 149 */ 150 public int getScavengePeriod() 151 { 152 return (int)(_scavengePeriodMs/1000); 153 } 154 155 156 /* ------------------------------------------------------------ */ 157 @Override 158 public int getSessions() 159 { 160 int sessions=super.getSessions(); 161 if (__log.isDebugEnabled()) 162 { 163 if (_sessions.size()!=sessions) 164 __log.warn("sessions: "+_sessions.size()+"!="+sessions); 165 } 166 return sessions; 167 } 168 169 /* ------------------------------------------------------------ */ 170 /** 171 * @return seconds Idle period after which a session is saved 172 */ 173 public int getIdleSavePeriod() 174 { 175 if (_idleSavePeriodMs <= 0) 176 return 0; 177 178 return (int)(_idleSavePeriodMs / 1000); 179 } 180 181 /* ------------------------------------------------------------ */ 182 /** 183 * Configures the period in seconds after which a session is deemed idle and saved 184 * to save on session memory. 185 * 186 * The session is persisted, the values attribute map is cleared and the session set to idled. 187 * 188 * @param seconds Idle period after which a session is saved 189 */ 190 public void setIdleSavePeriod(int seconds) 191 { 192 _idleSavePeriodMs = seconds * 1000L; 193 } 194 195 /* ------------------------------------------------------------ */ 196 @Override 197 public void setMaxInactiveInterval(int seconds) 198 { 199 super.setMaxInactiveInterval(seconds); 200 if (_dftMaxIdleSecs>0&&_scavengePeriodMs>_dftMaxIdleSecs*1000L) 201 setScavengePeriod((_dftMaxIdleSecs+9)/10); 202 } 203 204 /* ------------------------------------------------------------ */ 205 /** 206 * @param seconds the period is seconds at which sessions are periodically saved to disk 207 */ 208 public void setSavePeriod (int seconds) 209 { 210 long period = (seconds * 1000L); 211 if (period < 0) 212 period=0; 213 _savePeriodMs=period; 214 215 if (_timer!=null) 216 { 217 synchronized (this) 218 { 219 if (_saveTask!=null) 220 _saveTask.cancel(); 221 if (_savePeriodMs > 0 && _storeDir!=null) //only save if we have a directory configured 222 { 223 _saveTask = new TimerTask() 224 { 225 @Override 226 public void run() 227 { 228 try 229 { 230 saveSessions(true); 231 } 232 catch (Exception e) 233 { 234 __log.warn(e); 235 } 236 } 237 }; 238 _timer.schedule(_saveTask,_savePeriodMs,_savePeriodMs); 239 } 240 } 241 } 242 } 243 244 /* ------------------------------------------------------------ */ 245 /** 246 * @return the period in seconds at which sessions are periodically saved to disk 247 */ 248 public int getSavePeriod () 249 { 250 if (_savePeriodMs<=0) 251 return 0; 252 253 return (int)(_savePeriodMs/1000); 254 } 255 256 /* ------------------------------------------------------------ */ 257 /** 258 * @param seconds the period in seconds at which a check is made for sessions to be invalidated. 259 */ 260 public void setScavengePeriod(int seconds) 261 { 262 if (seconds==0) 263 seconds=60; 264 265 long old_period=_scavengePeriodMs; 266 long period=seconds*1000L; 267 if (period>60000) 268 period=60000; 269 if (period<1000) 270 period=1000; 271 272 _scavengePeriodMs=period; 273 274 if (_timer!=null && (period!=old_period || _task==null)) 275 { 276 synchronized (this) 277 { 278 if (_task!=null) 279 _task.cancel(); 280 _task = new TimerTask() 281 { 282 @Override 283 public void run() 284 { 285 scavenge(); 286 } 287 }; 288 _timer.schedule(_task,_scavengePeriodMs,_scavengePeriodMs); 289 } 290 } 291 } 292 293 /* -------------------------------------------------------------- */ 294 /** 295 * Find sessions that have timed out and invalidate them. This runs in the 296 * SessionScavenger thread. 297 */ 298 protected void scavenge() 299 { 300 //don't attempt to scavenge if we are shutting down 301 if (isStopping() || isStopped()) 302 return; 303 304 Thread thread=Thread.currentThread(); 305 ClassLoader old_loader=thread.getContextClassLoader(); 306 try 307 { 308 if (_loader!=null) 309 thread.setContextClassLoader(_loader); 310 311 // For each session 312 long now=System.currentTimeMillis(); 313 314 for (Iterator<HashedSession> i=_sessions.values().iterator(); i.hasNext();) 315 { 316 HashedSession session=i.next(); 317 long idleTime=session.getMaxInactiveInterval()*1000L; 318 if (idleTime>0&&session.getAccessed()+idleTime<now) 319 { 320 // Found a stale session, add it to the list 321 try 322 { 323 session.timeout(); 324 } 325 catch (Exception e) 326 { 327 __log.warn("Problem scavenging sessions", e); 328 } 329 } 330 else if (_idleSavePeriodMs > 0 && session.getAccessed()+_idleSavePeriodMs < now) 331 { 332 try 333 { 334 session.idle(); 335 } 336 catch (Exception e) 337 { 338 __log.warn("Problem idling session "+ session.getId(), e); 339 } 340 } 341 } 342 } 343 finally 344 { 345 thread.setContextClassLoader(old_loader); 346 } 347 } 348 349 /* ------------------------------------------------------------ */ 350 @Override 351 protected void addSession(AbstractSession session) 352 { 353 if (isRunning()) 354 _sessions.put(session.getClusterId(),(HashedSession)session); 355 } 356 357 /* ------------------------------------------------------------ */ 358 @Override 359 public AbstractSession getSession(String idInCluster) 360 { 361 if ( _lazyLoad && !_sessionsLoaded) 362 { 363 try 364 { 365 restoreSessions(); 366 } 367 catch(Exception e) 368 { 369 __log.warn(e); 370 } 371 } 372 373 Map<String,HashedSession> sessions=_sessions; 374 if (sessions==null) 375 return null; 376 377 HashedSession session = sessions.get(idInCluster); 378 379 if (session == null && _lazyLoad) 380 session=restoreSession(idInCluster); 381 if (session == null) 382 return null; 383 384 if (_idleSavePeriodMs!=0) 385 session.deIdle(); 386 387 return session; 388 } 389 390 /* ------------------------------------------------------------ */ 391 @Override 392 protected void invalidateSessions() throws Exception 393 { 394 // Invalidate all sessions to cause unbind events 395 ArrayList<HashedSession> sessions=new ArrayList<HashedSession>(_sessions.values()); 396 int loop=100; 397 while (sessions.size()>0 && loop-->0) 398 { 399 // If we are called from doStop 400 if (isStopping() && _storeDir != null && _storeDir.exists() && _storeDir.canWrite()) 401 { 402 // Then we only save and remove the session - it is not invalidated. 403 for (HashedSession session : sessions) 404 { 405 session.save(false); 406 removeSession(session,false); 407 } 408 } 409 else 410 { 411 for (HashedSession session : sessions) 412 session.invalidate(); 413 } 414 415 // check that no new sessions were created while we were iterating 416 sessions=new ArrayList<HashedSession>(_sessions.values()); 417 } 418 } 419 420 /* ------------------------------------------------------------ */ 421 @Override 422 protected AbstractSession newSession(HttpServletRequest request) 423 { 424 return new HashedSession(this, request); 425 } 426 427 /* ------------------------------------------------------------ */ 428 protected AbstractSession newSession(long created, long accessed, String clusterId) 429 { 430 return new HashedSession(this, created,accessed, clusterId); 431 } 432 433 /* ------------------------------------------------------------ */ 434 @Override 435 protected boolean removeSession(String clusterId) 436 { 437 return _sessions.remove(clusterId)!=null; 438 } 439 440 /* ------------------------------------------------------------ */ 441 public void setStoreDirectory (File dir) throws IOException 442 { 443 // CanonicalFile is used to capture the base store directory in a way that will 444 // work on Windows. Case differences may through off later checks using this directory. 445 _storeDir=dir.getCanonicalFile(); 446 } 447 448 /* ------------------------------------------------------------ */ 449 public File getStoreDirectory () 450 { 451 return _storeDir; 452 } 453 454 /* ------------------------------------------------------------ */ 455 public void setLazyLoad(boolean lazyLoad) 456 { 457 _lazyLoad = lazyLoad; 458 } 459 460 /* ------------------------------------------------------------ */ 461 public boolean isLazyLoad() 462 { 463 return _lazyLoad; 464 } 465 466 /* ------------------------------------------------------------ */ 467 public boolean isDeleteUnrestorableSessions() 468 { 469 return _deleteUnrestorableSessions; 470 } 471 472 /* ------------------------------------------------------------ */ 473 public void setDeleteUnrestorableSessions(boolean deleteUnrestorableSessions) 474 { 475 _deleteUnrestorableSessions = deleteUnrestorableSessions; 476 } 477 478 /* ------------------------------------------------------------ */ 479 public void restoreSessions () throws Exception 480 { 481 _sessionsLoaded = true; 482 483 if (_storeDir==null || !_storeDir.exists()) 484 { 485 return; 486 } 487 488 if (!_storeDir.canRead()) 489 { 490 __log.warn ("Unable to restore Sessions: Cannot read from Session storage directory "+_storeDir.getAbsolutePath()); 491 return; 492 } 493 494 String[] files = _storeDir.list(); 495 for (int i=0;files!=null&&i<files.length;i++) 496 { 497 restoreSession(files[i]); 498 } 499 } 500 501 /* ------------------------------------------------------------ */ 502 protected synchronized HashedSession restoreSession(String idInCuster) 503 { 504 File file = new File(_storeDir,idInCuster); 505 506 FileInputStream in = null; 507 Exception error = null; 508 try 509 { 510 if (file.exists()) 511 { 512 in = new FileInputStream(file); 513 HashedSession session = restoreSession(in, null); 514 addSession(session, false); 515 session.didActivate(); 516 return session; 517 } 518 } 519 catch (Exception e) 520 { 521 error = e; 522 } 523 finally 524 { 525 if (in != null) IO.close(in); 526 527 if (error != null) 528 { 529 if (isDeleteUnrestorableSessions() && file.exists() && file.getParentFile().equals(_storeDir) ) 530 { 531 file.delete(); 532 __log.warn("Deleting file for unrestorable session "+idInCuster, error); 533 } 534 else 535 { 536 __log.warn("Problem restoring session "+idInCuster, error); 537 } 538 } 539 else 540 file.delete(); //delete successfully restored file 541 542 } 543 return null; 544 } 545 546 /* ------------------------------------------------------------ */ 547 public void saveSessions(boolean reactivate) throws Exception 548 { 549 if (_storeDir==null || !_storeDir.exists()) 550 { 551 return; 552 } 553 554 if (!_storeDir.canWrite()) 555 { 556 __log.warn ("Unable to save Sessions: Session persistence storage directory "+_storeDir.getAbsolutePath()+ " is not writeable"); 557 return; 558 } 559 560 for (HashedSession session : _sessions.values()) 561 session.save(true); 562 } 563 564 /* ------------------------------------------------------------ */ 565 public HashedSession restoreSession (InputStream is, HashedSession session) throws Exception 566 { 567 /* 568 * Take care of this class's fields first by calling 569 * defaultReadObject 570 */ 571 DataInputStream in = new DataInputStream(is); 572 try 573 { 574 String clusterId = in.readUTF(); 575 in.readUTF(); // nodeId 576 long created = in.readLong(); 577 long accessed = in.readLong(); 578 int requests = in.readInt(); 579 580 if (session == null) 581 session = (HashedSession)newSession(created, accessed, clusterId); 582 session.setRequests(requests); 583 int size = in.readInt(); 584 if (size>0) 585 { 586 ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in); 587 try 588 { 589 for (int i=0; i<size;i++) 590 { 591 String key = ois.readUTF(); 592 Object value = ois.readObject(); 593 session.setAttribute(key,value); 594 } 595 } 596 finally 597 { 598 IO.close(ois); 599 } 600 } 601 return session; 602 } 603 finally 604 { 605 IO.close(in); 606 } 607 } 608 609 610 /* ------------------------------------------------------------ */ 611 /* ------------------------------------------------------------ */ 612 protected class ClassLoadingObjectInputStream extends ObjectInputStream 613 { 614 /* ------------------------------------------------------------ */ 615 public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException 616 { 617 super(in); 618 } 619 620 /* ------------------------------------------------------------ */ 621 public ClassLoadingObjectInputStream () throws IOException 622 { 623 super(); 624 } 625 626 /* ------------------------------------------------------------ */ 627 @Override 628 public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException 629 { 630 try 631 { 632 return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); 633 } 634 catch (ClassNotFoundException e) 635 { 636 return super.resolveClass(cl); 637 } 638 } 639 } 640} 641