blob: 68e5227e9474166d35350610f60939b9f22410ca [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2016 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.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
/* ------------------------------------------------------------ */
/**
* HashSessionManager
*
* 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 Scheduler _timer;
private Scheduler.Task _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 Scheduler.Task _saveTask;
File _storeDir;
private boolean _lazyLoad=false;
private volatile boolean _sessionsLoaded=false;
private boolean _deleteUnrestorableSessions=false;
/**
* Scavenger
*
*/
protected class Scavenger implements Runnable
{
@Override
public void run()
{
try
{
scavenge();
}
finally
{
if (_timer != null && _timer.isRunning()) {
_task = _timer.schedule(this, _scavengePeriodMs, TimeUnit.MILLISECONDS);
}
}
}
}
/**
* Saver
*
*/
protected class Saver implements Runnable
{
@Override
public void run()
{
try
{
saveSessions(true);
}
catch (Exception e)
{
LOG.warn(e);
}
finally
{
if (_timer != null && _timer.isRunning())
_saveTask = _timer.schedule(this, _savePeriodMs, TimeUnit.MILLISECONDS);
}
}
}
/* ------------------------------------------------------------ */
public HashSessionManager()
{
super();
}
/* ------------------------------------------------------------ */
/**
* @see AbstractSessionManager#doStart()
*/
@Override
public void doStart() throws Exception
{
//try shared scheduler from Server first
_timer = getSessionHandler().getServer().getBean(Scheduler.class);
if (_timer == null)
{
//try one passed into the context
ServletContext context = ContextHandler.getCurrentContext();
if (context!=null)
_timer = (Scheduler)context.getAttribute("org.eclipse.jetty.server.session.timer");
}
if (_timer == null)
{
//make a scheduler if none useable
_timer=new ScheduledExecutorScheduler(toString()+"Timer",true);
addBean(_timer,true);
}
else
addBean(_timer,false);
super.doStart();
setScavengePeriod(getScavengePeriod());
if (_storeDir!=null)
{
if (!_storeDir.exists())
_storeDir.mkdirs();
if (!_lazyLoad)
restoreSessions();
}
setSavePeriod(getSavePeriod());
}
/* ------------------------------------------------------------ */
/**
* @see 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 we're managing our own timer, remove it
if (isManaged(_timer))
removeBean(_timer);
_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();
_saveTask = null;
if (_savePeriodMs > 0 && _storeDir!=null) //only save if we have a directory configured
{
_saveTask = _timer.schedule(new Saver(),_savePeriodMs,TimeUnit.MILLISECONDS);
}
}
}
}
/* ------------------------------------------------------------ */
/**
* @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;
synchronized (this)
{
if (_timer!=null && (period!=old_period || _task==null))
{
if (_task!=null)
{
_task.cancel();
_task = null;
}
_task = _timer.schedule(new Scavenger(),_scavengePeriodMs, TimeUnit.MILLISECONDS);
}
}
}
/* -------------------------------------------------------------- */
/**
* 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();
__log.debug("Scavenging sessions at {}", now);
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
try
{
session.timeout();
}
catch (Exception e)
{
__log.warn("Problem scavenging sessions", e);
}
}
else if (_idleSavePeriodMs > 0 && session.getAccessed()+_idleSavePeriodMs < now)
{
try
{
session.idle();
}
catch (Exception e)
{
__log.warn("Problem idling session "+ session.getId(), e);
}
}
}
}
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 shutdownSessions() 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 from memory- it is not invalidated.
for (HashedSession session : sessions)
{
session.save(false);
_sessions.remove(session.getClusterId());
}
}
else
{
for (HashedSession session : sessions)
session.invalidate();
}
// check that no new sessions were created while we were iterating
sessions=new ArrayList<HashedSession>(_sessions.values());
}
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.server.SessionManager#renewSessionId(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public void renewSessionId(String oldClusterId, String oldNodeId, String newClusterId, String newNodeId)
{
try
{
Map<String,HashedSession> sessions=_sessions;
if (sessions == null)
return;
HashedSession session = sessions.remove(oldClusterId);
if (session == null)
return;
session.remove(); //delete any previously saved session
session.setClusterId(newClusterId); //update ids
session.setNodeId(newNodeId);
session.save(); //save updated session: TODO consider only saving file if idled
sessions.put(newClusterId, session);
super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
}
catch (Exception e)
{
LOG.warn(e);
}
}
/* ------------------------------------------------------------ */
@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) throws IOException
{
// CanonicalFile is used to capture the base store directory in a way that will
// work on Windows. Case differences may through off later checks using this directory.
_storeDir=dir.getCanonicalFile();
}
/* ------------------------------------------------------------ */
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);
Exception error = null;
if (!file.exists())
{
if (LOG.isDebugEnabled())
{
LOG.debug("Not loading: {}",file);
}
return null;
}
try (FileInputStream in = new FileInputStream(file))
{
HashedSession session = restoreSession(in,null);
addSession(session,false);
session.didActivate();
return session;
}
catch (Exception e)
{
error = e;
}
finally
{
if (error != null)
{
if (isDeleteUnrestorableSessions() && file.exists() && file.getParentFile().equals(_storeDir) )
{
file.delete();
LOG.warn("Deleting file for unrestorable session {} {}",idInCuster,error);
__log.debug(error);
}
else
{
__log.warn("Problem restoring session {} {}",idInCuster, error);
__log.debug(error);
}
}
else
{
// delete successfully restored file
file.delete();
}
}
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(reactivate);
}
/* ------------------------------------------------------------ */
public HashedSession restoreSession (InputStream is, HashedSession session) throws Exception
{
DataInputStream di = new DataInputStream(is);
String clusterId = di.readUTF();
di.readUTF(); // nodeId
long created = di.readLong();
long accessed = di.readLong();
int requests = di.readInt();
if (session == null)
session = (HashedSession)newSession(created, accessed, clusterId);
session.setRequests(requests);
// Attributes
int size = di.readInt();
restoreSessionAttributes(di, size, session);
try
{
int maxIdle = di.readInt();
session.setMaxInactiveInterval(maxIdle);
}
catch (IOException e)
{
LOG.debug("No maxInactiveInterval persisted for session "+clusterId);
LOG.ignore(e);
}
return session;
}
@SuppressWarnings("resource")
private void restoreSessionAttributes (InputStream is, int size, HashedSession session)
throws Exception
{
if (size>0)
{
// input stream should not be closed here
ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is);
for (int i=0; i<size;i++)
{
String key = ois.readUTF();
Object value = ois.readObject();
session.setAttribute(key,value);
}
}
}
}