All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.crimsonhexagon.rsm.RedisSessionManager Maven / Gradle / Ivy

There is a newer version: 2.2.1
Show newest version
/*-
 *  Copyright 2015 Crimson Hexagon
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.crimsonhexagon.rsm;

import java.io.IOException;
import java.util.Enumeration;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.apache.catalina.session.ManagerBase;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * Manages {@link RedisSession}s
 * 
 * @author Steve Ungerer
 */
public abstract class RedisSessionManager extends ManagerBase {
	protected final Log log = LogFactory.getLog(this.getClass());

	/**
	 * Default value to prefix redis keys with.
	 */
	public static final String DEFAULT_SESSION_KEY_PREFIX="_rsm_";
	
	static final int DO_NOT_CHECK = -1;
	
	private int sessionExpirationTime; // in minutes
	private RedisSessionClient _client; // access should be done via #getClient()
	private String sessionKeyPrefix = DEFAULT_SESSION_KEY_PREFIX;
	private String ignorePattern = RedisSessionRequestValve.DEFAULT_IGNORE_PATTERN;
	private boolean saveOnChange;
	private boolean forceSaveAfterRequest;
	private boolean dirtyOnMutation;
	private int maxSessionAttributeSize = DO_NOT_CHECK;
	private int maxSessionSize = DO_NOT_CHECK;
	private boolean allowOversizedSessions;
	
	private ThreadLocal currentSessionState = InheritableThreadLocal.withInitial(RedisSessionState::new);
	
	private RedisSessionRequestValve requestValve;

	protected RedisSessionClient getClient() {
	    return _client;
	}
	
	/**
	 * Construct the {@link RedisSessionClient}
	 * @return
	 * @throws ClassNotFoundException
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 */
	protected abstract RedisSessionClient buildClient() throws ClassNotFoundException, InstantiationException, IllegalAccessException;

	/**
	 * Get the encoded size of the object
	 * @param obj
	 * @return
	 */
	int getEncodedSize(Object obj) {
	    return getClient().getEncodedSize(obj);
	}
	
	/**
	 * Should the {@link RedisSession} be saved immediately when an attribute changes
	 * @return
	 */
	public boolean isSaveOnChange() {
		return saveOnChange;
	}

	/**
	 * Should the {@link RedisSession} be saved on completion of every request regardless of {@link RedisSession#isDirty()}
	 * @return
	 */
	public boolean isForceSaveAfterRequest() {
		return forceSaveAfterRequest;
	}
	
	/**
	 * Should any attribute mutation result in marking the session as dirty
	 * @return
	 */
	public boolean isDirtyOnMutation() {
		return dirtyOnMutation;
	}

	/**
	 * Obtain the {@link ClassLoader} for this context. Necessary for deserialization of {@link RedisSession}s
	 * @return
	 */
	public ClassLoader getContainerClassLoader() {
		return getContext().getLoader().getClassLoader();
	}
	
	@Override
	public void load() throws ClassNotFoundException, IOException {
		// noop
	}

	@Override
	public void unload() throws IOException {
		// noop
	}

	@Override
	protected synchronized void startInternal() throws LifecycleException {
		super.startInternal();

		try {
			this._client = buildClient();
		} catch (Throwable t) {
			log.fatal("Unable to load serializer", t);
			throw new LifecycleException(t);
		}

		this.requestValve = new RedisSessionRequestValve(this, ignorePattern);
		getContext().getParent().getPipeline().addValve(requestValve);
        this.sessionExpirationTime = getContext().getSessionTimeout();
        if (this.sessionExpirationTime < 0) {
        	log.warn("Ignoring negative session expiration time");
        	this.sessionExpirationTime = 0;
        }
		log.info("Will expire sessions after " + sessionExpirationTime + " minutes");
		setState(LifecycleState.STARTING);
	}

	@Override
	protected synchronized void stopInternal() throws LifecycleException {
		setState(LifecycleState.STOPPING);
		log.info("Stopping");
		getContext().getParent().getPipeline().removeValve(requestValve);
		getClient().shutdown();
		super.stopInternal();
	}

	@Override
	public Session createSession(String requestedSessionId) {
		RedisSession session = createEmptySession();
		session.setNew(true);
		session.setValid(true);
		session.setCreationTime(System.currentTimeMillis());
		session.setMaxInactiveInterval(sessionExpirationTime * 60);
		session.setId(requestedSessionId == null ? generateSessionId() : requestedSessionId);
		session.tellNew();
		currentSessionState.set(new RedisSessionState(session, false)); // persisted will be set to true in save()
		save(session, true);
		return session;
	}

	@Override
	protected void changeSessionId(Session session, String newId, boolean notifySessionListeners, boolean notifyContainerListeners) {
		final String oldId = session.getId();
		super.changeSessionId(session, newId, notifySessionListeners, notifyContainerListeners);
		if (RedisSession.class.isAssignableFrom(session.getClass())) {
			final RedisSession rSession = RedisSession.class.cast(session);
			currentSessionState.set(new RedisSessionState(rSession, false));
			getClient().delete(generateRedisSessionKey(oldId));
			save(rSession, true);
		} else {
			throw new UnsupportedOperationException("Could not change a session ID with class " + session.getClass());
		}
	}

	@Override
	protected String generateSessionId() {
		String sessionId = null;
		while (sessionId == null) {
			sessionId = prefixJvmRoute(super.generateSessionId());
			if (getClient().exists(generateRedisSessionKey(sessionId))) {
				log.debug("Rejecting duplicate sessionId: " + sessionId);
				sessionId = null;
			} else {
				log.debug("Generated new sessionId: " + sessionId);
			}
		}
		return sessionId;
	}

	/**
	 * Generate the storage key for the given sessionId
	 * @param sessionId
	 * @return
	 */
	private String generateRedisSessionKey(final String sessionId) {
		if (sessionId == null) {
			throw new IllegalArgumentException("sessionId must not be null");
		}
		String sessionKey = sessionId;
		if (!sessionKey.startsWith(sessionKeyPrefix)) {
			sessionKey = sessionKeyPrefix + sessionKey;
		}
		return sessionKey;
	}

	/**
	 * Prefix the given sessionId with the JVM Route
	 * @param sessionId
	 * @return
	 */
	private String prefixJvmRoute(String sessionId) {
		String jvmRoute = getJvmRoute();
		if (jvmRoute != null) {
			String jvmRoutePrefix = '.' + jvmRoute;
			return sessionId.endsWith(jvmRoutePrefix) ? sessionId : sessionId + jvmRoutePrefix;
		} else {
			return sessionId;
		}
	}
	
	@Override
	public RedisSession createEmptySession() {
		return new RedisSession(this);
	}

	@Override
	public void add(Session session) {
		if (RedisSession.class.isAssignableFrom(session.getClass())) {
			save(RedisSession.class.cast(session), false);
		} else {
			throw new UnsupportedOperationException("Could not add a session with class " + session.getClass());
		}
	}

	@Override
	public Session findSession(String id) throws IOException {
		RedisSession session = null;
		log.debug("Finding session " + id);
		if (id == null) {
			currentSessionState.remove();
		} else if (id.equals(currentSessionState.get().sessionId)) {
			log.debug("CurrentSession found for " + id);
			session = currentSessionState.get().session;
		} else {
			log.debug("Loading from redis");
			try {
				session = getClient().load(generateRedisSessionKey(id));
			} catch (Throwable t) {
				log.error("Failed to load session [" + id + "] from redis", t);
			}
			if (session != null) {
				log.debug("Found session " + id + " in redis");
				session.postDeserialization(this);
				session.setNew(false); // Fix issue #12
				currentSessionState.set(new RedisSessionState(session, true));
			} else {
				log.debug("Session " + id + " not found in redis");
				currentSessionState.remove();
			}
		}

		return session;
	}

	public void save(RedisSession redisSession, boolean forceSave) {
		log.debug("Checking if session " + redisSession.getId() + " needs to be saved in redis");

		if (log.isTraceEnabled()) {
			log.trace("Session Contents [" + redisSession.getId() + "]:");
			Enumeration en = redisSession.getAttributeNames();
			while (en.hasMoreElements()) {
				String e = en.nextElement();
				log.trace("  " + e + ": " + String.valueOf(redisSession.getAttribute(e)));
			}
		}
		
		if (currentSessionState.get().session == null) {
			currentSessionState.set(new RedisSessionState(redisSession, false));
		}

		final boolean currentSessionPersisted = currentSessionState.get().persisted;
		final String sessionKey = generateRedisSessionKey(redisSession.getId());
		if (forceSave
			|| redisSession.isDirty()
			|| !currentSessionPersisted
		) {
		    if (getMaxSessionSize() != DO_NOT_CHECK) {
		        final int size = getEncodedSize(redisSession);
		        if (size > getMaxSessionSize()) {
		            if (!isAllowOversizedSessions()) {
		                log.error("Not saving [" + redisSession.getId() + "] to redis. Size of [" + size + "] exceeds max of [" + getMaxSessionSize() +"]");
		                return;
		            } else {
		                log.error("Session [" + redisSession.getId() + "] size of [" + size + "] exceeds max of [" + getMaxSessionSize() +"]; still saving");
		            }
		        }
		    }
			log.debug("Saving " + redisSession.getId() + " to redis");
			try {
			    getClient().save(sessionKey, redisSession);
			} catch (Throwable t) {
				log.error("Failed to save session [" + redisSession.getId() + "]", t);
			}
			redisSession.clearDirty();
			currentSessionState.get().markPersisted();
		} else {
			log.debug("Not saving " + redisSession.getId() + " to redis");
		}

		log.trace("Setting expire on " + redisSession.getId() + " to " + sessionExpirationTime);
		getClient().expire(sessionKey, sessionExpirationTime, TimeUnit.MINUTES);
	}

	@Override
	public void remove(Session session, boolean update) {
		log.debug("Removing session ID : " + session.getId());
		try {
		    getClient().delete(generateRedisSessionKey(session.getId()));
		} catch (Throwable t) {
			log.error("Failed to remove session [" + session.getId() + "]", t);
		}
		currentSessionState.remove();
	}

	/**
	 * Handle post-request actions. Invoked from {@link RedisSessionRequestValve}
	 */
	public void afterRequest() {
		try {
			RedisSession session = currentSessionState.get().session;
			if (session != null) {
				if (session.isValid()) {
					log.trace("Request with session completed, saving session " + session.getId());
					save(session, isForceSaveAfterRequest());
				} else {
					log.debug("HTTP Session has been invalidated, removing :" + session.getId());
					remove(session);
				}
			}
		} catch (Exception e) {
			log.error("Error storing/removing session", e);
		} finally {
			currentSessionState.remove();
		}
	}

	@Override
	public void processExpires() {
		// Redis will handle expiration
	}

	/**
	 * Define the prefix for all redis keys.
* Defaults to {@value #DEFAULT_SESSION_KEY_PREFIX} * @param sessionKeyPrefix */ public void setSessionKeyPrefix(String sessionKeyPrefix) { this.sessionKeyPrefix = sessionKeyPrefix; } /** * If true the session will be persisted to redis immediately when any attribute is modified.
* Default is false which persists a modified session when the request is complete. * @param saveOnChange */ public void setSaveOnChange(boolean saveOnChange) { this.saveOnChange = saveOnChange; } /** * If true the session will always be persisted to redis after a request completes regardless of {@link RedisSession#isDirty()}.
* Default is false which persists a session after a request only if {@link RedisSession#isDirty()} is true * @param forceSaveAfterRequest */ public void setForceSaveAfterRequest(boolean forceSaveAfterRequest) { this.forceSaveAfterRequest = forceSaveAfterRequest; } /** * If true the session will be marked as dirty on any mutation. When false the attribute is * checked for equality against the old value and the session is marked as dirty only if they differ.
* When false the following code would not mark the session as dirty: *
*
     *  List stringList = (List)session.getAttribute("myList");
     *  stringList.add("another value");
     *  session.setAttribute("myList", stringList);
     * 
*
* Because the list is mutated, the 'old' value is identical to the 'new' value when equality is checked. It is a better * design to ensure session objects are immutable, but setting dirtyOnMutation to true will * workaround this with minimal overhead. * * @param dirtyOnMutation */ public void setDirtyOnMutation(boolean dirtyOnMutation) { this.dirtyOnMutation = dirtyOnMutation; } /** * Set a pattern (must adhere to {@link Pattern} specs) for requests to ignore. * This pattern is matched case-insensitive against {@link Request#getRequestURI()}.
* Defaults to {@value #DEFAULT_IGNORE_PATTERN} * @param ignorePattern */ public void setIgnorePattern(String ignorePattern) { this.ignorePattern = ignorePattern; } /** * Set a maximum size, in bytes, of each attribute within a session. If an attribute exceeds this size * it will not be stored in the session.
* Performance note: values will be encoded twice, once for size checking and once for actual storage. * @param maxSessionAttributeSize */ public void setMaxSessionAttributeSize(int maxSessionAttributeSize) { this.maxSessionAttributeSize = maxSessionAttributeSize; } int getMaxSessionAttributeSize() { return maxSessionAttributeSize; } /** * Set a maximum size, in bytes, of the entire serialized session in redis. If the session exceeds this size * it will not be saved to redis.
* Performance note: sessions will be encoded twice, once for size checking and once for actual storage. * @param maxSessionSize */ public void setMaxSessionSize(int maxSessionSize) { this.maxSessionSize = maxSessionSize; } int getMaxSessionSize() { return maxSessionSize; } /** * When {@link #setMaxSessionAttributeSize(int)} or {@link #setMaxSessionSize(int)} is used, the default * behavior is to prevent sessions with attributes or total size exceeding the configured value. * If {@link #allowOversizedSessions} is true then these exceptional attrs/sessions will * only be logged and will still be saved to redis. * @param allowAndLogSessionSizeErrors */ public void setAllowOversizedSessions(boolean allowAndLogSessionSizeErrors) { this.allowOversizedSessions = allowAndLogSessionSizeErrors; } boolean isAllowOversizedSessions() { return allowOversizedSessions; } /** * Get the current {@link RedisSessionState} * @return */ RedisSessionState getCurrentState() { return currentSessionState.get(); } /** * Set the current {@link RedisSessionState}; intended for testing * @param state */ void setCurrentState(RedisSessionState state) { currentSessionState.set(state); } /** * Encapsulates metadata about a {@link RedisSession} */ class RedisSessionState { final String sessionId; final RedisSession session; boolean persisted; RedisSessionState() { this.sessionId = null; this.session = null; this.persisted = false; } RedisSessionState(RedisSession session, boolean persisted) { this.sessionId = session.getId(); this.session = session; this.persisted = persisted; } void markPersisted() { // can't mark state as persisted if no session is set if (this.session == null) { throw new IllegalStateException("Can't mark a null session as persisted"); } this.persisted = true; } @Override public String toString() { return "sessionId: [" + sessionId + "]; persisted = [" + persisted +"]"; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy