//
// ========================================================================
// Copyright (c) 1995-2019 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.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.Locker.Lock;
AbstractSessionCache A base implementation of the SessionCache
interface for managing a set of Session objects pertaining to a context in memory. This implementation ensures that multiple requests for the same session id always return the same Session object. It will delay writing out a session to the SessionDataStore until the last request exits the session. If the SessionDataStore supports passivation then the session passivation and activation listeners are called appropriately as the session is written. This implementation also supports evicting idle Session objects. An idle Session is one that is still valid, has not expired, but has not been accessed by a request for a configurable amount of time. An idle session will be first passivated before it is evicted from the cache. /**
* AbstractSessionCache
*
* A base implementation of the {@link SessionCache} interface for managing a set of
* Session objects pertaining to a context in memory.
*
* This implementation ensures that multiple requests for the same session id
* always return the same Session object.
*
* It will delay writing out a session to the SessionDataStore until the
* last request exits the session. If the SessionDataStore supports passivation
* then the session passivation and activation listeners are called appropriately as
* the session is written.
*
* This implementation also supports evicting idle Session objects. An idle Session
* is one that is still valid, has not expired, but has not been accessed by a
* request for a configurable amount of time. An idle session will be first
* passivated before it is evicted from the cache.
*/
@ManagedObject
public abstract class AbstractSessionCache extends ContainerLifeCycle implements SessionCache
{
static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
The authoritative source of session data
/**
* The authoritative source of session data
*/
protected SessionDataStore _sessionDataStore;
The SessionHandler related to this SessionCache
/**
* The SessionHandler related to this SessionCache
*/
protected final SessionHandler _handler;
Information about the context to which this SessionCache pertains
/**
* Information about the context to which this SessionCache pertains
*/
protected SessionContext _context;
When, if ever, to evict sessions: never; only when the last request for them finishes; after inactivity time (expressed as secs)
/**
* When, if ever, to evict sessions: never; only when the last request for them finishes; after inactivity time (expressed as secs)
*/
protected int _evictionPolicy = SessionCache.NEVER_EVICT;
If true, as soon as a new session is created, it will be persisted to the SessionDataStore
/**
* If true, as soon as a new session is created, it will be persisted to the SessionDataStore
*/
protected boolean _saveOnCreate = false;
If true, a session that will be evicted from the cache because it has been
inactive too long will be saved before being evicted.
/**
* If true, a session that will be evicted from the cache because it has been
* inactive too long will be saved before being evicted.
*/
protected boolean _saveOnInactiveEviction;
If true, a Session whose data cannot be read will be
deleted from the SessionDataStore.
/**
* If true, a Session whose data cannot be read will be
* deleted from the SessionDataStore.
*/
protected boolean _removeUnloadableSessions;
Create a new Session object from pre-existing session data
Params: - data – the session data
Returns: a new Session object
/**
* Create a new Session object from pre-existing session data
*
* @param data the session data
* @return a new Session object
*/
@Override
public abstract Session newSession(SessionData data);
Create a new Session for a request.
Params: - request – the request
- data – the session data
Returns: the new session
/**
* Create a new Session for a request.
*
* @param request the request
* @param data the session data
* @return the new session
*/
public abstract Session newSession(HttpServletRequest request, SessionData data);
See Also: - newSession.newSession(HttpServletRequest, String, long, long)
/**
* @see org.eclipse.jetty.server.session.SessionCache#newSession(javax.servlet.http.HttpServletRequest, java.lang.String, long, long)
*/
@Override
public Session newSession(HttpServletRequest request, String id, long time, long maxInactiveMs)
{
if (LOG.isDebugEnabled())
LOG.debug("Creating new session id=" + id);
Session session = newSession(request, _sessionDataStore.newSessionData(id, time, time, time, maxInactiveMs));
session.getSessionData().setLastNode(_context.getWorkerName());
try
{
if (isSaveOnCreate() && _sessionDataStore != null)
_sessionDataStore.store(id, session.getSessionData());
}
catch (Exception e)
{
LOG.warn("Save of new session {} failed", id, e);
}
return session;
}
Get the session matching the key
Params: - id – session id
Returns: the Session object matching the id
/**
* Get the session matching the key
*
* @param id session id
* @return the Session object matching the id
*/
public abstract Session doGet(String id);
Put the session into the map if it wasn't already there
Params: - id – the identity of the session
- session – the session object
Returns: null if the session wasn't already in the map, or the existing entry otherwise
/**
* Put the session into the map if it wasn't already there
*
* @param id the identity of the session
* @param session the session object
* @return null if the session wasn't already in the map, or the existing entry otherwise
*/
public abstract Session doPutIfAbsent(String id, Session session);
Replace the mapping from id to oldValue with newValue
Params: - id – the id
- oldValue – the old value
- newValue – the new value
Returns: true if replacement was done
/**
* Replace the mapping from id to oldValue with newValue
*
* @param id the id
* @param oldValue the old value
* @param newValue the new value
* @return true if replacement was done
*/
public abstract boolean doReplace(String id, Session oldValue, Session newValue);
Remove the session with this identity from the store
Params: - id – the id
Returns: Session that was removed or null
/**
* Remove the session with this identity from the store
*
* @param id the id
* @return Session that was removed or null
*/
public abstract Session doDelete(String id);
PlaceHolder
/**
* PlaceHolder
*/
protected class PlaceHolderSession extends Session
{
Params: - handler – SessionHandler to which this session belongs
- data – the session data
/**
* @param handler SessionHandler to which this session belongs
* @param data the session data
*/
public PlaceHolderSession(SessionHandler handler, SessionData data)
{
super(handler, data);
}
}
Params: - handler – the
SessionHandler
to use
/**
* @param handler the {@link SessionHandler} to use
*/
public AbstractSessionCache(SessionHandler handler)
{
_handler = handler;
}
Returns: the SessionManger
/**
* @return the SessionManger
*/
@Override
public SessionHandler getSessionHandler()
{
return _handler;
}
See Also: - initialize.initialize(SessionContext)
/**
* @see org.eclipse.jetty.server.session.SessionCache#initialize(org.eclipse.jetty.server.session.SessionContext)
*/
@Override
public void initialize(SessionContext context)
{
if (isStarted())
throw new IllegalStateException("Context set after session store started");
_context = context;
}
See Also: - doStart.doStart()
/**
* @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
*/
@Override
protected void doStart() throws Exception
{
if (_sessionDataStore == null)
throw new IllegalStateException("No session data store configured");
if (_handler == null)
throw new IllegalStateException("No session manager");
if (_context == null)
throw new IllegalStateException("No ContextId");
_sessionDataStore.initialize(_context);
super.doStart();
}
See Also: - doStop.doStop()
/**
* @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
*/
@Override
protected void doStop() throws Exception
{
_sessionDataStore.stop();
super.doStop();
}
Returns: the SessionDataStore or null if there isn't one
/**
* @return the SessionDataStore or null if there isn't one
*/
@Override
public SessionDataStore getSessionDataStore()
{
return _sessionDataStore;
}
See Also: - setSessionDataStore.setSessionDataStore(SessionDataStore)
/**
* @see org.eclipse.jetty.server.session.SessionCache#setSessionDataStore(org.eclipse.jetty.server.session.SessionDataStore)
*/
@Override
public void setSessionDataStore(SessionDataStore sessionStore)
{
updateBean(_sessionDataStore, sessionStore);
_sessionDataStore = sessionStore;
}
See Also: - getEvictionPolicy.getEvictionPolicy()
/**
* @see org.eclipse.jetty.server.session.SessionCache#getEvictionPolicy()
*/
@ManagedAttribute(value = "session eviction policy", readonly = true)
@Override
public int getEvictionPolicy()
{
return _evictionPolicy;
}
-1 means we never evict inactive sessions.
0 means we evict a session after the last request for it exits
>0 is the number of seconds after which we evict inactive sessions from the cache
See Also: - setEvictionPolicy.setEvictionPolicy(int)
/**
* -1 means we never evict inactive sessions.
* 0 means we evict a session after the last request for it exits
* >0 is the number of seconds after which we evict inactive sessions from the cache
*
* @see org.eclipse.jetty.server.session.SessionCache#setEvictionPolicy(int)
*/
@Override
public void setEvictionPolicy(int evictionTimeout)
{
_evictionPolicy = evictionTimeout;
}
@ManagedAttribute(value = "immediately save new sessions", readonly = true)
@Override
public boolean isSaveOnCreate()
{
return _saveOnCreate;
}
@Override
public void setSaveOnCreate(boolean saveOnCreate)
{
_saveOnCreate = saveOnCreate;
}
Returns: true if sessions that can't be loaded are deleted from the store
/**
* @return true if sessions that can't be loaded are deleted from the store
*/
@ManagedAttribute(value = "delete unreadable stored sessions", readonly = true)
@Override
public boolean isRemoveUnloadableSessions()
{
return _removeUnloadableSessions;
}
If a session's data cannot be loaded from the store without error, remove
it from the persistent store.
Params: - removeUnloadableSessions – if
true
unloadable sessions will be removed from session store
/**
* If a session's data cannot be loaded from the store without error, remove
* it from the persistent store.
*
* @param removeUnloadableSessions if <code>true</code> unloadable sessions will be removed from session store
*/
@Override
public void setRemoveUnloadableSessions(boolean removeUnloadableSessions)
{
_removeUnloadableSessions = removeUnloadableSessions;
}
Get a session object.
If the session object is not in this session store, try getting
the data for it from a SessionDataStore associated with the
session manager.
See Also: - get.get(String)
/**
* Get a session object.
*
* If the session object is not in this session store, try getting
* the data for it from a SessionDataStore associated with the
* session manager.
*
* @see org.eclipse.jetty.server.session.SessionCache#get(java.lang.String)
*/
@Override
public Session get(String id) throws Exception
{
Session session = null;
Exception ex = null;
while (true)
{
session = doGet(id);
if (_sessionDataStore == null)
break; //can't load any session data so just return null or the session object
if (session == null)
{
if (LOG.isDebugEnabled())
LOG.debug("Session {} not found locally, attempting to load", id);
//didn't get a session, try and create one and put in a placeholder for it
PlaceHolderSession phs = new PlaceHolderSession(_handler, new SessionData(id, null, null, 0, 0, 0, 0));
Lock phsLock = phs.lock();
Session s = doPutIfAbsent(id, phs);
if (s == null)
{
//My placeholder won, go ahead and load the full session data
try
{
session = loadSession(id);
if (session == null)
{
//session does not exist, remove the placeholder
doDelete(id);
phsLock.close();
break;
}
try (Lock lock = session.lock())
{
//swap it in instead of the placeholder
boolean success = doReplace(id, phs, session);
if (!success)
{
//something has gone wrong, it should have been our placeholder
doDelete(id);
session = null;
LOG.warn("Replacement of placeholder for session {} failed", id);
phsLock.close();
break;
}
else
{
//successfully swapped in the session
session.setResident(true);
phsLock.close();
break;
}
}
}
catch (Exception e)
{
ex = e; //remember a problem happened loading the session
doDelete(id); //remove the placeholder
phsLock.close();
session = null;
break;
}
}
else
{
//my placeholder didn't win, check the session returned
phsLock.close();
try (Lock lock = s.lock())
{
//is it a placeholder? or is a non-resident session? In both cases, chuck it away and start again
if (!s.isResident() || s instanceof PlaceHolderSession)
{
session = null;
continue;
}
session = s;
break;
}
}
}
else
{
//check the session returned
try (Lock lock = session.lock())
{
//is it a placeholder? or is it passivated? In both cases, chuck it away and start again
if (!session.isResident() || session instanceof PlaceHolderSession)
{
session = null;
continue;
}
//got the session
break;
}
}
}
if (ex != null)
throw ex;
return session;
}
Load the info for the session from the session data store
Params: - id – the id
Returns: a Session object filled with data or null if the session doesn't exist
/**
* Load the info for the session from the session data store
*
* @param id the id
* @return a Session object filled with data or null if the session doesn't exist
*/
private Session loadSession(String id)
throws Exception
{
SessionData data = null;
Session session = null;
if (_sessionDataStore == null)
return null; //can't load it
try
{
data = _sessionDataStore.load(id);
if (data == null) //session doesn't exist
return null;
data.setLastNode(_context.getWorkerName());//we are going to manage the node
session = newSession(data);
return session;
}
catch (UnreadableSessionDataException e)
{
//can't load the session, delete it
if (isRemoveUnloadableSessions())
_sessionDataStore.delete(id);
throw e;
}
}
Put the Session object back into the session store.
This should be called when a request exists the session. Only when the last
simultaneous request exists the session will any action be taken.
If there is a SessionDataStore write the session data through to it.
If the SessionDataStore supports passivation, call the passivate/active listeners.
If the evictionPolicy == SessionCache.EVICT_ON_SESSION_EXIT then after we have saved
the session, we evict it from the cache.
See Also: - put.put(String, Session)
/**
* Put the Session object back into the session store.
*
* This should be called when a request exists the session. Only when the last
* simultaneous request exists the session will any action be taken.
*
* If there is a SessionDataStore write the session data through to it.
*
* If the SessionDataStore supports passivation, call the passivate/active listeners.
*
* If the evictionPolicy == SessionCache.EVICT_ON_SESSION_EXIT then after we have saved
* the session, we evict it from the cache.
*
* @see org.eclipse.jetty.server.session.SessionCache#put(java.lang.String, org.eclipse.jetty.server.session.Session)
*/
@Override
public void put(String id, Session session) throws Exception
{
if (id == null || session == null)
throw new IllegalArgumentException("Put key=" + id + " session=" + (session == null ? "null" : session.getId()));
try (Lock lock = session.lock())
{
if (session.getSessionHandler() == null)
throw new IllegalStateException("Session " + id + " is not managed");
if (!session.isValid())
return;
//don't do anything with the session until the last request for it has finished
if ((session.getRequests() <= 0))
{
//save the session
if (!_sessionDataStore.isPassivating())
{
//if our backing datastore isn't the passivating kind, just save the session
_sessionDataStore.store(id, session.getSessionData());
//if we evict on session exit, boot it from the cache
if (getEvictionPolicy() == EVICT_ON_SESSION_EXIT)
{
if (LOG.isDebugEnabled())
LOG.debug("Eviction on request exit id={}", id);
doDelete(session.getId());
session.setResident(false);
}
else
{
session.setResident(true);
doPutIfAbsent(id, session); //ensure it is in our map
if (LOG.isDebugEnabled())
LOG.debug("Non passivating SessionDataStore, session in SessionCache only id={}", id);
}
}
else
{
//backing store supports passivation, call the listeners
session.willPassivate();
if (LOG.isDebugEnabled())
LOG.debug("Session passivating id={}", id);
_sessionDataStore.store(id, session.getSessionData());
if (getEvictionPolicy() == EVICT_ON_SESSION_EXIT)
{
//throw out the passivated session object from the map
doDelete(id);
session.setResident(false);
if (LOG.isDebugEnabled())
LOG.debug("Evicted on request exit id={}", id);
}
else
{
//reactivate the session
session.didActivate();
session.setResident(true);
doPutIfAbsent(id, session);//ensure it is in our map
if (LOG.isDebugEnabled())
LOG.debug("Session reactivated id={}", id);
}
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Req count={} for id={}", session.getRequests(), id);
session.setResident(true);
doPutIfAbsent(id, session); //ensure it is the map, but don't save it to the backing store until the last request exists
}
}
}
Check to see if a session corresponding to the id exists.
This method will first check with the object store. If it
doesn't exist in the object store (might be passivated etc),
it will check with the data store.
Throws: - Exception – the Exception
See Also:
/**
* Check to see if a session corresponding to the id exists.
*
* This method will first check with the object store. If it
* doesn't exist in the object store (might be passivated etc),
* it will check with the data store.
*
* @throws Exception the Exception
* @see org.eclipse.jetty.server.session.SessionCache#exists(java.lang.String)
*/
@Override
public boolean exists(String id) throws Exception
{
//try the object store first
Session s = doGet(id);
if (s != null)
{
try (Lock lock = s.lock())
{
//wait for the lock and check the validity of the session
return s.isValid();
}
}
//not there, so find out if session data exists for it
return _sessionDataStore.exists(id);
}
Check to see if this cache contains an entry for the session
corresponding to the session id.
See Also: - contains.contains(String)
/**
* Check to see if this cache contains an entry for the session
* corresponding to the session id.
*
* @see org.eclipse.jetty.server.session.SessionCache#contains(java.lang.String)
*/
@Override
public boolean contains(String id) throws Exception
{
//just ask our object cache, not the store
return (doGet(id) != null);
}
Remove a session object from this store and from any backing store.
See Also: - delete.delete(String)
/**
* Remove a session object from this store and from any backing store.
*
* @see org.eclipse.jetty.server.session.SessionCache#delete(java.lang.String)
*/
@Override
public Session delete(String id) throws Exception
{
//get the session, if its not in memory, this will load it
Session session = get(id);
//Always delete it from the backing data store
if (_sessionDataStore != null)
{
boolean dsdel = _sessionDataStore.delete(id);
if (LOG.isDebugEnabled())
LOG.debug("Session {} deleted in session data store {}", id, dsdel);
}
//delete it from the session object store
if (session != null)
{
session.setResident(false);
}
return doDelete(id);
}
See Also: - checkExpiration.checkExpiration(Set)
/**
* @see org.eclipse.jetty.server.session.SessionCache#checkExpiration(Set)
*/
@Override
public Set<String> checkExpiration(Set<String> candidates)
{
if (!isStarted())
return Collections.emptySet();
if (LOG.isDebugEnabled())
LOG.debug("{} checking expiration on {}", this, candidates);
Set<String> allCandidates = _sessionDataStore.getExpired(candidates);
Set<String> sessionsInUse = new HashSet<>();
if (allCandidates != null)
{
for (String c : allCandidates)
{
Session s = doGet(c);
if (s != null && s.getRequests() > 0) //if the session is in my cache, check its not in use first
sessionsInUse.add(c);
}
try
{
allCandidates.removeAll(sessionsInUse);
}
catch (UnsupportedOperationException e)
{
Set<String> tmp = new HashSet<>(allCandidates);
tmp.removeAll(sessionsInUse);
allCandidates = tmp;
}
}
return allCandidates;
}
Check a session for being inactive and
thus being able to be evicted, if eviction
is enabled.
Params: - session – session to check
/**
* Check a session for being inactive and
* thus being able to be evicted, if eviction
* is enabled.
*
* @param session session to check
*/
@Override
public void checkInactiveSession(Session session)
{
if (session == null)
return;
if (LOG.isDebugEnabled())
LOG.debug("Checking for idle {}", session.getId());
try (Lock s = session.lock())
{
if (getEvictionPolicy() > 0 && session.isIdleLongerThan(getEvictionPolicy()) && session.isValid() && session.isResident() && session.getRequests() <= 0)
{
//Be careful with saveOnInactiveEviction - you may be able to re-animate a session that was
//being managed on another node and has expired.
try
{
if (LOG.isDebugEnabled())
LOG.debug("Evicting idle session {}", session.getId());
//save before evicting
if (isSaveOnInactiveEviction() && _sessionDataStore != null)
{
if (_sessionDataStore.isPassivating())
session.willPassivate();
_sessionDataStore.store(session.getId(), session.getSessionData());
}
doDelete(session.getId()); //detach from this cache
session.setResident(false);
}
catch (Exception e)
{
LOG.warn("Passivation of idle session {} failed", session.getId(), e);
//session.updateInactivityTimer();
}
}
}
}
@Override
public Session renewSessionId(String oldId, String newId, String oldExtendedId, String newExtendedId)
throws Exception
{
if (StringUtil.isBlank(oldId))
throw new IllegalArgumentException("Old session id is null");
if (StringUtil.isBlank(newId))
throw new IllegalArgumentException("New session id is null");
Session session = get(oldId);
renewSessionId(session, newId, newExtendedId);
return session;
}
Swap the id on a session.
Params: - session – the session for which to do the swap
- newId – the new id
- newExtendedId – the full id plus node id
Throws: - Exception – if there was a failure saving the change
/**
* Swap the id on a session.
*
* @param session the session for which to do the swap
* @param newId the new id
* @param newExtendedId the full id plus node id
* @throws Exception if there was a failure saving the change
*/
protected void renewSessionId(Session session, String newId, String newExtendedId)
throws Exception
{
if (session == null)
return;
try (Lock lock = session.lock())
{
final String oldId = session.getId();
session.checkValidForWrite(); //can't change id on invalid session
session.getSessionData().setId(newId);
session.getSessionData().setLastSaved(0); //pretend that the session has never been saved before to get a full save
session.getSessionData().setDirty(true); //ensure we will try to write the session out
session.setExtendedId(newExtendedId); //remember the new extended id
session.setIdChanged(true); //session id changed
doPutIfAbsent(newId, session); //put the new id into our map
doDelete(oldId); //take old out of map
if (_sessionDataStore != null)
{
_sessionDataStore.delete(oldId); //delete the session data with the old id
_sessionDataStore.store(newId, session.getSessionData()); //save the session data with the new id
}
if (LOG.isDebugEnabled())
LOG.debug("Session id {} swapped for new id {}", oldId, newId);
}
}
See Also: - setSaveOnInactiveEviction.setSaveOnInactiveEviction(boolean)
/**
* @see org.eclipse.jetty.server.session.SessionCache#setSaveOnInactiveEviction(boolean)
*/
@Override
public void setSaveOnInactiveEviction(boolean saveOnEvict)
{
_saveOnInactiveEviction = saveOnEvict;
}
Whether we should save a session that has been inactive before
we boot it from the cache.
Returns: true if an inactive session will be saved before being evicted
/**
* Whether we should save a session that has been inactive before
* we boot it from the cache.
*
* @return true if an inactive session will be saved before being evicted
*/
@ManagedAttribute(value = "save sessions before evicting from cache", readonly = true)
@Override
public boolean isSaveOnInactiveEviction()
{
return _saveOnInactiveEviction;
}
@Override
public String toString()
{
return String.format("%s@%x[evict=%d,removeUnloadable=%b,saveOnCreate=%b,saveOnInactiveEvict=%b]",
this.getClass().getName(), this.hashCode(), _evictionPolicy, _removeUnloadableSessions, _saveOnCreate, _saveOnInactiveEviction);
}
}