blob: 6480b97d2e493bd5b62cd77767c6a636f838df40 [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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.server.SessionIdManager;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.session.JDBCSessionIdManager.SessionTableSchema;
import org.eclipse.jetty.util.ClassLoadingObjectInputStream;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* JDBCSessionManager.
* <p>
* SessionManager that persists sessions to a database to enable clustering.
* <p>
* Session data is persisted to the JettySessions table:
* <dl>
* <dt>rowId</dt><dd>(unique in cluster: webapp name/path + virtualhost + sessionId)</dd>
* <dt>contextPath</dt><dd>(of the context owning the session)</dd>
* <dt>sessionId</dt><dd>(unique in a context)</dd>
* <dt>lastNode</dt><dd>(name of node last handled session)</dd>
* <dt>accessTime</dt><dd>(time in milliseconds session was accessed)</dd>
* <dt>lastAccessTime</dt><dd>(previous time in milliseconds session was accessed)</dd>
* <dt>createTime</dt><dd>(time in milliseconds session created)</dd>
* <dt>cookieTime</dt><dd>(time in milliseconds session cookie created)</dd>
* <dt>lastSavedTime</dt><dd>(last time in milliseconds session access times were saved)</dd>
* <dt>expiryTime</dt><dd>(time in milliseconds that the session is due to expire)</dd>
* <dt>map</dt><dd>(attribute map)</dd>
* </dl>
*
* As an optimization, to prevent thrashing the database, we do not persist
* the accessTime and lastAccessTime every time the session is accessed. Rather,
* we write it out every so often. The frequency is controlled by the saveIntervalSec
* field.
*/
public class JDBCSessionManager extends AbstractSessionManager
{
private static final Logger LOG = Log.getLogger(JDBCSessionManager.class);
private ConcurrentHashMap<String, Session> _sessions;
protected JDBCSessionIdManager _jdbcSessionIdMgr = null;
protected long _saveIntervalSec = 60; //only persist changes to session access times every 60 secs
protected SessionTableSchema _sessionTableSchema;
/**
* Session
*
* Session instance.
*/
public class Session extends MemSession
{
private static final long serialVersionUID = 5208464051134226143L;
/**
* If dirty, session needs to be (re)persisted
*/
protected boolean _dirty=false;
/**
* Time in msec since the epoch that the session will expire
*/
protected long _expiryTime;
/**
* Time in msec since the epoch that the session was last persisted
*/
protected long _lastSaved;
/**
* Unique identifier of the last node to host the session
*/
protected String _lastNode;
/**
* Virtual host for context (used to help distinguish 2 sessions with same id on different contexts)
*/
protected String _virtualHost;
/**
* Unique row in db for session
*/
protected String _rowId;
/**
* Mangled context name (used to help distinguish 2 sessions with same id on different contexts)
*/
protected String _canonicalContext;
/**
* Session from a request.
*
* @param request the request
*/
protected Session (HttpServletRequest request)
{
super(JDBCSessionManager.this,request);
int maxInterval=getMaxInactiveInterval();
_expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
_virtualHost = JDBCSessionManager.getVirtualHost(_context);
_canonicalContext = canonicalize(_context.getContextPath());
_lastNode = getSessionIdManager().getWorkerName();
}
/**
* Session restored from database
* @param sessionId the session id
* @param rowId the row id
* @param created the created timestamp
* @param accessed the access timestamp
* @param maxInterval the max inactive interval (in seconds)
*/
protected Session (String sessionId, String rowId, long created, long accessed, long maxInterval)
{
super(JDBCSessionManager.this, created, accessed, sessionId);
_rowId = rowId;
super.setMaxInactiveInterval((int)maxInterval); //restore the session's previous inactivity interval setting
_expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
}
protected synchronized String getRowId()
{
return _rowId;
}
protected synchronized void setRowId(String rowId)
{
_rowId = rowId;
}
public synchronized void setVirtualHost (String vhost)
{
_virtualHost=vhost;
}
public synchronized String getVirtualHost ()
{
return _virtualHost;
}
public synchronized long getLastSaved ()
{
return _lastSaved;
}
public synchronized void setLastSaved (long time)
{
_lastSaved=time;
}
public synchronized void setExpiryTime (long time)
{
_expiryTime=time;
}
public synchronized long getExpiryTime ()
{
return _expiryTime;
}
public synchronized void setCanonicalContext(String str)
{
_canonicalContext=str;
}
public synchronized String getCanonicalContext ()
{
return _canonicalContext;
}
public synchronized void setLastNode (String node)
{
_lastNode=node;
}
public synchronized String getLastNode ()
{
return _lastNode;
}
@Override
public void setAttribute (String name, Object value)
{
Object old = changeAttribute(name, value);
if (value == null && old == null)
return; //if same as remove attribute but attribute was already removed, no change
_dirty = true;
}
@Override
public void removeAttribute (String name)
{
Object old = changeAttribute(name, null);
if (old != null) //only dirty if there was a previous value
_dirty=true;
}
/**
* Entry to session.
* Called by SessionHandler on inbound request and the session already exists in this node's memory.
*
* @see org.eclipse.jetty.server.session.AbstractSession#access(long)
*/
@Override
protected boolean access(long time)
{
synchronized (this)
{
if (super.access(time))
{
int maxInterval=getMaxInactiveInterval();
_expiryTime = (maxInterval <= 0 ? 0 : (time + maxInterval*1000L));
return true;
}
return false;
}
}
/**
* Change the max idle time for this session. This recalculates the expiry time.
* @see org.eclipse.jetty.server.session.AbstractSession#setMaxInactiveInterval(int)
*/
@Override
public void setMaxInactiveInterval(int secs)
{
synchronized (this)
{
super.setMaxInactiveInterval(secs);
int maxInterval=getMaxInactiveInterval();
_expiryTime = (maxInterval <= 0 ? 0 : (System.currentTimeMillis() + maxInterval*1000L));
//force the session to be written out right now
try
{
updateSessionAccessTime(this);
}
catch (Exception e)
{
LOG.warn("Problem saving changed max idle time for session "+ this, e);
}
}
}
/**
* Exit from session
* @see org.eclipse.jetty.server.session.AbstractSession#complete()
*/
@Override
protected void complete()
{
synchronized (this)
{
super.complete();
try
{
if (isValid())
{
if (_dirty)
{
//The session attributes have changed, write to the db, ensuring
//http passivation/activation listeners called
save(true);
}
else if ((getAccessed() - _lastSaved) >= (getSaveInterval() * 1000L))
{
updateSessionAccessTime(this);
}
}
}
catch (Exception e)
{
LOG.warn("Problem persisting changed session data id="+getId(), e);
}
finally
{
_dirty=false;
}
}
}
protected void save() throws Exception
{
synchronized (this)
{
try
{
updateSession(this);
}
finally
{
_dirty = false;
}
}
}
protected void save (boolean reactivate) throws Exception
{
synchronized (this)
{
if (_dirty)
{
//The session attributes have changed, write to the db, ensuring
//http passivation/activation listeners called
willPassivate();
updateSession(this);
if (reactivate)
didActivate();
}
}
}
@Override
protected void timeout() throws IllegalStateException
{
if (LOG.isDebugEnabled())
LOG.debug("Timing out session id="+getClusterId());
super.timeout();
}
@Override
public String toString ()
{
return "Session rowId="+_rowId+",id="+getId()+",lastNode="+_lastNode+
",created="+getCreationTime()+",accessed="+getAccessed()+
",lastAccessed="+getLastAccessedTime()+",cookieSet="+getCookieSetTime()+
",maxInterval="+getMaxInactiveInterval()+",lastSaved="+_lastSaved+",expiry="+_expiryTime;
}
}
/**
* Set the time in seconds which is the interval between
* saving the session access time to the database.
*
* This is an optimization that prevents the database from
* being overloaded when a session is accessed very frequently.
*
* On session exit, if the session attributes have NOT changed,
* the time at which we last saved the accessed
* time is compared to the current accessed time. If the interval
* is at least saveIntervalSecs, then the access time will be
* persisted to the database.
*
* If any session attribute does change, then the attributes and
* the accessed time are persisted.
*
* @param sec the save interval in seconds
*/
public void setSaveInterval (long sec)
{
_saveIntervalSec=sec;
}
public long getSaveInterval ()
{
return _saveIntervalSec;
}
/**
* A method that can be implemented in subclasses to support
* distributed caching of sessions. This method will be
* called whenever the session is written to the database
* because the session data has changed.
*
* This could be used eg with a JMS backplane to notify nodes
* that the session has changed and to delete the session from
* the node's cache, and re-read it from the database.
* @param session the session to invalidate
*/
public void cacheInvalidate (Session session)
{
}
/**
* A session has been requested by its id on this node.
*
* Load the session by id AND context path from the database.
* Multiple contexts may share the same session id (due to dispatching)
* but they CANNOT share the same contents.
*
* Check if last node id is my node id, if so, then the session we have
* in memory cannot be stale. If another node used the session last, then
* we need to refresh from the db.
*
* NOTE: this method will go to the database, so if you only want to check
* for the existence of a Session in memory, use _sessions.get(id) instead.
*
* @see org.eclipse.jetty.server.session.AbstractSessionManager#getSession(java.lang.String)
*/
@Override
public Session getSession(String idInCluster)
{
Session session = null;
synchronized (this)
{
Session memSession = (Session)_sessions.get(idInCluster);
//check if we need to reload the session -
//as an optimization, don't reload on every access
//to reduce the load on the database. This introduces a window of
//possibility that the node may decide that the session is local to it,
//when the session has actually been live on another node, and then
//re-migrated to this node. This should be an extremely rare occurrence,
//as load-balancers are generally well-behaved and consistently send
//sessions to the same node, changing only iff that node fails.
//Session data = null;
long now = System.currentTimeMillis();
if (LOG.isDebugEnabled())
{
if (memSession==null)
LOG.debug("getSession("+idInCluster+"): not in session map,"+
" now="+now+
" lastSaved="+(memSession==null?0:memSession._lastSaved)+
" interval="+(_saveIntervalSec * 1000L));
else
LOG.debug("getSession("+idInCluster+"): in session map, "+
" hashcode="+memSession.hashCode()+
" now="+now+
" lastSaved="+(memSession==null?0:memSession._lastSaved)+
" interval="+(_saveIntervalSec * 1000L)+
" lastNode="+memSession._lastNode+
" thisNode="+getSessionIdManager().getWorkerName()+
" difference="+(now - memSession._lastSaved));
}
try
{
if (memSession==null)
{
if (LOG.isDebugEnabled())
LOG.debug("getSession("+idInCluster+"): no session in session map. Reloading session data from db.");
session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
}
else if ((now - memSession._lastSaved) >= (_saveIntervalSec * 1000L))
{
if (LOG.isDebugEnabled())
LOG.debug("getSession("+idInCluster+"): stale session. Reloading session data from db.");
session = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context));
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("getSession("+idInCluster+"): session in session map");
session = memSession;
}
}
catch (Exception e)
{
LOG.warn("Unable to load session "+idInCluster, e);
return null;
}
//If we have a session
if (session != null)
{
//If the session was last used on a different node, or session doesn't exist on this node
if (!session.getLastNode().equals(getSessionIdManager().getWorkerName()) || memSession==null)
{
//if session doesn't expire, or has not already expired, update it and put it in this nodes' memory
if (session._expiryTime <= 0 || session._expiryTime > now)
{
if (LOG.isDebugEnabled())
LOG.debug("getSession("+idInCluster+"): lastNode="+session.getLastNode()+" thisNode="+getSessionIdManager().getWorkerName());
session.setLastNode(getSessionIdManager().getWorkerName());
_sessions.put(idInCluster, session);
//update in db
try
{
updateSessionNode(session);
session.didActivate();
}
catch (Exception e)
{
LOG.warn("Unable to update freshly loaded session "+idInCluster, e);
return null;
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("getSession ({}): Session has expired", idInCluster);
//ensure that the session id for the expired session is deleted so that a new session with the
//same id cannot be created (because the idInUse() test would succeed)
_jdbcSessionIdMgr.removeSession(idInCluster);
session=null;
}
}
else
{
//the session loaded from the db and the one in memory are the same, so keep using the one in memory
session = memSession;
if (LOG.isDebugEnabled())
LOG.debug("getSession({}): Session not stale {}", idInCluster,session);
}
}
else
{
if (memSession != null)
{
//Session must have been removed from db by another node
removeSession(memSession, true);
}
//No session in db with matching id and context path.
LOG.debug("getSession({}): No session in database matching id={}",idInCluster,idInCluster);
}
return session;
}
}
/**
* Get the number of sessions.
*
* @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessions()
*/
@Override
public int getSessions()
{
return _sessions.size();
}
/**
* Start the session manager.
*
* @see org.eclipse.jetty.server.session.AbstractSessionManager#doStart()
*/
@Override
public void doStart() throws Exception
{
if (_sessionIdManager==null)
throw new IllegalStateException("No session id manager defined");
_jdbcSessionIdMgr = (JDBCSessionIdManager)_sessionIdManager;
_sessionTableSchema = _jdbcSessionIdMgr.getSessionTableSchema();
_sessions = new ConcurrentHashMap<String, Session>();
super.doStart();
}
/**
* Stop the session manager.
*
* @see org.eclipse.jetty.server.session.AbstractSessionManager#doStop()
*/
@Override
public void doStop() throws Exception
{
super.doStop();
_sessions.clear();
_sessions = null;
}
@Override
protected void shutdownSessions()
{
//Save the current state of all of our sessions,
//do NOT delete them (so other nodes can manage them)
long gracefulStopMs = getContextHandler().getServer().getStopTimeout();
long stopTime = 0;
if (gracefulStopMs > 0)
stopTime = System.nanoTime() + (TimeUnit.NANOSECONDS.convert(gracefulStopMs, TimeUnit.MILLISECONDS));
ArrayList<Session> sessions = (_sessions == null? new ArrayList<Session>() :new ArrayList<Session>(_sessions.values()) );
// loop while there are sessions, and while there is stop time remaining, or if no stop time, just 1 loop
while (sessions.size() > 0 && ((stopTime > 0 && (System.nanoTime() < stopTime)) || (stopTime == 0)))
{
for (Session session : sessions)
{
try
{
session.save(false);
}
catch (Exception e)
{
LOG.warn(e);
}
_sessions.remove(session.getClusterId());
}
//check if we should terminate our loop if we're not using the stop timer
if (stopTime == 0)
break;
// Get any sessions that were added by other requests during processing and go around the loop again
sessions=new ArrayList<Session>(_sessions.values());
}
}
/**
*
* @see org.eclipse.jetty.server.SessionManager#renewSessionId(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
public void renewSessionId (String oldClusterId, String oldNodeId, String newClusterId, String newNodeId)
{
Session session = null;
try
{
session = (Session)_sessions.remove(oldClusterId);
if (session != null)
{
synchronized (session)
{
session.setClusterId(newClusterId); //update ids
session.setNodeId(newNodeId);
_sessions.put(newClusterId, session); //put it into list in memory
updateSession(session); //update database
}
}
}
catch (Exception e)
{
LOG.warn(e);
}
super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
}
/**
* Invalidate a session.
*
* @param idInCluster the id in the cluster
*/
protected void invalidateSession (String idInCluster)
{
Session session = (Session)_sessions.get(idInCluster);
if (session != null)
{
session.invalidate();
}
}
/**
* Delete an existing session, both from the in-memory map and
* the database.
*
* @see org.eclipse.jetty.server.session.AbstractSessionManager#removeSession(java.lang.String)
*/
@Override
protected boolean removeSession(String idInCluster)
{
Session session = (Session)_sessions.remove(idInCluster);
try
{
if (session != null)
deleteSession(session);
}
catch (Exception e)
{
LOG.warn("Problem deleting session id="+idInCluster, e);
}
return session!=null;
}
/**
* Add a newly created session to our in-memory list for this node and persist it.
*
* @see org.eclipse.jetty.server.session.AbstractSessionManager#addSession(org.eclipse.jetty.server.session.AbstractSession)
*/
@Override
protected void addSession(AbstractSession session)
{
if (session==null)
return;
_sessions.put(session.getClusterId(), (Session)session);
try
{
synchronized (session)
{
session.willPassivate();
storeSession(((JDBCSessionManager.Session)session));
session.didActivate();
}
}
catch (Exception e)
{
LOG.warn("Unable to store new session id="+session.getId() , e);
}
}
/**
* Make a new Session.
*
* @see org.eclipse.jetty.server.session.AbstractSessionManager#newSession(javax.servlet.http.HttpServletRequest)
*/
@Override
protected AbstractSession newSession(HttpServletRequest request)
{
return new Session(request);
}
protected AbstractSession newSession (String sessionId, String rowId, long created, long accessed, long maxInterval)
{
return new Session(sessionId, rowId, created, accessed, maxInterval);
}
/* ------------------------------------------------------------ */
/** Remove session from manager
* @param session The session to remove
* @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and
* {@link SessionIdManager#invalidateAll(String)} should be called.
*/
@Override
public boolean removeSession(AbstractSession session, boolean invalidate)
{
// Remove session from context and global maps
boolean removed = super.removeSession(session, invalidate);
if (removed)
{
if (!invalidate)
{
session.willPassivate();
}
}
return removed;
}
/**
* Expire any Sessions we have in memory matching the list of
* expired Session ids.
*
* @param sessionIds the session ids to expire
* @return the set of successfully expired ids
*/
protected Set<String> expire (Set<String> sessionIds)
{
//don't attempt to scavenge if we are shutting down
if (isStopping() || isStopped())
return null;
Thread thread=Thread.currentThread();
ClassLoader old_loader=thread.getContextClassLoader();
Set<String> successfullyExpiredIds = new HashSet<String>();
try
{
Iterator<?> itor = sessionIds.iterator();
while (itor.hasNext())
{
String sessionId = (String)itor.next();
if (LOG.isDebugEnabled())
LOG.debug("Expiring session id "+sessionId);
Session session = (Session)_sessions.get(sessionId);
//if session is not in our memory, then fetch from db so we can call the usual listeners on it
if (session == null)
{
if (LOG.isDebugEnabled())LOG.debug("Force loading session id "+sessionId);
session = loadSession(sessionId, canonicalize(_context.getContextPath()), getVirtualHost(_context));
if (session != null)
{
//loaded an expired session last managed on this node for this context, add it to the list so we can
//treat it like a normal expired session
_sessions.put(session.getClusterId(), session);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Unrecognized session id="+sessionId);
continue;
}
}
if (session != null)
{
session.timeout();
successfullyExpiredIds.add(session.getClusterId());
}
}
return successfullyExpiredIds;
}
catch (Throwable t)
{
LOG.warn("Problem expiring sessions", t);
return successfullyExpiredIds;
}
finally
{
thread.setContextClassLoader(old_loader);
}
}
protected void expireCandidates (Set<String> candidateIds)
{
Iterator<String> itor = candidateIds.iterator();
long now = System.currentTimeMillis();
while (itor.hasNext())
{
String id = itor.next();
//check if expired in db
try
{
Session memSession = _sessions.get(id);
if (memSession == null)
{
continue; //no longer in memory
}
Session s = loadSession(id, canonicalize(_context.getContextPath()), getVirtualHost(_context));
if (s == null)
{
//session no longer exists, can be safely expired
memSession.timeout();
}
}
catch (Exception e)
{
LOG.warn("Error checking db for expiry for session {}", id);
}
}
}
protected Set<String> getCandidateExpiredIds ()
{
HashSet<String> expiredIds = new HashSet<>();
Iterator<String> itor = _sessions.keySet().iterator();
while (itor.hasNext())
{
String id = itor.next();
//check to see if session should have expired
Session session = _sessions.get(id);
if (session._expiryTime > 0 && System.currentTimeMillis() > session._expiryTime)
expiredIds.add(id);
}
return expiredIds;
}
/**
* Load a session from the database
* @param id the id
* @param canonicalContextPath the canonical context path
* @param vhost the virtual host
* @return the session data that was loaded
* @throws Exception if unable to load the session
*/
protected Session loadSession (final String id, final String canonicalContextPath, final String vhost)
throws Exception
{
final AtomicReference<Session> _reference = new AtomicReference<Session>();
final AtomicReference<Exception> _exception = new AtomicReference<Exception>();
Runnable load = new Runnable()
{
/**
* @see java.lang.Runnable#run()
*/
@SuppressWarnings("unchecked")
public void run()
{
try (Connection connection = getConnection();
PreparedStatement statement = _sessionTableSchema.getLoadStatement(connection, id, canonicalContextPath, vhost);
ResultSet result = statement.executeQuery())
{
Session session = null;
if (result.next())
{
long maxInterval = result.getLong(_sessionTableSchema.getMaxIntervalColumn());
if (maxInterval == JDBCSessionIdManager.MAX_INTERVAL_NOT_SET)
{
maxInterval = getMaxInactiveInterval(); //if value not saved for maxInactiveInterval, use current value from sessionmanager
}
session = (Session)newSession(id, result.getString(_sessionTableSchema.getRowIdColumn()),
result.getLong(_sessionTableSchema.getCreateTimeColumn()),
result.getLong(_sessionTableSchema.getAccessTimeColumn()),
maxInterval);
session.setCookieSetTime(result.getLong(_sessionTableSchema.getCookieTimeColumn()));
session.setLastAccessedTime(result.getLong(_sessionTableSchema.getLastAccessTimeColumn()));
session.setLastNode(result.getString(_sessionTableSchema.getLastNodeColumn()));
session.setLastSaved(result.getLong(_sessionTableSchema.getLastSavedTimeColumn()));
session.setExpiryTime(result.getLong(_sessionTableSchema.getExpiryTimeColumn()));
session.setCanonicalContext(result.getString(_sessionTableSchema.getContextPathColumn()));
session.setVirtualHost(result.getString(_sessionTableSchema.getVirtualHostColumn()));
try (InputStream is = ((JDBCSessionIdManager)getSessionIdManager())._dbAdaptor.getBlobInputStream(result, _sessionTableSchema.getMapColumn());
ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(is))
{
Object o = ois.readObject();
session.addAttributes((Map<String,Object>)o);
}
if (LOG.isDebugEnabled())
LOG.debug("LOADED session "+session);
}
else
if (LOG.isDebugEnabled())
LOG.debug("Failed to load session "+id);
_reference.set(session);
}
catch (Exception e)
{
_exception.set(e);
}
}
};
if (_context==null)
load.run();
else
_context.getContextHandler().handle(null,load);
if (_exception.get()!=null)
{
//if the session could not be restored, take its id out of the pool of currently-in-use
//session ids
_jdbcSessionIdMgr.removeSession(id);
throw _exception.get();
}
return _reference.get();
}
/**
* Insert a session into the database.
*
* @param session the session
* @throws Exception if unable to store the session
*/
protected void storeSession (Session session)
throws Exception
{
if (session==null)
return;
//put into the database
try (Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._insertSession))
{
String rowId = calculateRowId(session);
long now = System.currentTimeMillis();
connection.setAutoCommit(true);
statement.setString(1, rowId); //rowId
statement.setString(2, session.getClusterId()); //session id
statement.setString(3, session.getCanonicalContext()); //context path
statement.setString(4, session.getVirtualHost()); //first vhost
statement.setString(5, getSessionIdManager().getWorkerName());//my node id
statement.setLong(6, session.getAccessed());//accessTime
statement.setLong(7, session.getLastAccessedTime()); //lastAccessTime
statement.setLong(8, session.getCreationTime()); //time created
statement.setLong(9, session.getCookieSetTime());//time cookie was set
statement.setLong(10, now); //last saved time
statement.setLong(11, session.getExpiryTime());
statement.setLong(12, session.getMaxInactiveInterval());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(session.getAttributeMap());
oos.flush();
byte[] bytes = baos.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
statement.setBinaryStream(13, bais, bytes.length);//attribute map as blob
statement.executeUpdate();
session.setRowId(rowId); //set it on the in-memory data as well as in db
session.setLastSaved(now);
}
if (LOG.isDebugEnabled())
LOG.debug("Stored session "+session);
}
/**
* Update data on an existing persisted session.
*
* @param data the session
* @throws Exception if unable to update the session
*/
protected void updateSession (Session data)
throws Exception
{
if (data==null)
return;
try (Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSession))
{
long now = System.currentTimeMillis();
connection.setAutoCommit(true);
statement.setString(1, data.getClusterId());
statement.setString(2, getSessionIdManager().getWorkerName());//my node id
statement.setLong(3, data.getAccessed());//accessTime
statement.setLong(4, data.getLastAccessedTime()); //lastAccessTime
statement.setLong(5, now); //last saved time
statement.setLong(6, data.getExpiryTime());
statement.setLong(7, data.getMaxInactiveInterval());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(data.getAttributeMap());
oos.flush();
byte[] bytes = baos.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
statement.setBinaryStream(8, bais, bytes.length);//attribute map as blob
statement.setString(9, data.getRowId()); //rowId
statement.executeUpdate();
data.setLastSaved(now);
}
if (LOG.isDebugEnabled())
LOG.debug("Updated session "+data);
}
/**
* Update the node on which the session was last seen to be my node.
*
* @param data the session
* @throws Exception if unable to update the session node
*/
protected void updateSessionNode (Session data)
throws Exception
{
String nodeId = getSessionIdManager().getWorkerName();
try (Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionNode))
{
connection.setAutoCommit(true);
statement.setString(1, nodeId);
statement.setString(2, data.getRowId());
statement.executeUpdate();
}
if (LOG.isDebugEnabled())
LOG.debug("Updated last node for session id="+data.getId()+", lastNode = "+nodeId);
}
/**
* Persist the time the session was last accessed.
*
* @param data the session
* @throws Exception
*/
private void updateSessionAccessTime (Session data)
throws Exception
{
try (Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._updateSessionAccessTime))
{
long now = System.currentTimeMillis();
connection.setAutoCommit(true);
statement.setString(1, getSessionIdManager().getWorkerName());
statement.setLong(2, data.getAccessed());
statement.setLong(3, data.getLastAccessedTime());
statement.setLong(4, now);
statement.setLong(5, data.getExpiryTime());
statement.setLong(6, data.getMaxInactiveInterval());
statement.setString(7, data.getRowId());
statement.executeUpdate();
data.setLastSaved(now);
}
if (LOG.isDebugEnabled())
LOG.debug("Updated access time session id="+data.getId()+" with lastsaved="+data.getLastSaved());
}
/**
* Delete a session from the database. Should only be called
* when the session has been invalidated.
*
* @param data the session data
* @throws Exception if unable to delete the session
*/
protected void deleteSession (Session data)
throws Exception
{
try (Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(_jdbcSessionIdMgr._deleteSession))
{
connection.setAutoCommit(true);
statement.setString(1, data.getRowId());
statement.executeUpdate();
if (LOG.isDebugEnabled())
LOG.debug("Deleted Session "+data);
}
}
/**
* Get a connection from the driver.
* @return
* @throws SQLException
*/
private Connection getConnection ()
throws SQLException
{
return ((JDBCSessionIdManager)getSessionIdManager()).getConnection();
}
/**
* Calculate a unique id for this session across the cluster.
*
* Unique id is composed of: contextpath_virtualhost0_sessionid
* @param data
* @return
*/
private String calculateRowId (Session data)
{
String rowId = canonicalize(_context.getContextPath());
rowId = rowId + "_" + getVirtualHost(_context);
rowId = rowId+"_"+data.getId();
return rowId;
}
/**
* 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 static String getVirtualHost (ContextHandler.Context 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];
}
/**
* Make an acceptable file name from a context path.
*
* @param path
* @return
*/
private static String canonicalize (String path)
{
if (path==null)
return "";
return path.replace('/', '_').replace('.','_').replace('\\','_');
}
}