/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.undertow.session; import java.security.AccessController; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import io.undertow.UndertowMessages; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.server.session.SecureRandomSessionIdGenerator; import io.undertow.server.session.Session; import io.undertow.server.session.SessionConfig; import io.undertow.server.session.SessionIdGenerator; import io.undertow.server.session.SessionListener; import io.undertow.servlet.api.Deployment; import org.jboss.as.clustering.web.BatchingManager; import org.jboss.as.clustering.web.ClusteringNotSupportedException; import org.jboss.as.clustering.web.DistributableSessionMetadata; import org.jboss.as.clustering.web.DistributedCacheManager; import org.jboss.as.clustering.web.DistributedCacheManagerFactory; import org.jboss.as.clustering.web.IncomingDistributableSessionData; import org.jboss.as.clustering.web.LocalDistributableSessionManager; import org.jboss.as.clustering.web.OutgoingAttributeGranularitySessionData; import org.jboss.as.clustering.web.OutgoingDistributableSessionData; import org.jboss.as.clustering.web.OutgoingSessionGranularitySessionData; import org.jboss.as.undertow.UndertowLogger; import org.jboss.as.undertow.session.notification.ClusteredSessionNotificationCapability; import org.jboss.as.undertow.session.notification.ClusteredSessionNotificationCause; import org.jboss.as.undertow.session.notification.ClusteredSessionNotificationPolicy; import org.jboss.as.undertow.session.notification.IgnoreUndeployLegacyClusteredSessionNotificationPolicy; import org.jboss.as.util.security.ReadPropertyAction; import org.jboss.logging.Logger; import org.jboss.marshalling.ClassResolver; import org.jboss.metadata.web.jboss.JBossWebMetaData; import org.jboss.metadata.web.jboss.PassivationConfig; import org.jboss.metadata.web.jboss.ReplicationConfig; import org.jboss.metadata.web.jboss.ReplicationGranularity; import org.jboss.metadata.web.jboss.ReplicationTrigger; import org.jboss.metadata.web.jboss.SnapshotMode; import static org.jboss.as.undertow.UndertowMessages.MESSAGES; /** * @author Paul Ferraro */ public class DistributableSessionManager<O extends OutgoingDistributableSessionData> extends AbstractSessionManager implements LocalDistributableSessionManager, ClusteredSessionManager<O> { private static final String info = "DistributableSessionManager/1.0"; private static final int TOTAL_PERMITS = Integer.MAX_VALUE; private final DistributedCacheManager<O> distributedCacheManager; private SnapshotManager snapshotManager; private final ReplicationConfig replicationConfig; private final ClassResolver resolver; private ClusteredSessionNotificationPolicy notificationPolicy; private final OutdatedSessionChecker outdatedSessionChecker = new AskSessionOutdatedSessionChecker(); private final Semaphore semaphore = new Semaphore(TOTAL_PERMITS, true); private final Lock valveLock = new SemaphoreLock(this.semaphore); /** * Number of passivated sessions */ private final AtomicInteger passivatedCount = new AtomicInteger(); /** * Maximum number of concurrently passivated sessions */ private final AtomicInteger maxPassivatedCount = new AtomicInteger(); /** * Session passivation flag set in jboss-web.xml by the user. If true, then the session passivation is enabled for this web * application, otherwise, it's disabled */ private final boolean passivate; /** * Min time (milliseconds) the session must be idle since lastAccesstime before it's eligible for passivation if passivation * is enabled and more than maxActiveAllowed sessions are in memory. Setting to -1 means it's ignored. */ private final long passivationMinIdleTime; /** * Max time (milliseconds) the session must be idle since lastAccesstime before it will be passivated if passivation is * enabled. Setting to -1 means session should not be forced out. */ private final long passivationMaxIdleTime; /** * Whether the underlying cache supports persistence */ private final boolean persistence; private volatile int maxUnreplicatedInterval; protected int maxInactiveInterval = 30 * 60; /** * Id/timestamp of sessions in distributed cache that we haven't loaded locally */ private final Map<String, OwnedSessionUpdate> unloadedSessions = new ConcurrentHashMap<String, OwnedSessionUpdate>(); /** * Sessions that have been created but not yet loaded. Used to ensure concurrent threads trying to load the same session */ private final ConcurrentMap<String, ClusteredSession<O>> embryonicSessions = new ConcurrentHashMap<String, ClusteredSession<O>>(); private final SessionIdGenerator sessionIdGenerator = new SecureRandomSessionIdGenerator(); private final String contextPath; private final ClassLoader classLoader; private final String jvmRoute; private static final Logger log = Logger.getLogger(DistributableSessionManager.class); public DistributableSessionManager(DistributedCacheManagerFactory factory, JBossWebMetaData metaData, ClassResolver resolver, final String contextPath, final ClassLoader classLoader, final String jvmRoute) { super(metaData); this.contextPath = contextPath; this.classLoader = classLoader; this.jvmRoute = jvmRoute; PassivationConfig passivationConfig = metaData.getPassivationConfig(); Boolean useSessionPassivation = (passivationConfig != null) ? passivationConfig.getUseSessionPassivation() : null; this.passivate = (useSessionPassivation != null) ? useSessionPassivation.booleanValue() : false; Integer minIdleTime = (passivationConfig != null) ? passivationConfig.getPassivationMinIdleTime() : null; this.passivationMinIdleTime = (minIdleTime != null) && this.passivate ? minIdleTime.intValue() : -1; Integer maxIdleTime = (passivationConfig != null) ? passivationConfig.getPassivationMaxIdleTime() : null; this.passivationMaxIdleTime = (maxIdleTime != null) && this.passivate ? maxIdleTime.intValue() : -1; ReplicationConfig config = metaData.getReplicationConfig(); this.replicationConfig = (config != null) ? config : new ReplicationConfig(); if (this.replicationConfig.getReplicationGranularity() == ReplicationGranularity.FIELD) { this.replicationConfig.setReplicationGranularity(ReplicationGranularity.SESSION); } Integer interval = this.replicationConfig.getMaxUnreplicatedInterval(); this.maxUnreplicatedInterval = (interval != null) ? interval.intValue() : -1; this.notificationPolicy = this.createClusteredSessionNotificationPolicy(); this.resolver = resolver; try { this.distributedCacheManager = factory.getDistributedCacheManager(this); } catch (ClusteringNotSupportedException e) { throw new RuntimeException(e); } this.persistence = distributedCacheManager.isPersistenceEnabled(); } @Override public synchronized void start() { if (this.started) return; super.start(); this.notificationPolicy = this.createClusteredSessionNotificationPolicy(); // Start the DistributedCacheManager // Will need to pass the classloader that is associated with this // web app so de-serialization will work correctly. try { this.distributedCacheManager.start(); initializeUnloadedSessions(); // Setup our SnapshotManager this.snapshotManager = createSnapshotManager(); this.snapshotManager.start(); log.debug("start(): DistributedCacheManager started"); } catch (Exception e) { throw MESSAGES.failToStartManager(e); } // Handle re-entrance if (!this.semaphore.tryAcquire()) { log.debug("Opening up LockingValve"); // Make all permits available to locking valve this.semaphore.release(TOTAL_PERMITS); } else { // Release the one we just acquired this.semaphore.release(); } } /** * Instantiate a SnapshotManager and ClusteredSessionValve and add the valve to our parent Context's pipeline. Add a * JvmRouteValve and BatchReplicationClusteredSessionValve if needed. */ public HttpHandler wrapHandlers(final HttpHandler handler, final Deployment deployment) { HttpHandler current = handler; current = new LockingHandler(this.valveLock, current); if (this.getUseJK()) { current = new JvmRouteHandler(this, current, deployment); } // Add clustered session valve current = new ClusteredSessionHandler(deployment, this, null, current); return current; } protected SnapshotManager createSnapshotManager() { String ctxPath = contextPath; switch (this.getSnapshotMode()) { case INTERVAL: { int interval = this.getSnapshotInterval(); if (interval > 0) { return new IntervalSnapshotManager(this, classLoader, ctxPath, interval); } UndertowLogger.WEB_SESSION_LOGGER.invalidSnapshotInterval(); } case INSTANT: { return new InstantSnapshotManager(this, ctxPath); } default: { throw MESSAGES.invalidSnapshotMode(); } } } /** * Gets the ids of all sessions in the distributed cache and adds them to the unloaded sessions map, along with their * lastAccessedTime and their maxInactiveInterval. Passivates overage or excess sessions. */ protected void initializeUnloadedSessions() { Map<String, String> sessions = this.distributedCacheManager.getSessionIds(); if (sessions != null) { boolean passivate = isPassivationEnabled(); long passivationMax = passivationMaxIdleTime * 1000L; long passivationMin = passivationMinIdleTime * 1000L; for (Entry<String, String> entry : sessions.entrySet()) { String realId = entry.getKey(); String owner = entry.getValue(); long ts = -1; DistributableSessionMetadata md = null; try { IncomingDistributableSessionData sessionData = this.distributedCacheManager.getSessionData(realId, owner, false); if (sessionData == null) { log.debugf("Metadata unavailable for unloaded session %s", realId); continue; } ts = sessionData.getTimestamp(); md = sessionData.getMetadata(); } catch (Exception e) { // most likely a lock conflict if the session is being updated remotely; // ignore it and use default values for timestamp and maxInactive log.debug("Problem reading metadata for session " + realId + " -- " + e.toString(), e); } long lastMod = ts == -1 ? System.currentTimeMillis() : ts; int maxLife = md == null ? maxInactiveInterval : md.getMaxInactiveInterval(); OwnedSessionUpdate osu = new OwnedSessionUpdate(owner, lastMod, maxLife, false); unloadedSessions.put(realId, osu); } if (passivate) { for (Entry<String, OwnedSessionUpdate> entry : unloadedSessions.entrySet()) { String realId = entry.getKey(); OwnedSessionUpdate osu = entry.getValue(); try { long elapsed = System.currentTimeMillis() - osu.getUpdateTime(); // if maxIdle time configured, means that we need to passivate sessions that have // exceeded the max allowed idle time if (passivationMax >= 0 && elapsed > passivationMax) { log.tracef("Elapsed time of %d for session %s exceeds max of %d; passivating", elapsed, realId, passivationMax); processUnloadedSessionPassivation(realId, osu); } // If the session didn't exceed the passivationMaxIdleTime_, see // if the number of sessions managed by this manager greater than the max allowed // active sessions, passivate the session if it exceed passivationMinIdleTime_ else if ((maxActiveAllowed > 0) && (passivationMin >= 0) && (calcActiveSessions() > maxActiveAllowed) && (elapsed >= passivationMin)) { log.tracef("Elapsed time of %d for session %s exceeds min of %d; passivating", elapsed, realId, passivationMin); processUnloadedSessionPassivation(realId, osu); } } catch (Exception e) { // most likely a lock conflict if the session is being updated remotely; ignore it log.debugf("Problem passivating session %s -- %s", realId, e); } } } } } /** * Session passivation logic for sessions only in the distributed store. * * @param realId the session id, minus any jvmRoute */ private void processUnloadedSessionPassivation(String realId, OwnedSessionUpdate osu) { log.tracef("Passivating session with id: %s", realId); this.distributedCacheManager.evictSession(realId, osu.getOwner()); osu.setPassivated(true); sessionPassivated(); } private void sessionPassivated() { int pc = passivatedCount.incrementAndGet(); int max = maxPassivatedCount.get(); while (pc > max) { if (!maxPassivatedCount.compareAndSet(max, pc)) { max = maxPassivatedCount.get(); } } } protected ClusteredSessionNotificationPolicy createClusteredSessionNotificationPolicy() { String policyClass = this.replicationConfig.getSessionNotificationPolicy(); if (policyClass == null || policyClass.isEmpty()) { policyClass = AccessController.doPrivileged(new ReadPropertyAction("jboss.web.clustered.session.notification.policy", IgnoreUndeployLegacyClusteredSessionNotificationPolicy.class.getName())); } try { ClusteredSessionNotificationPolicy policy = loadClass(policyClass, ClusteredSessionNotificationPolicy.class).newInstance(); policy.setClusteredSessionNotificationCapability(new ClusteredSessionNotificationCapability()); return policy; } catch (RuntimeException e) { throw e; } catch (Exception e) { throw MESSAGES.failToCreateSessionNotificationPolicy(ClusteredSessionNotificationPolicy.class.getName(), policyClass, e); } } private static <T> Class<? extends T> loadClass(String className, Class<T> targetClass) throws Exception { Exception lastException = new IllegalStateException(); for (ClassLoader loader : Arrays.asList(Thread.currentThread().getContextClassLoader(), DistributableSessionManager.class.getClassLoader())) { if (loader != null) { try { return loader.loadClass(className).asSubclass(targetClass); } catch (ClassNotFoundException e) { lastException = e; } } } throw lastException; } @Override public void stop() { log.debug("Stopping"); // Validate and update our current component state if (!this.started) return; this.started = false; synchronized (this) { log.trace("Waiting until backgroundProcess() short-circuits."); } // Handle re-entrance if (this.semaphore.tryAcquire()) { try { log.debug("Closing off LockingValve"); // Acquire all remaining permits, shutting off locking valve this.semaphore.acquire(TOTAL_PERMITS - 1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); this.semaphore.release(); throw new RuntimeException(e); } } resetStats(); // Notify our interested LifecycleListeners clearSessions(); this.distributedCacheManager.stop(); this.snapshotManager.stop(); this.snapshotManager = null; // Clean up maps this.sessions.clear(); this.unloadedSessions.clear(); this.passivatedCount.set(0); } /** * Clear the underlying cache store. */ private void clearSessions() { boolean passivation = isPassivationEnabled(); boolean persistence = isPersistenceEnabled(); // First, the sessions we have actively loaded for (Session session : this.sessions.values()) { ClusteredSession<O> ses = cast(session); log.tracef("clearSessions(): clear session by expiring or passivating: %s", ses); try { // if session passivation is enabled, passivate sessions instead of expiring them which means // they'll be available to the manager for activation after a restart. if (passivation && ses.isValid()) { processSessionPassivation(ses.getRealId()); } else if (!persistence) { boolean notify = true; boolean localCall = true; boolean localOnly = true; ses.expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.UNDEPLOY); } } catch (Throwable t) { UndertowLogger.WEB_SESSION_LOGGER.errorPassivatingSession(ses.getIdInternal(), t); } } Set<Entry<String, OwnedSessionUpdate>> unloaded = unloadedSessions.entrySet(); for (Iterator<Entry<String, OwnedSessionUpdate>> it = unloaded.iterator(); it.hasNext(); ) { Entry<String, OwnedSessionUpdate> entry = it.next(); String realId = entry.getKey(); try { if (passivation) { OwnedSessionUpdate osu = entry.getValue(); // Ignore the marker entries for our passivated sessions if (!osu.isPassivated()) { this.distributedCacheManager.evictSession(realId, osu.getOwner()); } } else { this.distributedCacheManager.removeSessionLocal(realId); } } catch (Exception e) { // Not as big a problem; we don't own the session log.debugf("Problem %s session %s -- %s", passivation ? "evicting" : "removing", realId, e); } it.remove(); } } /** * Session passivation logic for an actively managed session. * * @param realId the session id, minus any jvmRoute */ private void processSessionPassivation(String realId) { // get the session from the local map ClusteredSession<O> session = cast(this.sessions.get(realId)); // Remove actively managed session and add to the unloaded sessions // if it's already unloaded session (session == null) don't do anything, if (session != null) { synchronized (session) { log.tracef("Passivating session with id: %s", realId); session.notifyWillPassivate(ClusteredSessionNotificationCause.PASSIVATION); this.distributedCacheManager.evictSession(realId); sessionPassivated(); // Put the session in the unloadedSessions map. This will // expose the session to regular invalidation. Object obj = unloadedSessions.put(realId, new OwnedSessionUpdate(null, session.getLastAccessedTimeInternal(), session.getMaxInactiveInterval(), true)); if (obj == null) { log.tracef("New session %s added to unloaded session map", realId); } else { log.tracef("Updated timestamp for unloaded session %s", realId); } sessions.remove(realId); } } else { log.tracef("processSessionPassivation(): could not find session %s", realId); } } @Override public Session createSession(final HttpServerExchange serverExchange, final SessionConfig sessionCookieConfig) { Session session = null; try { // [JBAS-7123] Make sure we're either in the call stack where LockingValve has // a lock, or that we acquire one ourselves boolean inLockingValve = SessionReplicationContext.isLocallyActive(); if (inLockingValve || this.valveLock.tryLock(0, TimeUnit.SECONDS)) { try { session = createSessionInternal(serverExchange, sessionCookieConfig); } finally { if (!inLockingValve) { this.valveLock.unlock(); } } } else { log.trace("createEmptySession(): Manager is not handling requests; returning null"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return session; } private ClusteredSession<O> createSessionInternal(final HttpServerExchange serverExchange, final SessionConfig config) { // First check if we've reached the max allowed sessions, // then try to expire/passivate sessions to free memory // maxActiveAllowed -1 is unlimited // We check here for maxActive instead of in add(). add() gets called // when we load an already existing session from the distributed cache // (e.g. in a failover) and we don't want to fail in that situation. if (maxActiveAllowed != -1 && calcActiveSessions() >= maxActiveAllowed) { log.tracef("createSession(): active sessions = %d and max allowed sessions = %d", calcActiveSessions(), maxActiveAllowed); processExpires(); if (calcActiveSessions() >= maxActiveAllowed) { // Exceeds limit. We need to reject it. rejectedCounter.incrementAndGet(); // Catalina api does not specify what happens // but we will throw a runtime exception for now. throw MESSAGES.tooManyActiveSessions(maxActiveAllowed); } } ClusteredSession<O> session = createEmptyClusteredSession(); if (session != null) { session.setNew(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(this.maxInactiveInterval); session.setValid(true); String clearInvalidated = null; // see below String sessionId = config.findSessionId(serverExchange); if (sessionId != null) { clearInvalidated = sessionId; if (sessions.containsKey(sessionId)) { throw UndertowMessages.MESSAGES.sessionAlreadyExists(sessionId); } } else { sessionId = generateSessionId(); } config.setSessionId(serverExchange, sessionId); session.setId(sessionId); // Setting the id leads to a call to add() getDistributedCacheManager().sessionCreated(session.getRealId()); session.tellNew(ClusteredSessionNotificationCause.CREATE); log.tracef("Created a ClusteredSession with id: %s", sessionId); createdCounter.incrementAndGet(); // the call to add() handles the other counters // Add this session to the set of those potentially needing replication SessionReplicationContext.bindSession(session, snapshotManager); if (clearInvalidated != null) { // We no longer need to track any earlier session w/ same id // invalidated by this thread SessionInvalidationTracker.clearInvalidatedSession(this); } } return session; } protected String generateSessionId() { return this.createSessionId(this.distributedCacheManager.createSessionId(), this.getJvmRoute()); } @Override public Session getSession(final HttpServerExchange serverExchange, final SessionConfig sessionCookieConfig) { String id = sessionCookieConfig.findSessionId(serverExchange); if (id == null) { return null; } return findSession(id); } @Override public void registerSessionListener(final SessionListener listener) { } @Override public void removeSessionListener(final SessionListener listener) { } @Override public void setDefaultSessionTimeout(final int timeout) { } private ClusteredSession<O> findSession(String id) { String realId = this.parse(id).getKey(); // Find it from the local store first ClusteredSession<O> session = cast(this.sessions.get(realId)); // If we didn't find it locally, only check the distributed cache // if we haven't previously handled this session id on this request. // If we handled it previously but it's no longer local, that means // it's been invalidated. If we request an invalidated session from // the distributed cache, it will be missing from the local cache but // may still exist on other nodes (i.e. if the invalidation hasn't // replicated yet because we are running in a tx). With buddy replication, // asking the local cache for the session will cause the out-of-date // session from the other nodes to be gravitated, thus resuscitating // the session. if (session == null && !SessionInvalidationTracker.isSessionInvalidated(realId, this)) { log.tracef("Checking for session %s in the distributed cache", realId); session = loadSession(realId); // if (session != null) // { // add(session); // // We now notify, since we've added a policy to allow listeners // // to discriminate. But the default policy will not allow the // // notification to be emitted for FAILOVER, so the standard // // behavior is unchanged. // session.tellNew(ClusteredSessionNotificationCause.FAILOVER); // } } else if (session != null && this.outdatedSessionChecker.isSessionOutdated(session)) { log.tracef("Updating session %s from the distributed cache", realId); // Need to update it from the cache session = loadSession(realId); if (session == null) { // We have a session locally but it's no longer available // from the distributed store; i.e. it's been invalidated elsewhere // So we need to clean up // TODO what about notifications? this.sessions.remove(realId); } } if (session != null) { // Add this session to the set of those potentially needing replication SessionReplicationContext.bindSession(session, snapshotManager); // If we previously called passivate() on the session due to // replication, we need to make an offsetting activate() call if (session.getNeedsPostReplicateActivation()) { session.notifyDidActivate(ClusteredSessionNotificationCause.REPLICATION); } } return session; } public void remove(Session s) { ClusteredSession<O> session = cast(s); synchronized (session) { String realId = session.getRealId(); if (realId == null) return; log.tracef("Removing session from store with id: %s", realId); try { session.removeMyself(); } finally { // We don't want to replicate this session at the end // of the request; the removal process took care of that SessionReplicationContext.sessionExpired(session, realId, snapshotManager); // Track this session to prevent reincarnation by this request // from the distributed cache SessionInvalidationTracker.sessionInvalidated(realId, this); sessions.remove(realId); this.getReplicationStatistics().removeStats(realId); // Compute how long this session has been alive, and update // our statistics accordingly int timeAlive = (int) ((System.currentTimeMillis() - session.getCreationTimeInternal()) / 1000); sessionExpired(timeAlive); } } } private void handleForceSynchronousNotification(String type, String enableType, String disableType) { boolean enabled = type.equals(enableType); if (enabled || type.equals(disableType)) { if (this.distributedCacheManager != null) { this.distributedCacheManager.setForceSynchronous(enabled); } } } @Override public String getJvmRoute() { return null; } @Override public String locateJvmRoute(String sessionId) { return this.distributedCacheManager.locate(sessionId); } @Override public String createSessionId() { return sessionIdGenerator.createSessionId(); } @Override public void removeLocal(Session s) { ClusteredSession<O> session = cast(s); synchronized (session) { String realId = session.getRealId(); if (realId == null) return; log.tracef("Removing session from local store with id: %s", realId); try { session.removeMyselfLocal(); } finally { // We don't want to replicate this session at the end // of the request; the removal process took care of that SessionReplicationContext.sessionExpired(session, realId, snapshotManager); // Track this session to prevent reincarnation by this request // from the distributed cache SessionInvalidationTracker.sessionInvalidated(realId, this); sessions.remove(realId); this.getReplicationStatistics().removeStats(realId); // Compute how long this session has been alive, and update // our statistics accordingly int timeAlive = (int) ((System.currentTimeMillis() - session.getCreationTimeInternal()) / 1000); this.sessionExpired(timeAlive); } } } @Override public boolean storeSession(Session s) { boolean stored = false; if (s != null) { ClusteredSession<O> session = cast(s); synchronized (session) { log.tracef("check to see if needs to store and replicate session with id %s ", session.getIdInternal()); if (session.isValid() && (session.isSessionDirty() || session.getMustReplicateTimestamp())) { String realId = session.getRealId(); // Notify all session attributes that they get serialized (SRV 7.7.2) long begin = System.currentTimeMillis(); session.notifyWillPassivate(ClusteredSessionNotificationCause.REPLICATION); long elapsed = System.currentTimeMillis() - begin; ReplicationStatistics stats = this.getReplicationStatistics(); stats.updatePassivationStats(realId, elapsed); // Do the actual replication begin = System.currentTimeMillis(); processSessionRepl(session); elapsed = System.currentTimeMillis() - begin; stored = true; stats.updateReplicationStats(realId, elapsed); } else { log.tracef("Session %s did not require replication.", session.getIdInternal()); } } } return stored; } public void add(Session session) { if (session == null) return; try { // [JBAS-7123] Make sure we're either in the call stack where LockingValve has // a lock, or that we acquire one ourselves boolean inLockingValve = SessionReplicationContext.isLocallyActive(); if (inLockingValve || this.valveLock.tryLock(0, TimeUnit.SECONDS)) { try { add(this.cast(session), false); // wait to replicate until req end } finally { if (!inLockingValve) { this.valveLock.unlock(); } } } else { log.trace("add(): ignoring add -- Manager is not actively handling requests"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } /** * Adds the given session to the collection of those being managed by this Manager. * * @param session the session. Cannot be <code>null</code>. * @param replicate whether the session should be replicated * @throws NullPointerException if <code>session</code> is <code>null</code>. */ private void add(ClusteredSession<O> session, boolean replicate) { // TODO -- why are we doing this check? The request checks session // validity and will expire the session; this seems redundant if (!session.isValid()) { // Not an error; this can happen if a failover request pulls in an // outdated session from the distributed cache (see TODO above) log.debugf("Cannot add session with id=%s because it is invalid", session.getIdInternal()); return; } String realId = session.getRealId(); Object existing = sessions.put(realId, session); unloadedSessions.remove(realId); if (!session.equals(existing)) { if (replicate) { storeSession(session); } // Update counters calcActiveSessions(); log.tracef("Session with id=%s added. Current active sessions %d", session.getIdInternal(), localActiveCounter.get()); } } public String getCacheConfigName() { return this.replicationConfig.getCacheName(); } public ReplicationGranularity getReplicationGranularity() { ReplicationGranularity granularity = this.replicationConfig.getReplicationGranularity(); return (granularity != null) ? granularity : ReplicationGranularity.SESSION; } public SnapshotMode getSnapshotMode() { SnapshotMode mode = this.replicationConfig.getSnapshotMode(); return (mode != null) ? mode : SnapshotMode.INSTANT; } public int getSnapshotInterval() { Integer interval = this.replicationConfig.getSnapshotInterval(); return (interval != null) ? interval.intValue() : -1; } public void setMaxUnreplicatedInterval(int maxUnreplicatedInterval) { this.maxUnreplicatedInterval = maxUnreplicatedInterval; } public String listLocalSessionIds() { List<String> ids = new ArrayList<String>(sessions.size()); this.addLocal(ids, sessions.keySet()); return reportSessionIds(ids); } private void addLocal(Collection<String> localIds, Collection<String> ids) { for (String id : ids) { if (this.distributedCacheManager.isLocal(id)) { localIds.add(id); } } } private String reportSessionIds(Collection<String> sessions) { StringBuilder builder = new StringBuilder(); Iterator<String> ids = sessions.iterator(); while (ids.hasNext()) { builder.append(ids.next()); if (ids.hasNext()) { builder.append(','); } } return builder.toString(); } public long getPassivatedSessionCount() { return this.passivatedCount.get(); } public long getMaxPassivatedSessionCount() { return this.maxPassivatedCount.get(); } public long getPassivationMaxIdleTime() { return this.passivationMaxIdleTime; } public long getPassivationMinIdleTime() { return this.passivationMinIdleTime; } @Override public int getMaxUnreplicatedInterval() { return this.maxUnreplicatedInterval; } @Override public ClusteredSessionNotificationPolicy getNotificationPolicy() { return this.notificationPolicy; } @Override public ReplicationTrigger getReplicationTrigger() { ReplicationTrigger trigger = this.replicationConfig.getReplicationTrigger(); return (trigger != null) ? trigger : ReplicationTrigger.SET_AND_NON_PRIMITIVE_GET; } public boolean getUseJK() { Boolean useJK = this.replicationConfig.getUseJK(); return (useJK != null) ? useJK : true; } @Override public DistributedCacheManager<O> getDistributedCacheManager() { return this.distributedCacheManager; } @Override public boolean isPassivationEnabled() { return this.passivate; } public boolean isPersistenceEnabled() { return this.persistence; } @Override public ClassResolver getApplicationClassResolver() { return this.resolver; } @Override public ReplicationConfig getReplicationConfig() { return this.replicationConfig; } @Override public void notifyRemoteInvalidation(String realId) { // Remove the session from our local map ClusteredSession<O> session = cast(this.sessions.remove(realId)); if (session == null) { // We weren't managing the session anyway. But remove it // from the list of cached sessions we haven't loaded if (unloadedSessions.remove(realId) != null) { log.tracef("Removed entry for session %s from unloaded session map", realId); } // If session has failed over and has been passivated here, // session will be null, but we'll have a TimeStatistic to clean up this.getReplicationStatistics().removeStats(realId); } else { // Expire the session // DON'T SYNCHRONIZE ON SESSION HERE -- isValid() and // expire() are meant to be multi-threaded and synchronize // properly internally; synchronizing externally can lead // to deadlocks!! boolean notify = false; // Don't notify listeners. SRV.10.7 // allows this, and sending notifications // leads to all sorts of issues; e.g. // circular calls with ClusteredSSO and // notifying when all that's happening is // data gravitation due to random failover boolean localCall = false; // this call originated from the cache; // we have already removed session boolean localOnly = true; // Don't pass attr removals to cache try { // Don't track this invalidation is if it were from a request SessionInvalidationTracker.suspend(); session.expire(notify, localCall, localOnly, ClusteredSessionNotificationCause.INVALIDATE); } finally { SessionInvalidationTracker.resume(); // Remove any stats for this session this.getReplicationStatistics().removeStats(realId); } } } @Override public void notifyLocalAttributeModification(String realId) { ClusteredSession<O> session = cast(this.sessions.get(realId)); if (session != null) { session.sessionAttributesDirty(); } else { UndertowLogger.WEB_SESSION_LOGGER.notificationForInactiveSession(realId); } } @Override public void sessionActivated() { int pc = passivatedCount.decrementAndGet(); // Correct for drift since we don't know the true passivation // count when we started. We can get activations of sessions // we didn't know were passivated. // FIXME -- is the above statement still correct? Is this needed? if (pc < 0) { // Just reverse our decrement. passivatedCount.incrementAndGet(); } } @Override public boolean sessionChangedInDistributedCache(String realId, String dataOwner, int distributedVersion, long timestamp, DistributableSessionMetadata metadata) { boolean updated = true; ClusteredSession<O> session = cast(this.sessions.get(realId)); if (session != null) { // Need to invalidate the loaded session. We get back whether // this an actual version increment updated = session.setVersionFromDistributedCache(distributedVersion); if (updated) { log.tracef("session in-memory data is invalidated for id: %s new version: %d", realId, distributedVersion); } } else { int maxLife = metadata == null ? maxInactiveInterval : metadata.getMaxInactiveInterval(); Object existing = unloadedSessions.put(realId, new OwnedSessionUpdate(dataOwner, timestamp, maxLife, false)); if (existing == null) { calcActiveSessions(); log.tracef("New session %s added to unloaded session map", realId); } else { log.tracef("Updated timestamp for unloaded session %s", realId); } } return updated; } @Override protected void processExpirationPassivation() { boolean expire = maxInactiveInterval >= 0; boolean passivate = isPassivationEnabled(); long passivationMax = passivationMaxIdleTime * 1000L; long passivationMin = passivationMinIdleTime * 1000L; log.trace("processExpirationPassivation(): Looking for sessions that have expired ..."); log.tracef("processExpirationPassivation(): active sessions = %d", calcActiveSessions()); log.tracef("processExpirationPassivation(): expired sessions = %d", expiredCounter.get()); if (passivate) { log.tracef("processExpirationPassivation(): passivated count = %d", getPassivatedSessionCount()); } // Holder for sessions or OwnedSessionUpdates that survive expiration, // sorted by last accessed time TreeSet<PassivationCheck> passivationChecks = new TreeSet<PassivationCheck>(); try { // Don't track sessions invalidated via this method as if they // were going to be re-requested by the thread SessionInvalidationTracker.suspend(); // First, handle the sessions we are actively managing for (Session s : this.sessions.values()) { if (!this.started) return; boolean likelyExpired = false; String realId = null; try { ClusteredSession<O> session = cast(s); realId = session.getRealId(); likelyExpired = expire; if (expire) { // JBAS-2403. Check for outdated sessions where we think // the local copy has timed out. If found, refresh the // session from the cache in case that might change the timeout likelyExpired = (session.isValid(false) == false); if (likelyExpired && this.outdatedSessionChecker.isSessionOutdated(session)) { // With JBC, every time we get a notification from the distributed // cache of an update, we get the latest timestamp. So // we shouldn't need to do a full session load here. A load // adds a risk of an unintended data gravitation. However, // with a database instead of JBC we don't get notifications // JBAS-2792 don't assign the result of loadSession to session // just update the object from the cache or fall through if // the session has been removed from the cache loadSession(session.getRealId()); } // Do a normal invalidation check that will expire the // session if it has timed out // DON'T SYNCHRONIZE on session here -- isValid() and // expire() are meant to be multi-threaded and synchronize // properly internally; synchronizing externally can lead // to deadlocks!! if (!session.isValid()) continue; likelyExpired = false; } // we now have a valid session; store it so we can check later // if we need to passivate it if (passivate) { passivationChecks.add(new PassivationCheck(session)); } } catch (Exception e) { if (likelyExpired) { // JBAS-7397 clean up bruteForceCleanup(realId, e); } else { UndertowLogger.WEB_SESSION_LOGGER.failToPassivateLoad(realId, e); } } } // Next, handle any unloaded sessions // We may have not gotten replication of a timestamp for requests // that occurred w/in maxUnreplicatedInterval of the previous // request. So we add a grace period to avoid flushing a session early // and permanently losing part of its node structure in JBoss Cache. long maxUnrep = maxUnreplicatedInterval < 0 ? 60 : maxUnreplicatedInterval; for (Entry<String, OwnedSessionUpdate> entry : this.unloadedSessions.entrySet()) { if (!this.started) return; String realId = entry.getKey(); OwnedSessionUpdate osu = entry.getValue(); boolean likelyExpired = false; long now = System.currentTimeMillis(); long elapsed = (now - osu.getUpdateTime()); try { likelyExpired = expire && osu.getMaxInactive() >= 1 && elapsed >= (osu.getMaxInactive() + maxUnrep) * 1000L; if (likelyExpired) { // if (osu.passivated && osu.owner == null) if (osu.isPassivated()) { // Passivated session needs to be expired. A call to // findSession will bring it out of passivation ClusteredSession session = findSession(realId); if (session != null) { session.isValid(); // will expire continue; } } // If we get here either !osu.passivated, or we don't own // the session or the session couldn't be reactivated (invalidated by user). // Either way, do a cleanup this.distributedCacheManager.removeSessionLocal(realId, osu.getOwner()); unloadedSessions.remove(realId); this.getReplicationStatistics().removeStats(realId); } else if (passivate && !osu.isPassivated()) { // we now have a valid session; store it so we can check later // if we need to passivate it passivationChecks.add(new PassivationCheck(realId, osu)); } } catch (Exception e) { // JBAS-7397 Don't try forever if (likelyExpired) { // JBAS-7397 bruteForceCleanup(realId, e); } else { UndertowLogger.WEB_SESSION_LOGGER.failToPassivate("unloaded", realId, e); } } } if (!this.started) return; // Now, passivations if (passivate) { // Iterate through sessions, earliest lastAccessedTime to latest for (PassivationCheck passivationCheck : passivationChecks) { try { long timeNow = System.currentTimeMillis(); long timeIdle = timeNow - passivationCheck.getLastUpdate(); // if maxIdle time configured, means that we need to passivate sessions that have // exceeded the max allowed idle time if (passivationMax >= 0 && timeIdle > passivationMax) { passivationCheck.passivate(); } // If the session didn't exceed the passivationMaxIdleTime_, see // if the number of sessions managed by this manager greater than the max allowed // active sessions, passivate the session if it exceed passivationMinIdleTime_ else if ((maxActiveAllowed > 0) && (passivationMin > 0) && (calcActiveSessions() >= maxActiveAllowed) && (timeIdle > passivationMin)) { passivationCheck.passivate(); } else { // the entries are ordered by lastAccessed, so once // we don't passivate one, we won't passivate any break; } } catch (Exception e) { UndertowLogger.WEB_SESSION_LOGGER.failToPassivate(passivationCheck.isUnloaded() ? "unloaded " : "", passivationCheck.getRealId(), e); } } } } catch (Exception ex) { UndertowLogger.WEB_SESSION_LOGGER.processExpirationPassivationException(ex); } finally { SessionInvalidationTracker.resume(); } log.trace("processExpirationPassivation(): Completed ..."); log.tracef("processExpirationPassivation(): active sessions = %d", calcActiveSessions()); log.tracef("processExpirationPassivation(): expired sessions = %d", expiredCounter.get()); if (passivate) { log.tracef("processExpirationPassivation(): passivated count = %d", getPassivatedSessionCount()); } } /** * Loads a session from the distributed store. If an existing session with the id is already under local management, that * session's internal state will be updated from the distributed store. Otherwise a new session will be created and added to * the collection of those sessions under local management. * * @param realId id of the session-id with any jvmRoute removed * @return the session or <code>null</code> if the session cannot be found in the distributed store */ private ClusteredSession<O> loadSession(String realId) { if (realId == null) return null; try { // [JBAS-7123] Make sure we're either in the call stack where LockingValve has // a lock, or that we acquire one ourselves boolean inLockingValve = SessionReplicationContext.isLocallyActive(); if (inLockingValve || this.valveLock.tryLock(0, TimeUnit.SECONDS)) { try { long begin = System.currentTimeMillis(); boolean mustAdd = false; boolean passivated = false; ClusteredSession<O> session = cast(this.sessions.get(realId)); boolean initialLoad = false; if (session == null) { initialLoad = true; // This is either the first time we've seen this session on this // server, or we previously expired it and have since gotten // a replication message from another server mustAdd = true; session = createEmptyClusteredSession(); // JBAS-7379 Ensure concurrent threads trying to load same session id // use the same session ClusteredSession<O> embryo = this.embryonicSessions.putIfAbsent(realId, session); if (embryo != null) { session = embryo; } OwnedSessionUpdate osu = unloadedSessions.get(realId); passivated = (osu != null && osu.isPassivated()); } synchronized (session) { // JBAS-7379 check if we lost the race to the sync block // and another thread has already loaded this session if (initialLoad && session.isOutdated() == false) { // some one else loaded this return session; } IncomingDistributableSessionData data = this.distributedCacheManager.getSessionData(realId, initialLoad); if (data != null) { session.update(data); } else { // Clunky; we set the session variable to null to indicate // no data so move on session = null; } if (session != null) { ClusteredSessionNotificationCause cause = passivated ? ClusteredSessionNotificationCause.ACTIVATION : ClusteredSessionNotificationCause.FAILOVER; session.notifyDidActivate(cause); } if (session != null) { if (mustAdd) { add(session, false); // don't replicate if (!passivated) { session.tellNew(ClusteredSessionNotificationCause.FAILOVER); } } long elapsed = System.currentTimeMillis() - begin; this.getReplicationStatistics().updateLoadStats(realId, elapsed); log.tracef("loadSession(): id=%s, session=%s", realId, session); } else { log.tracef("loadSession(): session %s not found in distributed cache", realId); } if (initialLoad) { // The session is now in the regular map, or the session // doesn't exist in the distributed cache. either way // it's now safe to stop tracking this embryonic session embryonicSessions.remove(realId); } } return session; } finally { if (!inLockingValve) { this.valveLock.unlock(); } } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; } @SuppressWarnings("unchecked") private ClusteredSession<O> createEmptyClusteredSession() { try { // [JBAS-7123] Make sure we're either in the call stack where LockingValve has // a lock, or that we acquire one ourselves boolean inLockingValve = SessionReplicationContext.isLocallyActive(); if (inLockingValve || this.valveLock.tryLock(0, TimeUnit.SECONDS)) { try { switch (this.getReplicationGranularity()) { case ATTRIBUTE: return (ClusteredSession<O>) new AttributeBasedClusteredSession((DistributableSessionManager<OutgoingAttributeGranularitySessionData>) this); default: return (ClusteredSession<O>) new SessionBasedClusteredSession((DistributableSessionManager<OutgoingSessionGranularitySessionData>) this); } } finally { if (!inLockingValve) { this.valveLock.unlock(); } } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; } private void bruteForceCleanup(String realId, Exception ex) { UndertowLogger.WEB_SESSION_LOGGER.bruteForceCleanup(realId, ex.getLocalizedMessage()); try { this.distributedCacheManager.removeSessionLocal(realId, null); } catch (Exception e) { UndertowLogger.WEB_SESSION_LOGGER.failToBruteForceCleanup(realId, e); } finally { // Get rid of our refs even if distributed store fails unloadedSessions.remove(realId); this.getReplicationStatistics().removeStats(realId); } } @Override public Entry<String, String> parse(String sessionId) { return this.getUseJK() ? super.parse(sessionId) : new AbstractMap.SimpleImmutableEntry<String, String>(sessionId, null); } @Override public String createSessionId(String realId, String jvmRoute) { return this.getUseJK() ? super.createSessionId(realId, jvmRoute) : realId; } @Override protected int getTotalActiveSessions() { return localActiveCounter.get() + unloadedSessions.size() - passivatedCount.get(); } /** * Places the current session contents in the distributed cache and replicates them to the cluster * * @param session the session. Cannot be <code>null</code>. */ private void processSessionRepl(ClusteredSession<O> session) { boolean endBatch = false; BatchingManager batchingManager = this.distributedCacheManager.getBatchingManager(); try { // We need transaction so all the replication are sent in batch. // Don't do anything if there is already transaction context // associated with this thread. if (!batchingManager.isBatchInProgress()) { batchingManager.startBatch(); endBatch = true; } session.processSessionReplication(); } catch (Exception ex) { log.debug("processSessionRepl(): failed with exception", ex); RuntimeException exception = null; try { batchingManager.setBatchRollbackOnly(); } catch (RuntimeException e) { exception = e; } catch (Exception e) { exception = MESSAGES.failedSessionReplication(e); } if (exception != null) { UndertowLogger.WEB_SESSION_LOGGER.exceptionRollingBackTransaction(exception); throw exception; } } finally { if (endBatch) { batchingManager.endBatch(); } } } @SuppressWarnings("unchecked") private ClusteredSession<O> cast(Session session) { if (session == null) return null; if (!(session instanceof ClusteredSession)) { throw MESSAGES.invalidSession(getClass().getName()); } return (ClusteredSession<O>) session; } private class PassivationCheck implements Comparable<PassivationCheck> { private final String realId; private final OwnedSessionUpdate osu; private final ClusteredSession<O> session; private PassivationCheck(String realId, OwnedSessionUpdate osu) { assert osu != null : MESSAGES.nullParamter("osu"); assert realId != null : MESSAGES.nullParamter("realId"); this.realId = realId; this.osu = osu; this.session = null; } private PassivationCheck(ClusteredSession<O> session) { assert session != null : MESSAGES.nullParamter("session"); this.realId = session.getRealId(); this.session = session; this.osu = null; } private long getLastUpdate() { return osu == null ? session.getLastAccessedTimeInternal() : osu.getUpdateTime(); } private void passivate() { if (osu == null) { DistributableSessionManager.this.processSessionPassivation(realId); } else { DistributableSessionManager.this.processUnloadedSessionPassivation(realId, osu); } } private String getRealId() { return realId; } private boolean isUnloaded() { return osu != null; } // This is what causes sorting based on lastAccessed @Override public int compareTo(PassivationCheck o) { long thisVal = getLastUpdate(); long anotherVal = o.getLastUpdate(); return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1)); } } private static class SemaphoreLock implements Lock { private final Semaphore semaphore; SemaphoreLock(Semaphore semaphore) { this.semaphore = semaphore; } /** * @see java.util.concurrent.locks.Lock#lock() */ @Override public void lock() { this.semaphore.acquireUninterruptibly(); } /** * @see java.util.concurrent.locks.Lock#lockInterruptibly() */ @Override public void lockInterruptibly() throws InterruptedException { this.semaphore.acquire(); } /** * @see java.util.concurrent.locks.Lock#newCondition() */ @Override public Condition newCondition() { throw new UnsupportedOperationException(); } /** * @see java.util.concurrent.locks.Lock#tryLock() */ @Override public boolean tryLock() { return this.semaphore.tryAcquire(); } /** * @see java.util.concurrent.locks.Lock#tryLock(long, java.util.concurrent.TimeUnit) */ @Override public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return this.semaphore.tryAcquire(timeout, unit); } /** * @see java.util.concurrent.locks.Lock#unlock() */ @Override public void unlock() { this.semaphore.release(); } } }