// ======================================================================== // Copyright (c) 1996-2009 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // You may elect to redistribute this code under either of these licenses. // ======================================================================== package org.eclipse.jetty.server.session; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.log.Logger; /* ------------------------------------------------------------ */ /** An in-memory implementation of SessionManager. * <p> * This manager supports saving sessions to disk, either periodically or at shutdown. * Sessions can also have their content idle saved to disk to reduce the memory overheads of large idle sessions. * <p> * This manager will create it's own Timer instance to scavenge threads, unless it discovers a shared Timer instance * set as the "org.eclipse.jetty.server.session.timer" attribute of the ContextHandler. * */ public class HashSessionManager extends AbstractSessionManager { final static Logger __log = SessionHandler.LOG; protected final ConcurrentMap<String,HashedSession> _sessions=new ConcurrentHashMap<String,HashedSession>(); private static int __id; private Timer _timer; private boolean _timerStop=false; private TimerTask _task; long _scavengePeriodMs=30000; long _savePeriodMs=0; //don't do period saves by default long _idleSavePeriodMs = 0; // don't idle save sessions by default. private TimerTask _saveTask; File _storeDir; private boolean _lazyLoad=false; private volatile boolean _sessionsLoaded=false; private boolean _deleteUnrestorableSessions=false; /* ------------------------------------------------------------ */ public HashSessionManager() { super(); } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStart() */ @Override public void doStart() throws Exception { super.doStart(); _timerStop=false; ServletContext context = ContextHandler.getCurrentContext(); if (context!=null) _timer=(Timer)context.getAttribute("org.eclipse.jetty.server.session.timer"); if (_timer==null) { _timerStop=true; _timer=new Timer("HashSessionScavenger-"+__id++, true); } setScavengePeriod(getScavengePeriod()); if (_storeDir!=null) { if (!_storeDir.exists()) _storeDir.mkdirs(); if (!_lazyLoad) restoreSessions(); } setSavePeriod(getSavePeriod()); } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStop() */ @Override public void doStop() throws Exception { // stop the scavengers synchronized(this) { if (_saveTask!=null) _saveTask.cancel(); _saveTask=null; if (_task!=null) _task.cancel(); _task=null; if (_timer!=null && _timerStop) _timer.cancel(); _timer=null; } // This will callback invalidate sessions - where we decide if we will save super.doStop(); _sessions.clear(); } /* ------------------------------------------------------------ */ /** * @return the period in seconds at which a check is made for sessions to be invalidated. */ public int getScavengePeriod() { return (int)(_scavengePeriodMs/1000); } /* ------------------------------------------------------------ */ @Override public int getSessions() { int sessions=super.getSessions(); if (__log.isDebugEnabled()) { if (_sessions.size()!=sessions) __log.warn("sessions: "+_sessions.size()+"!="+sessions); } return sessions; } /* ------------------------------------------------------------ */ /** * @return seconds Idle period after which a session is saved */ public int getIdleSavePeriod() { if (_idleSavePeriodMs <= 0) return 0; return (int)(_idleSavePeriodMs / 1000); } /* ------------------------------------------------------------ */ /** * Configures the period in seconds after which a session is deemed idle and saved * to save on session memory. * * The session is persisted, the values attribute map is cleared and the session set to idled. * * @param seconds Idle period after which a session is saved */ public void setIdleSavePeriod(int seconds) { _idleSavePeriodMs = seconds * 1000L; } /* ------------------------------------------------------------ */ @Override public void setMaxInactiveInterval(int seconds) { super.setMaxInactiveInterval(seconds); if (_dftMaxIdleSecs>0&&_scavengePeriodMs>_dftMaxIdleSecs*1000L) setScavengePeriod((_dftMaxIdleSecs+9)/10); } /* ------------------------------------------------------------ */ /** * @param seconds the period is seconds at which sessions are periodically saved to disk */ public void setSavePeriod (int seconds) { long period = (seconds * 1000L); if (period < 0) period=0; _savePeriodMs=period; if (_timer!=null) { synchronized (this) { if (_saveTask!=null) _saveTask.cancel(); if (_savePeriodMs > 0 && _storeDir!=null) //only save if we have a directory configured { _saveTask = new TimerTask() { @Override public void run() { try { saveSessions(true); } catch (Exception e) { __log.warn(e); } } }; _timer.schedule(_saveTask,_savePeriodMs,_savePeriodMs); } } } } /* ------------------------------------------------------------ */ /** * @return the period in seconds at which sessions are periodically saved to disk */ public int getSavePeriod () { if (_savePeriodMs<=0) return 0; return (int)(_savePeriodMs/1000); } /* ------------------------------------------------------------ */ /** * @param seconds the period in seconds at which a check is made for sessions to be invalidated. */ public void setScavengePeriod(int seconds) { if (seconds==0) seconds=60; long old_period=_scavengePeriodMs; long period=seconds*1000L; if (period>60000) period=60000; if (period<1000) period=1000; _scavengePeriodMs=period; if (_timer!=null && (period!=old_period || _task==null)) { synchronized (this) { if (_task!=null) _task.cancel(); _task = new TimerTask() { @Override public void run() { scavenge(); } }; _timer.schedule(_task,_scavengePeriodMs,_scavengePeriodMs); } } } /* -------------------------------------------------------------- */ /** * Find sessions that have timed out and invalidate them. This runs in the * SessionScavenger thread. */ protected void scavenge() { //don't attempt to scavenge if we are shutting down if (isStopping() || isStopped()) return; Thread thread=Thread.currentThread(); ClassLoader old_loader=thread.getContextClassLoader(); try { if (_loader!=null) thread.setContextClassLoader(_loader); // For each session long now=System.currentTimeMillis(); for (Iterator<HashedSession> i=_sessions.values().iterator(); i.hasNext();) { HashedSession session=i.next(); long idleTime=session.getMaxInactiveInterval()*1000L; if (idleTime>0&&session.getAccessed()+idleTime<now) { // Found a stale session, add it to the list session.timeout(); } else if (_idleSavePeriodMs>0&&session.getAccessed()+_idleSavePeriodMs<now) { session.idle(); } } } catch (Throwable t) { __log.warn("Problem scavenging sessions", t); } finally { thread.setContextClassLoader(old_loader); } } /* ------------------------------------------------------------ */ @Override protected void addSession(AbstractSession session) { if (isRunning()) _sessions.put(session.getClusterId(),(HashedSession)session); } /* ------------------------------------------------------------ */ @Override public AbstractSession getSession(String idInCluster) { if ( _lazyLoad && !_sessionsLoaded) { try { restoreSessions(); } catch(Exception e) { __log.warn(e); } } Map<String,HashedSession> sessions=_sessions; if (sessions==null) return null; HashedSession session = sessions.get(idInCluster); if (session == null && _lazyLoad) session=restoreSession(idInCluster); if (session == null) return null; if (_idleSavePeriodMs!=0) session.deIdle(); return session; } /* ------------------------------------------------------------ */ @Override protected void invalidateSessions() throws Exception { // Invalidate all sessions to cause unbind events ArrayList<HashedSession> sessions=new ArrayList<HashedSession>(_sessions.values()); int loop=100; while (sessions.size()>0 && loop-->0) { // If we are called from doStop if (isStopping() && _storeDir != null && _storeDir.exists() && _storeDir.canWrite()) { // Then we only save and remove the session - it is not invalidated. for (HashedSession session : sessions) { session.save(false); removeSession(session,false); } } else { for (HashedSession session : sessions) session.invalidate(); } // check that no new sessions were created while we were iterating sessions=new ArrayList<HashedSession>(_sessions.values()); } } /* ------------------------------------------------------------ */ @Override protected AbstractSession newSession(HttpServletRequest request) { return new HashedSession(this, request); } /* ------------------------------------------------------------ */ protected AbstractSession newSession(long created, long accessed, String clusterId) { return new HashedSession(this, created,accessed, clusterId); } /* ------------------------------------------------------------ */ @Override protected boolean removeSession(String clusterId) { return _sessions.remove(clusterId)!=null; } /* ------------------------------------------------------------ */ public void setStoreDirectory (File dir) { _storeDir=dir; } /* ------------------------------------------------------------ */ public File getStoreDirectory () { return _storeDir; } /* ------------------------------------------------------------ */ public void setLazyLoad(boolean lazyLoad) { _lazyLoad = lazyLoad; } /* ------------------------------------------------------------ */ public boolean isLazyLoad() { return _lazyLoad; } /* ------------------------------------------------------------ */ public boolean isDeleteUnrestorableSessions() { return _deleteUnrestorableSessions; } /* ------------------------------------------------------------ */ public void setDeleteUnrestorableSessions(boolean deleteUnrestorableSessions) { _deleteUnrestorableSessions = deleteUnrestorableSessions; } /* ------------------------------------------------------------ */ public void restoreSessions () throws Exception { _sessionsLoaded = true; if (_storeDir==null || !_storeDir.exists()) { return; } if (!_storeDir.canRead()) { __log.warn ("Unable to restore Sessions: Cannot read from Session storage directory "+_storeDir.getAbsolutePath()); return; } String[] files = _storeDir.list(); for (int i=0;files!=null&&i<files.length;i++) { restoreSession(files[i]); } } /* ------------------------------------------------------------ */ protected synchronized HashedSession restoreSession(String idInCuster) { File file = new File(_storeDir,idInCuster); try { if (file.exists()) { FileInputStream in = new FileInputStream(file); HashedSession session = restoreSession(in, null); in.close(); addSession(session, false); session.didActivate(); file.delete(); return session; } } catch (Exception e) { if (isDeleteUnrestorableSessions()) { if (file.exists()) { file.delete(); __log.warn("Deleting file for unrestorable session "+idInCuster, e); } } else __log.warn("Problem restoring session "+idInCuster, e); } return null; } /* ------------------------------------------------------------ */ public void saveSessions(boolean reactivate) throws Exception { if (_storeDir==null || !_storeDir.exists()) { return; } if (!_storeDir.canWrite()) { __log.warn ("Unable to save Sessions: Session persistence storage directory "+_storeDir.getAbsolutePath()+ " is not writeable"); return; } for (HashedSession session : _sessions.values()) session.save(true); } /* ------------------------------------------------------------ */ public HashedSession restoreSession (InputStream is, HashedSession session) throws Exception { /* * Take care of this class's fields first by calling * defaultReadObject */ DataInputStream in = new DataInputStream(is); String clusterId = in.readUTF(); in.readUTF(); // nodeId long created = in.readLong(); long accessed = in.readLong(); int requests = in.readInt(); if (session == null) session = (HashedSession)newSession(created, accessed, clusterId); session.setRequests(requests); int size = in.readInt(); if (size>0) { ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in); for (int i=0; i<size;i++) { String key = ois.readUTF(); Object value = ois.readObject(); session.setAttribute(key,value); } ois.close(); } else in.close(); return session; } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ protected class ClassLoadingObjectInputStream extends ObjectInputStream { /* ------------------------------------------------------------ */ public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException { super(in); } /* ------------------------------------------------------------ */ public ClassLoadingObjectInputStream () throws IOException { super(); } /* ------------------------------------------------------------ */ @Override public Class<?> resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException { try { return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); } catch (ClassNotFoundException e) { return super.resolveClass(cl); } } } }