// // ======================================================================== // Copyright (c) 1995-2015 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. *

* SessionManager that persists sessions to a database to enable clustering. *

* Session data is persisted to the JettySessions table: *

*
rowId
(unique in cluster: webapp name/path + virtualhost + sessionId)
*
contextPath
(of the context owning the session)
*
sessionId
(unique in a context)
*
lastNode
(name of node last handled session)
*
accessTime
(time in milliseconds session was accessed)
*
lastAccessTime
(previous time in milliseconds session was accessed)
*
createTime
(time in milliseconds session created)
*
cookieTime
(time in milliseconds session cookie created)
*
lastSavedTime
(last time in milliseconds session access times were saved)
*
expiryTime
(time in milliseconds that the session is due to expire)
*
map
(attribute map)
*
* * 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 _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(); 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 sessions = (_sessions == null? new ArrayList() :new ArrayList(_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(_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 expire (Set 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 successfullyExpiredIds = new HashSet(); 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 candidateIds) { Iterator 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 getCandidateExpiredIds () { HashSet expiredIds = new HashSet<>(); Iterator 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 _reference = new AtomicReference(); final AtomicReference _exception = new AtomicReference(); 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)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('\\','_'); } }