// ========================================================================
// Copyright 2008 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
package org.mortbay.jetty.openspaces;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.mortbay.jetty.handler.ContextHandler;
import org.mortbay.log.Log;
import org.mortbay.util.LazyList;
import org.openspaces.core.GigaSpace;
import org.openspaces.core.GigaSpaceConfigurer;
import org.openspaces.core.space.UrlSpaceConfigurer;
import org.openspaces.core.space.cache.LocalCacheSpaceConfigurer;
import com.gigaspaces.annotation.pojo.SpaceId;
import com.gigaspaces.annotation.pojo.SpaceProperty;
import com.gigaspaces.annotation.pojo.SpaceRouting;
import com.gigaspaces.annotation.pojo.SpaceProperty.IndexType;
import com.j_spaces.core.client.Query;
import com.j_spaces.core.client.SQLQuery;
/**
* GigaspacesSessionManager
*
* A Jetty SessionManager where the session data is stored in a
* data grid "cloud".
*
* On each request, the session data is looked up in the "cloud"
* and brought into the local space cache if doesn't already exist,
* and an entry put into the managers map of sessions. When the request
* exists, any changes, including changes to the access time of the session
* are written back out to the grid.
*
* TODO if we follow this strategy of having the grid store the session
* data for us, and it is relatively cheap to access, then we could dispense
* with the in-memory _sessions map.
*/
public class GigaSessionManager extends org.mortbay.jetty.servlet.AbstractSessionManager
{
private static int __id; //for identifying the scavenger thread
private ConcurrentHashMap _sessions;
private GigaSpace _space;
private String _spaceUrl;
private long _waitMsec = 5000L; //wait up to 5secs for requested objects to appear
protected Timer _timer; //scavenge timer
protected TimerTask _task; //scavenge task
protected int _scavengePeriodMs = 1000 * 60 * 10; //10mins
protected int _scavengeCount = 0;
protected int _savePeriodMs = 60 * 1000; //60 sec
protected SQLQuery _query;
/**
* SessionData
*
* Data about a session.
*
* NOTE: we let gigaspaces assign a globally unique identifier for
* a SessionData object, although we could compose our own, based on:
* canonicalized(contextPath) + virtualhost[0] + sessionid
*/
public static class SessionData implements Serializable
{
private String _uid; //unique id
private String _id;
private long _accessed=-1;
private long _lastAccessed=-1;
private long _lastSaved=-1;
private long _maxIdleMs=-1;
private long _cookieSet=-1;
private long _created=-1;
private ConcurrentHashMap _attributes=null;
private String _contextPath;
private long _expiryTime=-1;
private String _virtualHost;
public SessionData ()
{
}
public SessionData (String sessionId)
{
_id=sessionId;
_created=System.currentTimeMillis();
_accessed = _created;
_lastAccessed = 0;
_lastSaved = 0;
}
@SpaceId(autoGenerate=true)
public synchronized String getUid ()
{
return _uid;
}
public synchronized void setUid (String uid)
{
_uid=uid;
}
public synchronized void setId (String id)
{
_id=id;
}
@SpaceProperty(index=IndexType.BASIC)
@SpaceRouting
public synchronized String getId ()
{
return _id;
}
@SpaceProperty(nullValue="-1")
public synchronized long getCreated ()
{
return _created;
}
public synchronized void setCreated (long ms)
{
_created = ms;
}
@SpaceProperty(nullValue="-1")
public synchronized long getAccessed ()
{
return _accessed;
}
public synchronized void setAccessed (long ms)
{
_accessed = ms;
}
public synchronized void setLastSaved (long ms)
{
_lastSaved = ms;
}
@SpaceProperty(nullValue="-1")
public synchronized long getLastSaved ()
{
return _lastSaved;
}
public synchronized void setMaxIdleMs (long ms)
{
_maxIdleMs = ms;
}
@SpaceProperty(nullValue="-1")
public synchronized long getMaxIdleMs()
{
return _maxIdleMs;
}
public synchronized void setLastAccessed (long ms)
{
_lastAccessed = ms;
}
@SpaceProperty(nullValue="-1")
public synchronized long getLastAccessed()
{
return _lastAccessed;
}
public void setCookieSet (long ms)
{
_cookieSet = ms;
}
@SpaceProperty(nullValue="-1")
public synchronized long getCookieSet ()
{
return _cookieSet;
}
@SpaceProperty
protected synchronized ConcurrentHashMap getAttributeMap ()
{
return _attributes;
}
protected synchronized void setAttributeMap (ConcurrentHashMap map)
{
_attributes = map;
}
public synchronized void setContextPath(String str)
{
_contextPath=str;
}
@SpaceProperty(index=IndexType.BASIC)
public synchronized String getContextPath ()
{
return _contextPath;
}
public synchronized void setExpiryTime (long time)
{
_expiryTime=time;
}
@SpaceProperty(nullValue="-1")
public synchronized long getExpiryTime ()
{
return _expiryTime;
}
public synchronized void setVirtualHost (String vhost)
{
_virtualHost=vhost;
}
public synchronized String getVirtualHost ()
{
return _virtualHost;
}
public String toString ()
{
return "Session uid="+_uid+", id="+_id+
", contextpath="+_contextPath+
", virtualHost="+_virtualHost+
",created="+_created+",accessed="+_accessed+
",lastAccessed="+_lastAccessed+
",cookieSet="+_cookieSet+
",expiryTime="+_expiryTime;
}
public String toStringExtended ()
{
return toString()+"values="+_attributes;
}
}
/**
* Session
*
* A session in memory of a Context. Adds behaviour around SessionData.
*/
public class Session extends org.mortbay.jetty.servlet.AbstractSessionManager.Session
{
private SessionData _data;
private boolean _dirty=false;
/**
* Session from a request.
*
* @param request
*/
protected Session (HttpServletRequest request)
{
super(request);
_data = new SessionData(_clusterId);
_data.setMaxIdleMs(_dftMaxIdleSecs*1000);
_data.setContextPath(_context.getContextPath());
_data.setVirtualHost(getVirtualHost(_context));
_data.setExpiryTime(_maxIdleMs < 0 ? 0 : (System.currentTimeMillis() + _maxIdleMs));
_data.setCookieSet(0);
if (_data.getAttributeMap()==null)
newAttributeMap();
_values=_data.getAttributeMap();
if (Log.isDebugEnabled()) Log.debug("New Session from request, "+_data.toStringExtended());
}
/**
* Session restored in database.
* @param row
*/
protected Session (SessionData data)
{
super(data.getCreated(), data.getId());
_data=data;
_values = data.getAttributeMap();
if (Log.isDebugEnabled()) Log.debug("New Session from existing session data "+_data.toStringExtended());
}
protected void cookieSet()
{
_data.setCookieSet(_data.getAccessed());
}
protected Map newAttributeMap()
{
if (_data.getAttributeMap()==null)
{
_data.setAttributeMap(new ConcurrentHashMap());
}
return _data.getAttributeMap();
}
public void setAttribute (String name, Object value)
{
super.setAttribute(name, value);
_dirty=true;
}
public void removeAttribute (String name)
{
super.removeAttribute(name);
_dirty=true;
}
/**
* Entry to session.
* Called by SessionHandler on inbound request and the session already exists in this node's memory.
*
* @see org.mortbay.jetty.servlet.AbstractSessionManager.Session#access(long)
*/
protected void access(long time)
{
super.access(time);
_data.setLastAccessed(_data.getAccessed());
_data.setAccessed(time);
_data.setExpiryTime(_maxIdleMs < 0 ? 0 : (time + _maxIdleMs));
}
/**
* Exit from session
*
* If the session attributes changed then always write the session
* to the cloud.
*
* If just the session access time changed, we don't always write out the
* session, because the gigaspace will serialize the unchanged sesssion
* attributes. To save on serialization overheads, we only write out the
* session when only the access time has changed if the time at which we
* last saved the session exceeds the chosen save interval.
*
* @see org.mortbay.jetty.servlet.AbstractSessionManager.Session#complete()
*/
protected void complete()
{
super.complete();
try
{
if (_dirty || (_data._accessed - _data._lastSaved) >= (_savePeriodMs))
{
_data.setLastSaved(System.currentTimeMillis());
willPassivate();
update(_data);
didActivate();
if (Log.isDebugEnabled()) Log.debug("Dirty="+_dirty+", accessed-saved="+_data._accessed +"-"+ _data._lastSaved+", savePeriodMs="+_savePeriodMs);
}
}
catch (Exception e)
{
Log.warn("Problem persisting changed session data id="+getId(), e);
}
finally
{
_dirty=false;
}
}
protected void timeout() throws IllegalStateException
{
if (Log.isDebugEnabled()) Log.debug("Timing out session id="+getClusterId());
super.timeout();
}
protected void willPassivate ()
{
super.willPassivate();
}
protected void didActivate ()
{
super.didActivate();
}
public String getClusterId()
{
return super.getClusterId();
}
public String getNodeId()
{
return super.getNodeId();
}
}
/**
* Start the session manager.
*
* @see org.mortbay.jetty.servlet.AbstractSessionManager#doStart()
*/
public void doStart() throws Exception
{
if (_sessionIdManager==null)
throw new IllegalStateException("No session id manager defined");
_sessions = new ConcurrentHashMap();
if (_space==null)
initSpace();
super.doStart();
_timer=new Timer("GigaspaceSessionScavenger_"+(++__id), true);
setScavengePeriod(getScavengePeriod());
}
/**
* Stop the session manager.
*
* @see org.mortbay.jetty.servlet.AbstractSessionManager#doStop()
*/
public void doStop() throws Exception
{
// stop the scavenger
synchronized(this)
{
if (_task!=null)
_task.cancel();
if (_timer!=null)
_timer.cancel();
_timer=null;
}
_sessions.clear();
_sessions = null;
_space = null;
super.doStop();
}
public int getSavePeriod ()
{
return _savePeriodMs/1000;
}
public void setSavePeriod (int seconds)
{
if (seconds <= 0)
seconds=60;
_savePeriodMs = seconds*1000;
}
public int getScavengePeriod()
{
return _scavengePeriodMs/1000;
}
public void setScavengePeriod(int seconds)
{
if (seconds<=0)
seconds=60;
int old_period=_scavengePeriodMs;
int period=seconds*1000;
_scavengePeriodMs=period;
//add a bit of variability into the scavenge time so that not all
//contexts with the same scavenge time sync up
int tenPercent = _scavengePeriodMs/10;
if ((System.currentTimeMillis()%2) == 0)
_scavengePeriodMs += tenPercent;
if (Log.isDebugEnabled()) Log.debug("GigspacesSessionScavenger scavenging every "+_scavengePeriodMs+" ms");
if (_timer!=null && (period!=old_period || _task==null))
{
synchronized (this)
{
if (_task!=null)
_task.cancel();
_task = new TimerTask()
{
public void run()
{
scavenge();
}
};
_timer.schedule(_task,_scavengePeriodMs,_scavengePeriodMs);
}
}
}
public void setSpace (GigaSpace space)
{
_space=space;
}
public GigaSpace getSpace ()
{
return _space;
}
/**
* Get a session matching the id.
*
* Look in the grid to see if such a session exists, as it may have moved from
* another node.
*
* @see org.mortbay.jetty.servlet.AbstractSessionManager#getSession(java.lang.String)
*/
public Session getSession(String idInCluster)
{
synchronized (this)
{
try
{
//Ask the space for the session. This might incur serialization:
//if we have no localcache, OR the localcache has to fetch the session
//because of a cache miss OR the localcache is set to pull mode (where it
//checks for changes to an object when that object is requested).
//Alternatively, if the localcache is set to push mode, the cloud will
//keep the localcache up-to-date with object changes in the background,
//so serialization is occuring beyond our control.
//TODO consider using the jdbc approach, were we only ask the cloud
//intermittently for the session.
SessionData template = new SessionData();
template.setId(idInCluster);
template.setContextPath(_context.getContextPath());
template.setVirtualHost(getVirtualHost(_context));
SessionData data = fetch (template);
Session session = null;
if (data == null)
{
//No session in cloud with matching id and context path.
session=null;
if (Log.isDebugEnabled()) Log.debug("No session matching id="+idInCluster);
}
else
{
Session oldSession = (Session)_sessions.get(idInCluster);
//if we had no prior session, or the session from the cloud has been
//more recently updated than our copy in memory, we should use it
//instead
if ((oldSession == null) || (data.getAccessed() > oldSession._data.getAccessed()))
{
session = new Session(data);
_sessions.put(idInCluster, session);
session.didActivate();
if (Log.isDebugEnabled()) Log.debug("Refreshed in-memory Session with "+data.toStringExtended());
}
else
{
if (Log.isDebugEnabled()) Log.debug("Not updating session "+idInCluster+", in-memory session is as fresh or fresher");
session = oldSession;
}
}
return session;
}
catch (Exception e)
{
Log.warn("Unable to load session from database", e);
return null;
}
}
}
public void setSpaceUrl (String url)
{
_spaceUrl=url;
}
public String getSpaceUrl ()
{
return _spaceUrl;
}
public void setWaitMs (long msec)
{
_waitMsec=msec;
}
public long getWaitMs ()
{
return _waitMsec;
}
public Map getSessionMap()
{
return Collections.unmodifiableMap(_sessions);
}
public int getSessions()
{
int size = 0;
synchronized (this)
{
size = _sessions.size();
}
return size;
}
protected void invalidateSessions()
{
//Do nothing - we don't want to remove and
//invalidate all the sessions because this
//method is called from doStop(), and just
//because this context is stopping does not
//mean that we should remove the session from
//any other nodes
}
protected Session newSession(HttpServletRequest request)
{
return new Session(request);
}
protected void removeSession(String idInCluster)
{
synchronized (this)
{
try
{
Session session = (Session)_sessions.remove(idInCluster);
delete(session._data);
}
catch (Exception e)
{
Log.warn("Problem deleting session id="+idInCluster, e);
}
}
}
public void removeSession(org.mortbay.jetty.servlet.AbstractSessionManager.Session abstractSession, boolean invalidate)
{
if (! (abstractSession instanceof GigaSessionManager.Session))
throw new IllegalStateException("Session is not a GigaspacesSessionManager.Session "+abstractSession);
GigaSessionManager.Session session = (GigaSessionManager.Session)abstractSession;
synchronized (_sessionIdManager)
{
boolean removed = false;
synchronized (this)
{
//take this session out of the map of sessions for this context
if (_sessions.get(getClusterId(session)) != null)
{
removed = true;
removeSession(getClusterId(session));
}
}
if (removed)
{
// Remove session from all context and global id maps
_sessionIdManager.removeSession(session);
if (invalidate)
_sessionIdManager.invalidateAll(getClusterId(session));
}
}
if (invalidate && _sessionListeners!=null)
{
HttpSessionEvent event=new HttpSessionEvent(session);
for (int i=LazyList.size(_sessionListeners); i-->0;)
((HttpSessionListener)LazyList.get(_sessionListeners,i)).sessionDestroyed(event);
}
if (!invalidate)
{
session.willPassivate();
}
}
public void invalidateSession(String idInCluster)
{
synchronized (this)
{
Session session = (Session)_sessions.get(idInCluster);
if (session != null)
{
session.invalidate();
}
}
}
protected void addSession(org.mortbay.jetty.servlet.AbstractSessionManager.Session abstractSession)
{
if (abstractSession==null)
return;
if (!(abstractSession instanceof GigaSessionManager.Session))
throw new IllegalStateException("Not a GigaspacesSessionManager.Session "+abstractSession);
synchronized (this)
{
GigaSessionManager.Session session = (GigaSessionManager.Session)abstractSession;
try
{
_sessions.put(getClusterId(session), session);
add(session._data);
}
catch (Exception e)
{
Log.warn("Problem writing new SessionData to space ", e);
}
}
}
/**
* Look for expired sessions that we know about in our
* session map, and double check with the grid that
* it has really expired, or already been removed.
*/
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();
_scavengeCount++;
try
{
if (_loader!=null)
thread.setContextClassLoader(_loader);
long now = System.currentTimeMillis();
if (Log.isDebugEnabled()) Log.debug("Scavenger running at "+now+" for context = "+_context.getContextPath());
//go through in-memory map of Sessions, pick out any that are candidates for removal
//due to expiry time being reached or passed.
synchronized (this)
{
ArrayList removalCandidates = new ArrayList();
Iterator itor = _sessions.values().iterator();
while (itor.hasNext())
{
Session session = (Session)itor.next();
if (session._data._expiryTime < now)
removalCandidates.add(session);
}
//for each candidate, check the session data in the cloud to ensure that some other
//node hasn't been updating it's access time. If it's still expired, then delete it
//locally and in the cloud.
itor = removalCandidates.listIterator();
while (itor.hasNext())
{
Session candidate = (Session)itor.next();
SessionData template = new SessionData();
template.setUid(candidate._data._uid);
template.setId(candidate.getId());
try
{
SessionData currentSessionData = fetch(template);
if (currentSessionData==null)
{
//it's no longer in the cloud - either some other node has
//expired it or invalidated it
_sessions.remove(candidate.getId());
if (Log.isDebugEnabled()) Log.debug("Dropped non-existant session "+candidate._data);
}
else if (currentSessionData._expiryTime < now)
{
//its expired, run all the listeners etc
candidate.timeout();
itor.remove();
if (Log.isDebugEnabled()) Log.debug("Timed out session "+candidate._data);
}
}
catch (Exception e)
{
Log.warn("Problem checking current state of session "+candidate._data, e);
}
}
//every so often do a bigger sweep for very old sessions in
//the cloud. A very old session is one that is defined to have
//expired at least 2 sweeps of the scavenger ago. TODO make
//this configurable
if ((_scavengeCount % 2) == 0)
{
if (Log.isDebugEnabled()) Log.debug("Scavenging old sessions, expiring before: "+(now - (2 * _scavengePeriodMs)));
Object[] expiredSessions = findExpiredSessions((now - (2 * _scavengePeriodMs)));
for (int i = 0; i < expiredSessions.length; i++)
{
if (Log.isDebugEnabled()) Log.debug("Timing out expired sesson " + expiredSessions[i]);
GigaSessionManager.Session expiredSession = new GigaSessionManager.Session((SessionData)expiredSessions[i]);
_sessions.put(expiredSession.getClusterId(), expiredSession); //needs to be in there so removeSession test will succeed and remove it
expiredSession.timeout();
if (Log.isDebugEnabled()) Log.debug("Expiring old session "+expiredSession._data);
}
}
int count = this._sessions.size();
if (count < this._minSessions)
this._minSessions=count;
}
}
catch (Throwable t)
{
if (t instanceof ThreadDeath)
throw ((ThreadDeath)t);
else
Log.warn("Problem scavenging sessions", t);
}
finally
{
thread.setContextClassLoader(old_loader);
}
}
protected void add (SessionData data)
throws Exception
{
_space.write(data);
}
protected void delete (SessionData data)
throws Exception
{
SessionData sd = new SessionData();
sd.setUid(data.getUid());
sd.setId(data.getId());
_space.takeIfExists(sd, getWaitMs());
}
protected void update (SessionData data)
throws Exception
{
_space.write(data);
if (Log.isDebugEnabled()) Log.debug("Wrote session "+data.toStringExtended());
}
protected SessionData fetch (SessionData template)
throws Exception
{
SessionData obj = (SessionData)_space.readIfExists(template, getWaitMs());
return obj;
}
protected Object[] findExpiredSessions (long timestamp)
throws Exception
{
_query.setParameter(1, new Long(timestamp));
Object[] sessions = _space.takeMultiple(_query, Integer.MAX_VALUE);
return sessions;
}
protected void initSpace ()
throws Exception
{
if (_spaceUrl==null)
throw new IllegalStateException ("No url for space");
UrlSpaceConfigurer usc = new UrlSpaceConfigurer(_spaceUrl);
LocalCacheSpaceConfigurer lcsc = new LocalCacheSpaceConfigurer(usc.space());
GigaSpaceConfigurer gigaSpaceConfigurer = new GigaSpaceConfigurer(usc.space());
_space = gigaSpaceConfigurer.gigaSpace();
_query = new SQLQuery(SessionData.class, "expiryTime < ?");
}
/**
* Get the first virtual host for the context.
*
* Used to help identify the exact session/contextPath.
*
* @return 0.0.0.0 if no virtual host is defined
*/
private String getVirtualHost (ContextHandler.SContext context)
{
String vhost = "0.0.0.0";
if (context==null)
return vhost;
String [] vhosts = context.getContextHandler().getVirtualHosts();
if (vhosts==null || vhosts.length==0 || vhosts[0]==null)
return vhost;
return vhosts[0];
}
}