diff --git a/.gitignore b/.gitignore index 982a41f6..577ea1cb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ example-app/.gradle/* .rspec *.iml .idea/* +/bin/ +/.gradle/ diff --git a/README.markdown b/README.markdown index d479d579..a689f7ec 100644 --- a/README.markdown +++ b/README.markdown @@ -55,6 +55,19 @@ Architecture Note: this architecture differs from the Apache PersistentManager implementation which implements persistent sticky sessions. Because that implementation expects all requests from a specific session to be routed to the same server, the timing persistence of sessions is non-deterministic since it is primarily for failover capabilities. + +Support Reids Cluster & JDK 8 +------------ + + + + maxInactiveInterval="60" + sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.." + clusters="host-1:port,host-2:port,.." /> + + + Usage ----- @@ -69,14 +82,15 @@ Add the following into your Tomcat context.xml (or the context block of the serv sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.." sentinelMaster="SentinelMasterName" sentinels="sentinel-host-1:port,sentinel-host-2:port,.." /> + clusters="host-1:port,host-2:port,.." /> The Valve must be declared before the Manager. Copy the following files into the `TOMCAT_BASE/lib` directory: * tomcat-redis-session-manager-VERSION.jar -* jedis-2.5.2.jar -* commons-pool2-2.2.jar +* jedis-2.9.0.jar +* commons-pool2-2.4.2.jar Reboot the server, and sessions should now be stored in Redis. diff --git a/build.gradle b/build.gradle index 2bd0db6a..66bab23f 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'maven' apply plugin: 'signing' group = 'com.orangefunction' -version = '2.0.0' +version = '2.0.2' repositories { mavenCentral() @@ -16,8 +16,8 @@ compileJava { dependencies { compile group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '7.0.27' - compile group: 'redis.clients', name: 'jedis', version: '2.5.2' - compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.2' + compile group: 'redis.clients', name: 'jedis', version: '2.9.0' + compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.4.2' //compile group: 'commons-codec', name: 'commons-codec', version: '1.9' testCompile group: 'junit', name: 'junit', version: '4.+' @@ -40,22 +40,22 @@ task sourcesJar(type: Jar) { artifacts { archives jar - archives javadocJar + archives javadocJar archives sourcesJar } -signing { - sign configurations.archives -} +//signing { +// sign configurations.archives +//} uploadArchives { repositories { mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } + // repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + // authentication(userName: sonatypeUsername, password: sonatypePassword) + // } //repository(url: "https://oss.sonatype.org/content/repositories/snapshots") { // authentication(userName: sonatypeUsername, password: sonatypePassword) //} diff --git a/src/main/java/com/orangefunction/tomcat/redissessions/JavaSerializer.java b/src/main/java/com/orangefunction/tomcat/redissessions/JavaSerializer.java index 4cfedff4..8ad7a1d0 100644 --- a/src/main/java/com/orangefunction/tomcat/redissessions/JavaSerializer.java +++ b/src/main/java/com/orangefunction/tomcat/redissessions/JavaSerializer.java @@ -2,7 +2,6 @@ import org.apache.catalina.util.CustomObjectInputStream; -import javax.servlet.http.HttpSession; import java.util.Enumeration; import java.util.HashMap; diff --git a/src/main/java/com/orangefunction/tomcat/redissessions/RedisSession.java b/src/main/java/com/orangefunction/tomcat/redissessions/RedisSession.java index 0fa742d8..30ff564f 100644 --- a/src/main/java/com/orangefunction/tomcat/redissessions/RedisSession.java +++ b/src/main/java/com/orangefunction/tomcat/redissessions/RedisSession.java @@ -9,112 +9,109 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; - public class RedisSession extends StandardSession { - private final Log log = LogFactory.getLog(RedisSession.class); - - protected static Boolean manualDirtyTrackingSupportEnabled = false; - - public static void setManualDirtyTrackingSupportEnabled(Boolean enabled) { - manualDirtyTrackingSupportEnabled = enabled; - } - - protected static String manualDirtyTrackingAttributeKey = "__changed__"; - - public static void setManualDirtyTrackingAttributeKey(String key) { - manualDirtyTrackingAttributeKey = key; - } - - - protected HashMap changedAttributes; - protected Boolean dirty; - - public RedisSession(Manager manager) { - super(manager); - resetDirtyTracking(); - } - - public Boolean isDirty() { - return dirty || !changedAttributes.isEmpty(); - } - - public HashMap getChangedAttributes() { - return changedAttributes; - } - - public void resetDirtyTracking() { - changedAttributes = new HashMap<>(); - dirty = false; - } - - @Override - public void setAttribute(String key, Object value) { - if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) { - dirty = true; - return; - } - - Object oldValue = getAttribute(key); - super.setAttribute(key, value); - - if ( (value != null || oldValue != null) - && ( value == null && oldValue != null - || oldValue == null && value != null - || !value.getClass().isInstance(oldValue) - || !value.equals(oldValue) ) ) { - if (this.manager instanceof RedisSessionManager - && ((RedisSessionManager)this.manager).getSaveOnChange()) { - try { - ((RedisSessionManager)this.manager).save(this, true); - } catch (IOException ex) { - log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage()); - } - } else { - changedAttributes.put(key, value); - } - } - } - - @Override - public void removeAttribute(String name) { - super.removeAttribute(name); - if (this.manager instanceof RedisSessionManager - && ((RedisSessionManager)this.manager).getSaveOnChange()) { - try { - ((RedisSessionManager)this.manager).save(this, true); - } catch (IOException ex) { - log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage()); - } - } else { - dirty = true; - } - } - - @Override - public void setId(String id) { - // Specifically do not call super(): it's implementation does unexpected things - // like calling manager.remove(session.id) and manager.add(session). - - this.id = id; - } - - @Override - public void setPrincipal(Principal principal) { - dirty = true; - super.setPrincipal(principal); - } - - @Override - public void writeObjectData(java.io.ObjectOutputStream out) throws IOException { - super.writeObjectData(out); - out.writeLong(this.getCreationTime()); - } - - @Override - public void readObjectData(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { - super.readObjectData(in); - this.setCreationTime(in.readLong()); - } + private static final long serialVersionUID = 1L; + + private final Log log = LogFactory.getLog(RedisSession.class); + + protected static Boolean manualDirtyTrackingSupportEnabled = false; + + public static void setManualDirtyTrackingSupportEnabled(Boolean enabled) { + manualDirtyTrackingSupportEnabled = enabled; + } + + protected static String manualDirtyTrackingAttributeKey = "__changed__"; + + public static void setManualDirtyTrackingAttributeKey(String key) { + manualDirtyTrackingAttributeKey = key; + } + + protected HashMap changedAttributes; + protected Boolean dirty; + + public RedisSession(Manager manager) { + super(manager); + resetDirtyTracking(); + } + + public Boolean isDirty() { + return dirty || !changedAttributes.isEmpty(); + } + + public HashMap getChangedAttributes() { + return changedAttributes; + } + + public void resetDirtyTracking() { + changedAttributes = new HashMap<>(); + dirty = false; + } + + @Override + public void setAttribute(String key, Object value) { + if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) { + dirty = true; + return; + } + + Object oldValue = getAttribute(key); + super.setAttribute(key, value); + + if ((value != null || oldValue != null) + && (value == null && oldValue != null || oldValue == null && value != null + || !value.getClass().isInstance(oldValue) || !value.equals(oldValue))) { + if (this.manager instanceof RedisSessionManager && ((RedisSessionManager) this.manager).getSaveOnChange()) { + try { + ((RedisSessionManager) this.manager).save(this, true); + } catch (IOException ex) { + log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + + ex.getMessage()); + } + } else { + changedAttributes.put(key, value); + } + } + } + + @Override + public void removeAttribute(String name) { + super.removeAttribute(name); + if (this.manager instanceof RedisSessionManager && ((RedisSessionManager) this.manager).getSaveOnChange()) { + try { + ((RedisSessionManager) this.manager).save(this, true); + } catch (IOException ex) { + log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage()); + } + } else { + dirty = true; + } + } + + @Override + public void setId(String id) { + // Specifically do not call super(): it's implementation does unexpected things + // like calling manager.remove(session.id) and manager.add(session). + + this.id = id; + } + + @Override + public void setPrincipal(Principal principal) { + dirty = true; + super.setPrincipal(principal); + } + + @Override + public void writeObjectData(java.io.ObjectOutputStream out) throws IOException { + super.writeObjectData(out); + out.writeLong(this.getCreationTime()); + } + + @Override + public void readObjectData(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readObjectData(in); + this.setCreationTime(in.readLong()); + } } diff --git a/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionHandlerValve.java b/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionHandlerValve.java index 13da6765..7df5d2be 100644 --- a/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionHandlerValve.java +++ b/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionHandlerValve.java @@ -1,19 +1,15 @@ package com.orangefunction.tomcat.redissessions; -import org.apache.catalina.Session; -import org.apache.catalina.connector.Request; -import org.apache.catalina.connector.Response; -import org.apache.catalina.valves.ValveBase; +import java.io.IOException; import javax.servlet.ServletException; -import java.io.IOException; -import org.apache.juli.logging.Log; -import org.apache.juli.logging.LogFactory; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; public class RedisSessionHandlerValve extends ValveBase { - private final Log log = LogFactory.getLog(RedisSessionManager.class); private RedisSessionManager manager; public void setRedisSessionManager(RedisSessionManager manager) { diff --git a/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionManager.java b/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionManager.java index 2b58a261..9112674d 100644 --- a/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionManager.java +++ b/src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionManager.java @@ -4,25 +4,24 @@ import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleListener; import org.apache.catalina.util.LifecycleSupport; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.apache.catalina.LifecycleState; import org.apache.catalina.Loader; import org.apache.catalina.Valve; import org.apache.catalina.Session; import org.apache.catalina.session.ManagerBase; -import org.apache.commons.pool2.impl.GenericObjectPoolConfig; -import org.apache.commons.pool2.impl.BaseObjectPoolConfig; - import redis.clients.util.Pool; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisSentinelPool; import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; import redis.clients.jedis.Protocol; import java.io.IOException; import java.util.Arrays; -import java.util.Collections; import java.util.Enumeration; import java.util.Set; import java.util.EnumSet; @@ -32,854 +31,988 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; - public class RedisSessionManager extends ManagerBase implements Lifecycle { - enum SessionPersistPolicy { - DEFAULT, - SAVE_ON_CHANGE, - ALWAYS_SAVE_AFTER_REQUEST; - - static SessionPersistPolicy fromName(String name) { - for (SessionPersistPolicy policy : SessionPersistPolicy.values()) { - if (policy.name().equalsIgnoreCase(name)) { - return policy; - } - } - throw new IllegalArgumentException("Invalid session persist policy [" + name + "]. Must be one of " + Arrays.asList(SessionPersistPolicy.values())+ "."); - } - } - - protected byte[] NULL_SESSION = "null".getBytes(); - - private final Log log = LogFactory.getLog(RedisSessionManager.class); - - protected String host = "localhost"; - protected int port = 6379; - protected int database = 0; - protected String password = null; - protected int timeout = Protocol.DEFAULT_TIMEOUT; - protected String sentinelMaster = null; - Set sentinelSet = null; - - protected Pool connectionPool; - protected JedisPoolConfig connectionPoolConfig = new JedisPoolConfig(); - - protected RedisSessionHandlerValve handlerValve; - protected ThreadLocal currentSession = new ThreadLocal<>(); - protected ThreadLocal currentSessionSerializationMetadata = new ThreadLocal<>(); - protected ThreadLocal currentSessionId = new ThreadLocal<>(); - protected ThreadLocal currentSessionIsPersisted = new ThreadLocal<>(); - protected Serializer serializer; - - protected static String name = "RedisSessionManager"; - - protected String serializationStrategyClass = "com.orangefunction.tomcat.redissessions.JavaSerializer"; - - protected EnumSet sessionPersistPoliciesSet = EnumSet.of(SessionPersistPolicy.DEFAULT); - - /** - * The lifecycle event support for this component. - */ - protected LifecycleSupport lifecycle = new LifecycleSupport(this); - - public String getHost() { - return host; - } - - public void setHost(String host) { - this.host = host; - } - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public int getDatabase() { - return database; - } - - public void setDatabase(int database) { - this.database = database; - } - - public int getTimeout() { - return timeout; - } - - public void setTimeout(int timeout) { - this.timeout = timeout; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public void setSerializationStrategyClass(String strategy) { - this.serializationStrategyClass = strategy; - } - - public String getSessionPersistPolicies() { - StringBuilder policies = new StringBuilder(); - for (Iterator iter = this.sessionPersistPoliciesSet.iterator(); iter.hasNext();) { - SessionPersistPolicy policy = iter.next(); - policies.append(policy.name()); - if (iter.hasNext()) { - policies.append(","); - } - } - return policies.toString(); - } - - public void setSessionPersistPolicies(String sessionPersistPolicies) { - String[] policyArray = sessionPersistPolicies.split(","); - EnumSet policySet = EnumSet.of(SessionPersistPolicy.DEFAULT); - for (String policyName : policyArray) { - SessionPersistPolicy policy = SessionPersistPolicy.fromName(policyName); - policySet.add(policy); - } - this.sessionPersistPoliciesSet = policySet; - } - - public boolean getSaveOnChange() { - return this.sessionPersistPoliciesSet.contains(SessionPersistPolicy.SAVE_ON_CHANGE); - } - - public boolean getAlwaysSaveAfterRequest() { - return this.sessionPersistPoliciesSet.contains(SessionPersistPolicy.ALWAYS_SAVE_AFTER_REQUEST); - } - - public String getSentinels() { - StringBuilder sentinels = new StringBuilder(); - for (Iterator iter = this.sentinelSet.iterator(); iter.hasNext();) { - sentinels.append(iter.next()); - if (iter.hasNext()) { - sentinels.append(","); - } - } - return sentinels.toString(); - } - - public void setSentinels(String sentinels) { - if (null == sentinels) { - sentinels = ""; - } - - String[] sentinelArray = sentinels.split(","); - this.sentinelSet = new HashSet(Arrays.asList(sentinelArray)); - } - - public Set getSentinelSet() { - return this.sentinelSet; - } - - public String getSentinelMaster() { - return this.sentinelMaster; - } - - public void setSentinelMaster(String master) { - this.sentinelMaster = master; - } - - @Override - public int getRejectedSessions() { - // Essentially do nothing. - return 0; - } - - public void setRejectedSessions(int i) { - // Do nothing. - } - - protected Jedis acquireConnection() { - Jedis jedis = connectionPool.getResource(); - - if (getDatabase() != 0) { - jedis.select(getDatabase()); - } - - return jedis; - } - - protected void returnConnection(Jedis jedis, Boolean error) { - if (error) { - connectionPool.returnBrokenResource(jedis); - } else { - connectionPool.returnResource(jedis); - } - } - - protected void returnConnection(Jedis jedis) { - returnConnection(jedis, false); - } - - @Override - public void load() throws ClassNotFoundException, IOException { - - } - - @Override - public void unload() throws IOException { - - } - - /** - * Add a lifecycle event listener to this component. - * - * @param listener The listener to add - */ - @Override - public void addLifecycleListener(LifecycleListener listener) { - lifecycle.addLifecycleListener(listener); - } - - /** - * Get the lifecycle listeners associated with this lifecycle. If this - * Lifecycle has no listeners registered, a zero-length array is returned. - */ - @Override - public LifecycleListener[] findLifecycleListeners() { - return lifecycle.findLifecycleListeners(); - } - - - /** - * Remove a lifecycle event listener from this component. - * - * @param listener The listener to remove - */ - @Override - public void removeLifecycleListener(LifecycleListener listener) { - lifecycle.removeLifecycleListener(listener); - } - - /** - * Start this component and implement the requirements - * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. - * - * @exception LifecycleException if this component detects a fatal error - * that prevents this component from being used - */ - @Override - protected synchronized void startInternal() throws LifecycleException { - super.startInternal(); - - setState(LifecycleState.STARTING); - - Boolean attachedToValve = false; - for (Valve valve : getContainer().getPipeline().getValves()) { - if (valve instanceof RedisSessionHandlerValve) { - this.handlerValve = (RedisSessionHandlerValve) valve; - this.handlerValve.setRedisSessionManager(this); - log.info("Attached to RedisSessionHandlerValve"); - attachedToValve = true; - break; - } - } - - if (!attachedToValve) { - String error = "Unable to attach to session handling valve; sessions cannot be saved after the request without the valve starting properly."; - log.fatal(error); - throw new LifecycleException(error); - } - - try { - initializeSerializer(); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - log.fatal("Unable to load serializer", e); - throw new LifecycleException(e); - } - - log.info("Will expire sessions after " + getMaxInactiveInterval() + " seconds"); - - initializeDatabaseConnection(); - - setDistributable(true); - } - - - /** - * Stop this component and implement the requirements - * of {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. - * - * @exception LifecycleException if this component detects a fatal error - * that prevents this component from being used - */ - @Override - protected synchronized void stopInternal() throws LifecycleException { - if (log.isDebugEnabled()) { - log.debug("Stopping"); - } - - setState(LifecycleState.STOPPING); - - try { - connectionPool.destroy(); - } catch(Exception e) { - // Do nothing. - } - - // Require a new random number generator if we are restarted - super.stopInternal(); - } - - @Override - public Session createSession(String requestedSessionId) { - RedisSession session = null; - String sessionId = null; - String jvmRoute = getJvmRoute(); - - Boolean error = true; - Jedis jedis = null; - try { - jedis = acquireConnection(); - - // Ensure generation of a unique session identifier. - if (null != requestedSessionId) { - sessionId = sessionIdWithJvmRoute(requestedSessionId, jvmRoute); - if (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L) { - sessionId = null; - } - } else { - do { - sessionId = sessionIdWithJvmRoute(generateSessionId(), jvmRoute); - } while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L); // 1 = key set; 0 = key already existed - } - - /* Even though the key is set in Redis, we are not going to flag - the current thread as having had the session persisted since - the session isn't actually serialized to Redis yet. - This ensures that the save(session) at the end of the request - will serialize the session into Redis with 'set' instead of 'setnx'. */ - - error = false; - - if (null != sessionId) { - session = (RedisSession)createEmptySession(); - session.setNew(true); - session.setValid(true); - session.setCreationTime(System.currentTimeMillis()); - session.setMaxInactiveInterval(getMaxInactiveInterval()); - session.setId(sessionId); - session.tellNew(); - } - - currentSession.set(session); - currentSessionId.set(sessionId); - currentSessionIsPersisted.set(false); - currentSessionSerializationMetadata.set(new SessionSerializationMetadata()); - - if (null != session) { - try { - error = saveInternal(jedis, session, true); - } catch (IOException ex) { - log.error("Error saving newly created session: " + ex.getMessage()); - currentSession.set(null); - currentSessionId.set(null); - session = null; - } - } - } finally { - if (jedis != null) { - returnConnection(jedis, error); - } - } - - return session; - } - - private String sessionIdWithJvmRoute(String sessionId, String jvmRoute) { - if (jvmRoute != null) { - String jvmRoutePrefix = '.' + jvmRoute; - return sessionId.endsWith(jvmRoutePrefix) ? sessionId : sessionId + jvmRoutePrefix; - } - return sessionId; - } - - @Override - public Session createEmptySession() { - return new RedisSession(this); - } - - @Override - public void add(Session session) { - try { - save(session); - } catch (IOException ex) { - log.warn("Unable to add to session manager store: " + ex.getMessage()); - throw new RuntimeException("Unable to add to session manager store.", ex); - } - } - - @Override - public Session findSession(String id) throws IOException { - RedisSession session = null; - - if (null == id) { - currentSessionIsPersisted.set(false); - currentSession.set(null); - currentSessionSerializationMetadata.set(null); - currentSessionId.set(null); - } else if (id.equals(currentSessionId.get())) { - session = currentSession.get(); - } else { - byte[] data = loadSessionDataFromRedis(id); - if (data != null) { - DeserializedSessionContainer container = sessionFromSerializedData(id, data); - session = container.session; - currentSession.set(session); - currentSessionSerializationMetadata.set(container.metadata); - currentSessionIsPersisted.set(true); - currentSessionId.set(id); - } else { - currentSessionIsPersisted.set(false); - currentSession.set(null); - currentSessionSerializationMetadata.set(null); - currentSessionId.set(null); - } - } - - return session; - } - - public void clear() { - Jedis jedis = null; - Boolean error = true; - try { - jedis = acquireConnection(); - jedis.flushDB(); - error = false; - } finally { - if (jedis != null) { - returnConnection(jedis, error); - } - } - } - - public int getSize() throws IOException { - Jedis jedis = null; - Boolean error = true; - try { - jedis = acquireConnection(); - int size = jedis.dbSize().intValue(); - error = false; - return size; - } finally { - if (jedis != null) { - returnConnection(jedis, error); - } - } - } - - public String[] keys() throws IOException { - Jedis jedis = null; - Boolean error = true; - try { - jedis = acquireConnection(); - Set keySet = jedis.keys("*"); - error = false; - return keySet.toArray(new String[keySet.size()]); - } finally { - if (jedis != null) { - returnConnection(jedis, error); - } - } - } - - public byte[] loadSessionDataFromRedis(String id) throws IOException { - Jedis jedis = null; - Boolean error = true; - - try { - log.trace("Attempting to load session " + id + " from Redis"); - - jedis = acquireConnection(); - byte[] data = jedis.get(id.getBytes()); - error = false; - - if (data == null) { - log.trace("Session " + id + " not found in Redis"); - } - - return data; - } finally { - if (jedis != null) { - returnConnection(jedis, error); - } - } - } - - public DeserializedSessionContainer sessionFromSerializedData(String id, byte[] data) throws IOException { - log.trace("Deserializing session " + id + " from Redis"); - - if (Arrays.equals(NULL_SESSION, data)) { - log.error("Encountered serialized session " + id + " with data equal to NULL_SESSION. This is a bug."); - throw new IOException("Serialized session data was equal to NULL_SESSION"); - } - - RedisSession session = null; - SessionSerializationMetadata metadata = new SessionSerializationMetadata(); - - try { - session = (RedisSession)createEmptySession(); - - serializer.deserializeInto(data, session, metadata); - - session.setId(id); - session.setNew(false); - session.setMaxInactiveInterval(getMaxInactiveInterval()); - session.access(); - session.setValid(true); - session.resetDirtyTracking(); - - if (log.isTraceEnabled()) { - log.trace("Session Contents [" + id + "]:"); - Enumeration en = session.getAttributeNames(); - while(en.hasMoreElements()) { - log.trace(" " + en.nextElement()); - } - } - } catch (ClassNotFoundException ex) { - log.fatal("Unable to deserialize into session", ex); - throw new IOException("Unable to deserialize into session", ex); - } - - return new DeserializedSessionContainer(session, metadata); - } - - public void save(Session session) throws IOException { - save(session, false); - } - - public void save(Session session, boolean forceSave) throws IOException { - Jedis jedis = null; - Boolean error = true; - - try { - jedis = acquireConnection(); - error = saveInternal(jedis, session, forceSave); - } catch (IOException e) { - throw e; - } finally { - if (jedis != null) { - returnConnection(jedis, error); - } - } - } - - protected boolean saveInternal(Jedis jedis, Session session, boolean forceSave) throws IOException { - Boolean error = true; - - try { - log.trace("Saving session " + session + " into Redis"); - - RedisSession redisSession = (RedisSession)session; - - if (log.isTraceEnabled()) { - log.trace("Session Contents [" + redisSession.getId() + "]:"); - Enumeration en = redisSession.getAttributeNames(); - while(en.hasMoreElements()) { - log.trace(" " + en.nextElement()); - } - } - - byte[] binaryId = redisSession.getId().getBytes(); - - Boolean isCurrentSessionPersisted; - SessionSerializationMetadata sessionSerializationMetadata = currentSessionSerializationMetadata.get(); - byte[] originalSessionAttributesHash = sessionSerializationMetadata.getSessionAttributesHash(); - byte[] sessionAttributesHash = null; - if ( - forceSave - || redisSession.isDirty() - || null == (isCurrentSessionPersisted = this.currentSessionIsPersisted.get()) - || !isCurrentSessionPersisted - || !Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.attributesHashFrom(redisSession))) - ) { - - log.trace("Save was determined to be necessary"); - - if (null == sessionAttributesHash) { - sessionAttributesHash = serializer.attributesHashFrom(redisSession); - } - - SessionSerializationMetadata updatedSerializationMetadata = new SessionSerializationMetadata(); - updatedSerializationMetadata.setSessionAttributesHash(sessionAttributesHash); - - jedis.set(binaryId, serializer.serializeFrom(redisSession, updatedSerializationMetadata)); - - redisSession.resetDirtyTracking(); - currentSessionSerializationMetadata.set(updatedSerializationMetadata); - currentSessionIsPersisted.set(true); - } else { - log.trace("Save was determined to be unnecessary"); - } - - log.trace("Setting expire timeout on session [" + redisSession.getId() + "] to " + getMaxInactiveInterval()); - jedis.expire(binaryId, getMaxInactiveInterval()); - - error = false; - - return error; - } catch (IOException e) { - log.error(e.getMessage()); - - throw e; - } finally { - return error; - } - } - - @Override - public void remove(Session session) { - remove(session, false); - } - - @Override - public void remove(Session session, boolean update) { - Jedis jedis = null; - Boolean error = true; - - log.trace("Removing session ID : " + session.getId()); - - try { - jedis = acquireConnection(); - jedis.del(session.getId()); - error = false; - } finally { - if (jedis != null) { - returnConnection(jedis, error); - } - } - } - - public void afterRequest() { - RedisSession redisSession = currentSession.get(); - if (redisSession != null) { - try { - if (redisSession.isValid()) { - log.trace("Request with session completed, saving session " + redisSession.getId()); - save(redisSession, getAlwaysSaveAfterRequest()); - } else { - log.trace("HTTP Session has been invalidated, removing :" + redisSession.getId()); - remove(redisSession); - } - } catch (Exception e) { - log.error("Error storing/removing session", e); - } finally { - currentSession.remove(); - currentSessionId.remove(); - currentSessionIsPersisted.remove(); - log.trace("Session removed from ThreadLocal :" + redisSession.getIdInternal()); - } - } - } - - @Override - public void processExpires() { - // We are going to use Redis's ability to expire keys for session expiration. - - // Do nothing. - } - - private void initializeDatabaseConnection() throws LifecycleException { - try { - if (getSentinelMaster() != null) { - Set sentinelSet = getSentinelSet(); - if (sentinelSet != null && sentinelSet.size() > 0) { - connectionPool = new JedisSentinelPool(getSentinelMaster(), sentinelSet, this.connectionPoolConfig, getTimeout(), getPassword()); - } else { - throw new LifecycleException("Error configuring Redis Sentinel connection pool: expected both `sentinelMaster` and `sentiels` to be configured"); - } - } else { - connectionPool = new JedisPool(this.connectionPoolConfig, getHost(), getPort(), getTimeout(), getPassword()); - } - } catch (Exception e) { - e.printStackTrace(); - throw new LifecycleException("Error connecting to Redis", e); - } - } - - private void initializeSerializer() throws ClassNotFoundException, IllegalAccessException, InstantiationException { - log.info("Attempting to use serializer :" + serializationStrategyClass); - serializer = (Serializer) Class.forName(serializationStrategyClass).newInstance(); - - Loader loader = null; - - if (getContainer() != null) { - loader = getContainer().getLoader(); - } - - ClassLoader classLoader = null; - - if (loader != null) { - classLoader = loader.getClassLoader(); - } - serializer.setClassLoader(classLoader); - } - - - // Connection Pool Config Accessors - - // - from org.apache.commons.pool2.impl.GenericObjectPoolConfig - - public int getConnectionPoolMaxTotal() { - return this.connectionPoolConfig.getMaxTotal(); - } - - public void setConnectionPoolMaxTotal(int connectionPoolMaxTotal) { - this.connectionPoolConfig.setMaxTotal(connectionPoolMaxTotal); - } - - public int getConnectionPoolMaxIdle() { - return this.connectionPoolConfig.getMaxIdle(); - } - - public void setConnectionPoolMaxIdle(int connectionPoolMaxIdle) { - this.connectionPoolConfig.setMaxIdle(connectionPoolMaxIdle); - } - - public int getConnectionPoolMinIdle() { - return this.connectionPoolConfig.getMinIdle(); - } - - public void setConnectionPoolMinIdle(int connectionPoolMinIdle) { - this.connectionPoolConfig.setMinIdle(connectionPoolMinIdle); - } - - - // - from org.apache.commons.pool2.impl.BaseObjectPoolConfig - - public boolean getLifo() { - return this.connectionPoolConfig.getLifo(); - } - public void setLifo(boolean lifo) { - this.connectionPoolConfig.setLifo(lifo); - } - public long getMaxWaitMillis() { - return this.connectionPoolConfig.getMaxWaitMillis(); - } - - public void setMaxWaitMillis(long maxWaitMillis) { - this.connectionPoolConfig.setMaxWaitMillis(maxWaitMillis); - } - - public long getMinEvictableIdleTimeMillis() { - return this.connectionPoolConfig.getMinEvictableIdleTimeMillis(); - } - - public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) { - this.connectionPoolConfig.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); - } - - public long getSoftMinEvictableIdleTimeMillis() { - return this.connectionPoolConfig.getSoftMinEvictableIdleTimeMillis(); - } - - public void setSoftMinEvictableIdleTimeMillis(long softMinEvictableIdleTimeMillis) { - this.connectionPoolConfig.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis); - } - - public int getNumTestsPerEvictionRun() { - return this.connectionPoolConfig.getNumTestsPerEvictionRun(); - } - - public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) { - this.connectionPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun); - } - - public boolean getTestOnCreate() { - return this.connectionPoolConfig.getTestOnCreate(); - } - - public void setTestOnCreate(boolean testOnCreate) { - this.connectionPoolConfig.setTestOnCreate(testOnCreate); - } + enum SessionPersistPolicy { + DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST; + + static SessionPersistPolicy fromName(String name) { + for (SessionPersistPolicy policy : SessionPersistPolicy.values()) { + if (policy.name().equalsIgnoreCase(name)) { + return policy; + } + } + throw new IllegalArgumentException("Invalid session persist policy [" + name + "]. Must be one of " + + Arrays.asList(SessionPersistPolicy.values()) + "."); + } + } + + protected byte[] NULL_SESSION = "null".getBytes(); + + private final Log log = LogFactory.getLog(RedisSessionManager.class); + + protected String host = "localhost"; + protected int port = 6379; + protected int database = 0; + protected String password = null; + protected int timeout = Protocol.DEFAULT_TIMEOUT; + protected String sentinelMaster = null; + Set sentinelSet = null; + Set clusterSet = null; + + protected Pool connectionPool; + protected JedisCluster jedisCluster; + + protected JedisPoolConfig connectionPoolConfig = new JedisPoolConfig(); + + protected RedisSessionHandlerValve handlerValve; + protected ThreadLocal currentSession = new ThreadLocal<>(); + protected ThreadLocal currentSessionSerializationMetadata = new ThreadLocal<>(); + protected ThreadLocal currentSessionId = new ThreadLocal<>(); + protected ThreadLocal currentSessionIsPersisted = new ThreadLocal<>(); + protected Serializer serializer; + + protected static String name = "RedisSessionManager"; + + protected String serializationStrategyClass = "com.orangefunction.tomcat.redissessions.JavaSerializer"; + + protected EnumSet sessionPersistPoliciesSet = EnumSet.of(SessionPersistPolicy.DEFAULT); + + /** + * The lifecycle event support for this component. + */ + protected LifecycleSupport lifecycle = new LifecycleSupport(this); + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public int getDatabase() { + return database; + } + + public void setDatabase(int database) { + this.database = database; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setSerializationStrategyClass(String strategy) { + this.serializationStrategyClass = strategy; + } + + public String getSessionPersistPolicies() { + StringBuilder policies = new StringBuilder(); + for (Iterator iter = this.sessionPersistPoliciesSet.iterator(); iter.hasNext();) { + SessionPersistPolicy policy = iter.next(); + policies.append(policy.name()); + if (iter.hasNext()) { + policies.append(","); + } + } + return policies.toString(); + } + + public void setSessionPersistPolicies(String sessionPersistPolicies) { + String[] policyArray = sessionPersistPolicies.split(","); + EnumSet policySet = EnumSet.of(SessionPersistPolicy.DEFAULT); + for (String policyName : policyArray) { + SessionPersistPolicy policy = SessionPersistPolicy.fromName(policyName); + policySet.add(policy); + } + this.sessionPersistPoliciesSet = policySet; + } + + public boolean getSaveOnChange() { + return this.sessionPersistPoliciesSet.contains(SessionPersistPolicy.SAVE_ON_CHANGE); + } + + public boolean getAlwaysSaveAfterRequest() { + return this.sessionPersistPoliciesSet.contains(SessionPersistPolicy.ALWAYS_SAVE_AFTER_REQUEST); + } + + public String getClusters() { + StringBuilder clusters = new StringBuilder(); + for (Iterator iter = this.clusterSet.iterator(); iter.hasNext();) { + clusters.append(iter.next()); + if (iter.hasNext()) { + clusters.append(","); + } + } + return clusters.toString(); + } + + public void setClusters(String clusters) { + if (null == clusters) { + clusters = ""; + } + String[] clusterArray = clusters.split(","); + this.clusterSet = new HashSet(Arrays.asList(clusterArray)); + } + + public String getSentinels() { + StringBuilder sentinels = new StringBuilder(); + for (Iterator iter = this.sentinelSet.iterator(); iter.hasNext();) { + sentinels.append(iter.next()); + if (iter.hasNext()) { + sentinels.append(","); + } + } + return sentinels.toString(); + } + + public void setSentinels(String sentinels) { + if (null == sentinels) { + sentinels = ""; + } + + String[] sentinelArray = sentinels.split(","); + this.sentinelSet = new HashSet(Arrays.asList(sentinelArray)); + } + + public Set getSentinelSet() { + return this.sentinelSet; + } + + public Set getClusterSet() { + return this.clusterSet; + } + + public String getSentinelMaster() { + return this.sentinelMaster; + } + + public void setSentinelMaster(String master) { + this.sentinelMaster = master; + } + + @Override + public int getRejectedSessions() { + // Essentially do nothing. + return 0; + } + + public void setRejectedSessions(int i) { + // Do nothing. + } + + protected Jedis acquireConnection() { + Jedis jedis = connectionPool.getResource(); + + if (getDatabase() != 0) { + jedis.select(getDatabase()); + } + + return jedis; + } + + protected void returnConnection(Jedis jedis, Boolean error) { + if (error) { + connectionPool.returnBrokenResource(jedis); + } else { + connectionPool.returnResource(jedis); + } + } + + protected void returnConnection(Jedis jedis) { + returnConnection(jedis, false); + } + + @Override + public void load() throws ClassNotFoundException, IOException { + + } + + @Override + public void unload() throws IOException { + + } + + /** + * Add a lifecycle event listener to this component. + * + * @param listener + * The listener to add + */ + @Override + public void addLifecycleListener(LifecycleListener listener) { + lifecycle.addLifecycleListener(listener); + } + + /** + * Get the lifecycle listeners associated with this lifecycle. If this Lifecycle + * has no listeners registered, a zero-length array is returned. + */ + @Override + public LifecycleListener[] findLifecycleListeners() { + return lifecycle.findLifecycleListeners(); + } + + /** + * Remove a lifecycle event listener from this component. + * + * @param listener + * The listener to remove + */ + @Override + public void removeLifecycleListener(LifecycleListener listener) { + lifecycle.removeLifecycleListener(listener); + } + + /** + * Start this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. + * + * @exception LifecycleException + * if this component detects a fatal error that prevents this + * component from being used + */ + @Override + protected synchronized void startInternal() throws LifecycleException { + super.startInternal(); + + setState(LifecycleState.STARTING); + + Boolean attachedToValve = false; + for (Valve valve : getContainer().getPipeline().getValves()) { + if (valve instanceof RedisSessionHandlerValve) { + this.handlerValve = (RedisSessionHandlerValve) valve; + this.handlerValve.setRedisSessionManager(this); + log.info("Attached to RedisSessionHandlerValve"); + attachedToValve = true; + break; + } + } + + if (!attachedToValve) { + String error = "Unable to attach to session handling valve; sessions cannot be saved after the request without the valve starting properly."; + log.fatal(error); + throw new LifecycleException(error); + } + + try { + initializeSerializer(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + log.fatal("Unable to load serializer", e); + throw new LifecycleException(e); + } + + log.info("Will expire sessions after " + getMaxInactiveInterval() + " seconds"); + + initializeDatabaseConnection(); + + setDistributable(true); + } + + /** + * Stop this component and implement the requirements of + * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. + * + * @exception LifecycleException + * if this component detects a fatal error that prevents this + * component from being used + */ + @Override + protected synchronized void stopInternal() throws LifecycleException { + if (log.isDebugEnabled()) { + log.debug("Stopping"); + } + + setState(LifecycleState.STOPPING); + + try { + if (connectionPool != null) { + connectionPool.destroy(); + connectionPool = null; + } + if (jedisCluster != null) { + jedisCluster.close(); + } + } catch (Exception e) { + // Do nothing. + } + + // Require a new random number generator if we are restarted + super.stopInternal(); + } + + @Override + public Session createSession(String requestedSessionId) { + if (jedisCluster == null) { + return createByPool(requestedSessionId); + } else { + return createByCluster(requestedSessionId); + } + } + + private Session createByCluster(String requestedSessionId) { + RedisSession session = null; + String sessionId = null; + String jvmRoute = getJvmRoute(); + try { + if (null != requestedSessionId) { + sessionId = sessionIdWithJvmRoute(requestedSessionId, jvmRoute); + if (jedisCluster.setnx(sessionId.getBytes(), NULL_SESSION) == 0L) { + sessionId = null; + } + } else { + do { + sessionId = sessionIdWithJvmRoute(generateSessionId(), jvmRoute); + } while (jedisCluster.setnx(sessionId.getBytes(), NULL_SESSION) == 0L); // 1 = key set; 0 = key already + // existed + } + + if (null != sessionId) { + session = (RedisSession) createEmptySession(); + session.setNew(true); + session.setValid(true); + session.setCreationTime(System.currentTimeMillis()); + session.setMaxInactiveInterval(getMaxInactiveInterval()); + session.setId(sessionId); + session.tellNew(); + } + currentSession.set(session); + currentSessionId.set(sessionId); + currentSessionIsPersisted.set(false); + currentSessionSerializationMetadata.set(new SessionSerializationMetadata()); + if (null != session) { + try { + saveInternal(null, session, true); + } catch (IOException ex) { + log.error("Error saving newly created session: " + ex.getMessage()); + currentSession.set(null); + currentSessionId.set(null); + session = null; + } + } + } finally { + + } + + return session; + } + + private Session createByPool(String requestedSessionId) { + RedisSession session = null; + String sessionId = null; + String jvmRoute = getJvmRoute(); + Boolean error = true; + Jedis jedis = null; + try { + jedis = acquireConnection(); + if (null != requestedSessionId) { + sessionId = sessionIdWithJvmRoute(requestedSessionId, jvmRoute); + if (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L) { + sessionId = null; + } + } else { + do { + sessionId = sessionIdWithJvmRoute(generateSessionId(), jvmRoute); + } while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L); // 1 = key set; 0 = key already existed + } + error = false; + if (null != sessionId) { + session = (RedisSession) createEmptySession(); + session.setNew(true); + session.setValid(true); + session.setCreationTime(System.currentTimeMillis()); + session.setMaxInactiveInterval(getMaxInactiveInterval()); + session.setId(sessionId); + session.tellNew(); + } + currentSession.set(session); + currentSessionId.set(sessionId); + currentSessionIsPersisted.set(false); + currentSessionSerializationMetadata.set(new SessionSerializationMetadata()); + if (null != session) { + try { + error = saveInternal(jedis, session, true); + } catch (IOException ex) { + log.error("Error saving newly created session: " + ex.getMessage()); + currentSession.set(null); + currentSessionId.set(null); + session = null; + } + } + } finally { + if (jedis != null) { + returnConnection(jedis, error); + } + } + + return session; + } + + private String sessionIdWithJvmRoute(String sessionId, String jvmRoute) { + if (jvmRoute != null) { + String jvmRoutePrefix = '.' + jvmRoute; + return sessionId.endsWith(jvmRoutePrefix) ? sessionId : sessionId + jvmRoutePrefix; + } + return sessionId; + } + + @Override + public Session createEmptySession() { + return new RedisSession(this); + } + + @Override + public void add(Session session) { + try { + save(session); + } catch (IOException ex) { + log.warn("Unable to add to session manager store: " + ex.getMessage()); + throw new RuntimeException("Unable to add to session manager store.", ex); + } + } + + @Override + public Session findSession(String id) throws IOException { + RedisSession session = null; + + if (null == id) { + currentSessionIsPersisted.set(false); + currentSession.set(null); + currentSessionSerializationMetadata.set(null); + currentSessionId.set(null); + } else if (id.equals(currentSessionId.get())) { + session = currentSession.get(); + } else { + byte[] data = loadSessionDataFromRedis(id); + if (data != null) { + DeserializedSessionContainer container = sessionFromSerializedData(id, data); + session = container.session; + currentSession.set(session); + currentSessionSerializationMetadata.set(container.metadata); + currentSessionIsPersisted.set(true); + currentSessionId.set(id); + } else { + currentSessionIsPersisted.set(false); + currentSession.set(null); + currentSessionSerializationMetadata.set(null); + currentSessionId.set(null); + } + } + + return session; + } + + @SuppressWarnings("deprecation") + public void clear() { + if (jedisCluster != null) { + jedisCluster.flushDB(); + } else { + Jedis jedis = null; + Boolean error = true; + try { + jedis = acquireConnection(); + jedis.flushDB(); + error = false; + } finally { + if (jedis != null) { + returnConnection(jedis, error); + } + } + } + } + + public int getSize() throws IOException { + if (jedisCluster != null) { + int size = jedisCluster.dbSize().intValue(); + return size; + } else { + Jedis jedis = null; + Boolean error = true; + try { + jedis = acquireConnection(); + int size = jedis.dbSize().intValue(); + error = false; + return size; + } finally { + if (jedis != null) { + returnConnection(jedis, error); + } + } + } + } + + public String[] keys() throws IOException { + if (jedisCluster != null) { + return new String[] {}; + } else { + Jedis jedis = null; + Boolean error = true; + try { + jedis = acquireConnection(); + Set keySet = jedis.keys("*"); + error = false; + return keySet.toArray(new String[keySet.size()]); + } finally { + if (jedis != null) { + returnConnection(jedis, error); + } + } + } + } + + public byte[] loadSessionDataFromRedis(String id) throws IOException { + + if (jedisCluster != null) { + byte[] data = jedisCluster.get(id.getBytes()); + if (data == null) { + log.trace("Session " + id + " not found in Redis"); + } + return data; + } else { + + Jedis jedis = null; + Boolean error = true; + try { + log.trace("Attempting to load session " + id + " from Redis"); + + jedis = acquireConnection(); + byte[] data = jedis.get(id.getBytes()); + error = false; + + if (data == null) { + log.trace("Session " + id + " not found in Redis"); + } + + return data; + } finally { + if (jedis != null) { + returnConnection(jedis, error); + } + } + } + } + + public DeserializedSessionContainer sessionFromSerializedData(String id, byte[] data) throws IOException { + log.trace("Deserializing session " + id + " from Redis"); + + if (Arrays.equals(NULL_SESSION, data)) { + log.error("Encountered serialized session " + id + " with data equal to NULL_SESSION. This is a bug."); + throw new IOException("Serialized session data was equal to NULL_SESSION"); + } + + RedisSession session = null; + SessionSerializationMetadata metadata = new SessionSerializationMetadata(); + + try { + session = (RedisSession) createEmptySession(); + + serializer.deserializeInto(data, session, metadata); + + session.setId(id); + session.setNew(false); + session.setMaxInactiveInterval(getMaxInactiveInterval()); + session.access(); + session.setValid(true); + session.resetDirtyTracking(); + + if (log.isTraceEnabled()) { + log.trace("Session Contents [" + id + "]:"); + Enumeration en = session.getAttributeNames(); + while (en.hasMoreElements()) { + log.trace(" " + en.nextElement()); + } + } + } catch (ClassNotFoundException ex) { + log.fatal("Unable to deserialize into session", ex); + throw new IOException("Unable to deserialize into session", ex); + } + + return new DeserializedSessionContainer(session, metadata); + } + + public void save(Session session) throws IOException { + save(session, false); + } + + public void save(Session session, boolean forceSave) throws IOException { + if (jedisCluster != null) { + saveInternal(null, session, forceSave); + } else { + Jedis jedis = null; + Boolean error = true; + try { + jedis = acquireConnection(); + error = saveInternal(jedis, session, forceSave); + } catch (IOException e) { + throw e; + } finally { + if (jedis != null) { + returnConnection(jedis, error); + } + } + } + } + + protected boolean saveInternal(Jedis jedis, Session session, boolean forceSave) throws IOException { + Boolean error = true; + + checkConfig(jedis); + + try { + log.trace("Saving session " + session + " into Redis"); + + RedisSession redisSession = (RedisSession) session; + + if (log.isTraceEnabled()) { + log.trace("Session Contents [" + redisSession.getId() + "]:"); + Enumeration en = redisSession.getAttributeNames(); + while (en.hasMoreElements()) { + log.trace(" " + en.nextElement()); + } + } + + byte[] binaryId = redisSession.getId().getBytes(); + + Boolean isCurrentSessionPersisted; + SessionSerializationMetadata sessionSerializationMetadata = currentSessionSerializationMetadata.get(); + byte[] originalSessionAttributesHash = sessionSerializationMetadata.getSessionAttributesHash(); + byte[] sessionAttributesHash = null; + if (forceSave || redisSession.isDirty() + || null == (isCurrentSessionPersisted = this.currentSessionIsPersisted.get()) + || !isCurrentSessionPersisted || !Arrays.equals(originalSessionAttributesHash, + (sessionAttributesHash = serializer.attributesHashFrom(redisSession)))) { + + log.trace("Save was determined to be necessary"); + + if (null == sessionAttributesHash) { + sessionAttributesHash = serializer.attributesHashFrom(redisSession); + } + + SessionSerializationMetadata updatedSerializationMetadata = new SessionSerializationMetadata(); + updatedSerializationMetadata.setSessionAttributesHash(sessionAttributesHash); + if (jedisCluster != null) { + jedisCluster.set(binaryId, serializer.serializeFrom(redisSession, updatedSerializationMetadata)); + } else { + jedis.set(binaryId, serializer.serializeFrom(redisSession, updatedSerializationMetadata)); + } + + redisSession.resetDirtyTracking(); + currentSessionSerializationMetadata.set(updatedSerializationMetadata); + currentSessionIsPersisted.set(true); + } else { + log.trace("Save was determined to be unnecessary"); + } + + log.trace( + "Setting expire timeout on session [" + redisSession.getId() + "] to " + getMaxInactiveInterval()); + + if (jedisCluster != null) { + jedisCluster.expire(binaryId, getMaxInactiveInterval()); + } else { + jedis.expire(binaryId, getMaxInactiveInterval()); + } + error = false; + return error; + } catch (IOException e) { + log.error(e.getMessage()); + throw e; + } finally { + + } + } + + private void checkConfig(Jedis jedis) { + if (jedis != null && jedisCluster != null) { + throw new IllegalArgumentException("jedisCluster pool can not both config"); + } + } + + @Override + public void remove(Session session) { + remove(session, false); + } + + @Override + public void remove(Session session, boolean update) { + + if (jedisCluster != null) { + jedisCluster.del(session.getId()); + } else { + Jedis jedis = null; + Boolean error = true; + + log.trace("Removing session ID : " + session.getId()); + + try { + jedis = acquireConnection(); + jedis.del(session.getId()); + error = false; + } finally { + if (jedis != null) { + returnConnection(jedis, error); + } + } + } + } + + public void afterRequest() { + RedisSession redisSession = currentSession.get(); + if (redisSession != null) { + try { + if (redisSession.isValid()) { + log.trace("Request with session completed, saving session " + redisSession.getId()); + save(redisSession, getAlwaysSaveAfterRequest()); + } else { + log.trace("HTTP Session has been invalidated, removing :" + redisSession.getId()); + remove(redisSession); + } + } catch (Exception e) { + log.error("Error storing/removing session", e); + } finally { + currentSession.remove(); + currentSessionId.remove(); + currentSessionIsPersisted.remove(); + log.trace("Session removed from ThreadLocal :" + redisSession.getIdInternal()); + } + } + } + + @Override + public void processExpires() { + // We are going to use Redis's ability to expire keys for session expiration. + + // Do nothing. + } + + private void initializeDatabaseConnection() throws LifecycleException { + try { + log.info("clusters is " + clusterSet); + Set clusterSet = getClusterSet(); + if (clusterSet != null && clusterSet.size() > 0) { + Set jedisClusterNodes = new HashSet(); + for (String c : clusterSet) { + String[] sp = c.split(":"); + jedisClusterNodes.add(new HostAndPort(sp[0], Integer.parseInt(sp[1]))); + } + JedisCluster jedisCluster = new JedisCluster(jedisClusterNodes, Protocol.DEFAULT_TIMEOUT, 30, + this.connectionPoolConfig); + this.jedisCluster = jedisCluster; + + } else if (getSentinelMaster() != null) { + Set sentinelSet = getSentinelSet(); + if (sentinelSet != null && sentinelSet.size() > 0) { + connectionPool = new JedisSentinelPool(getSentinelMaster(), sentinelSet, this.connectionPoolConfig, + getTimeout(), getPassword()); + } else { + throw new LifecycleException( + "Error configuring Redis Sentinel connection pool: expected both `sentinelMaster` and `sentiels` to be configured"); + } + } else { + connectionPool = new JedisPool(this.connectionPoolConfig, getHost(), getPort(), getTimeout(), + getPassword()); + } + } catch (Exception e) { + e.printStackTrace(); + throw new LifecycleException("Error connecting to Redis", e); + } + } + + private void initializeSerializer() throws ClassNotFoundException, IllegalAccessException, InstantiationException { + log.info("Attempting to use serializer :" + serializationStrategyClass); + serializer = (Serializer) Class.forName(serializationStrategyClass).newInstance(); + + Loader loader = null; + + if (getContainer() != null) { + loader = getContainer().getLoader(); + } + + ClassLoader classLoader = null; + + if (loader != null) { + classLoader = loader.getClassLoader(); + } + serializer.setClassLoader(classLoader); + } + + // Connection Pool Config Accessors + + // - from org.apache.commons.pool2.impl.GenericObjectPoolConfig + + public int getConnectionPoolMaxTotal() { + return this.connectionPoolConfig.getMaxTotal(); + } + + public void setConnectionPoolMaxTotal(int connectionPoolMaxTotal) { + this.connectionPoolConfig.setMaxTotal(connectionPoolMaxTotal); + } + + public int getConnectionPoolMaxIdle() { + return this.connectionPoolConfig.getMaxIdle(); + } + + public void setConnectionPoolMaxIdle(int connectionPoolMaxIdle) { + this.connectionPoolConfig.setMaxIdle(connectionPoolMaxIdle); + } + + public int getConnectionPoolMinIdle() { + return this.connectionPoolConfig.getMinIdle(); + } + + public void setConnectionPoolMinIdle(int connectionPoolMinIdle) { + this.connectionPoolConfig.setMinIdle(connectionPoolMinIdle); + } + + // - from org.apache.commons.pool2.impl.BaseObjectPoolConfig + + public boolean getLifo() { + return this.connectionPoolConfig.getLifo(); + } + + public void setLifo(boolean lifo) { + this.connectionPoolConfig.setLifo(lifo); + } + + public long getMaxWaitMillis() { + return this.connectionPoolConfig.getMaxWaitMillis(); + } + + public void setMaxWaitMillis(long maxWaitMillis) { + this.connectionPoolConfig.setMaxWaitMillis(maxWaitMillis); + } + + public long getMinEvictableIdleTimeMillis() { + return this.connectionPoolConfig.getMinEvictableIdleTimeMillis(); + } + + public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) { + this.connectionPoolConfig.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); + } + + public long getSoftMinEvictableIdleTimeMillis() { + return this.connectionPoolConfig.getSoftMinEvictableIdleTimeMillis(); + } + + public void setSoftMinEvictableIdleTimeMillis(long softMinEvictableIdleTimeMillis) { + this.connectionPoolConfig.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis); + } + + public int getNumTestsPerEvictionRun() { + return this.connectionPoolConfig.getNumTestsPerEvictionRun(); + } + + public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) { + this.connectionPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun); + } + + public boolean getTestOnCreate() { + return this.connectionPoolConfig.getTestOnCreate(); + } + + public void setTestOnCreate(boolean testOnCreate) { + this.connectionPoolConfig.setTestOnCreate(testOnCreate); + } + + public boolean getTestOnBorrow() { + return this.connectionPoolConfig.getTestOnBorrow(); + } + + public void setTestOnBorrow(boolean testOnBorrow) { + this.connectionPoolConfig.setTestOnBorrow(testOnBorrow); + } + + public boolean getTestOnReturn() { + return this.connectionPoolConfig.getTestOnReturn(); + } + + public void setTestOnReturn(boolean testOnReturn) { + this.connectionPoolConfig.setTestOnReturn(testOnReturn); + } - public boolean getTestOnBorrow() { - return this.connectionPoolConfig.getTestOnBorrow(); - } + public boolean getTestWhileIdle() { + return this.connectionPoolConfig.getTestWhileIdle(); + } - public void setTestOnBorrow(boolean testOnBorrow) { - this.connectionPoolConfig.setTestOnBorrow(testOnBorrow); - } + public void setTestWhileIdle(boolean testWhileIdle) { + this.connectionPoolConfig.setTestWhileIdle(testWhileIdle); + } - public boolean getTestOnReturn() { - return this.connectionPoolConfig.getTestOnReturn(); - } + public long getTimeBetweenEvictionRunsMillis() { + return this.connectionPoolConfig.getTimeBetweenEvictionRunsMillis(); + } - public void setTestOnReturn(boolean testOnReturn) { - this.connectionPoolConfig.setTestOnReturn(testOnReturn); - } + public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) { + this.connectionPoolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); + } - public boolean getTestWhileIdle() { - return this.connectionPoolConfig.getTestWhileIdle(); - } + public String getEvictionPolicyClassName() { + return this.connectionPoolConfig.getEvictionPolicyClassName(); + } - public void setTestWhileIdle(boolean testWhileIdle) { - this.connectionPoolConfig.setTestWhileIdle(testWhileIdle); - } - - public long getTimeBetweenEvictionRunsMillis() { - return this.connectionPoolConfig.getTimeBetweenEvictionRunsMillis(); - } - - public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) { - this.connectionPoolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); - } - - public String getEvictionPolicyClassName() { - return this.connectionPoolConfig.getEvictionPolicyClassName(); - } - - public void setEvictionPolicyClassName(String evictionPolicyClassName) { - this.connectionPoolConfig.setEvictionPolicyClassName(evictionPolicyClassName); - } - - public boolean getBlockWhenExhausted() { - return this.connectionPoolConfig.getBlockWhenExhausted(); - } - - public void setBlockWhenExhausted(boolean blockWhenExhausted) { - this.connectionPoolConfig.setBlockWhenExhausted(blockWhenExhausted); - } - - public boolean getJmxEnabled() { - return this.connectionPoolConfig.getJmxEnabled(); - } - - public void setJmxEnabled(boolean jmxEnabled) { - this.connectionPoolConfig.setJmxEnabled(jmxEnabled); - } - public String getJmxNameBase() { - return this.connectionPoolConfig.getJmxNameBase(); - } - public void setJmxNameBase(String jmxNameBase) { - this.connectionPoolConfig.setJmxNameBase(jmxNameBase); - } - - public String getJmxNamePrefix() { - return this.connectionPoolConfig.getJmxNamePrefix(); - } - - public void setJmxNamePrefix(String jmxNamePrefix) { - this.connectionPoolConfig.setJmxNamePrefix(jmxNamePrefix); - } + public void setEvictionPolicyClassName(String evictionPolicyClassName) { + this.connectionPoolConfig.setEvictionPolicyClassName(evictionPolicyClassName); + } + + public boolean getBlockWhenExhausted() { + return this.connectionPoolConfig.getBlockWhenExhausted(); + } + + public void setBlockWhenExhausted(boolean blockWhenExhausted) { + this.connectionPoolConfig.setBlockWhenExhausted(blockWhenExhausted); + } + + public boolean getJmxEnabled() { + return this.connectionPoolConfig.getJmxEnabled(); + } + + public void setJmxEnabled(boolean jmxEnabled) { + this.connectionPoolConfig.setJmxEnabled(jmxEnabled); + } + + public String getJmxNameBase() { + return this.connectionPoolConfig.getJmxNameBase(); + } + + public void setJmxNameBase(String jmxNameBase) { + this.connectionPoolConfig.setJmxNameBase(jmxNameBase); + } + + public String getJmxNamePrefix() { + return this.connectionPoolConfig.getJmxNamePrefix(); + } + + public void setJmxNamePrefix(String jmxNamePrefix) { + this.connectionPoolConfig.setJmxNamePrefix(jmxNamePrefix); + } } class DeserializedSessionContainer { - public final RedisSession session; - public final SessionSerializationMetadata metadata; - public DeserializedSessionContainer(RedisSession session, SessionSerializationMetadata metadata) { - this.session = session; - this.metadata = metadata; - } + public final RedisSession session; + public final SessionSerializationMetadata metadata; + + public DeserializedSessionContainer(RedisSession session, SessionSerializationMetadata metadata) { + this.session = session; + this.metadata = metadata; + } } diff --git a/src/main/java/com/orangefunction/tomcat/redissessions/Serializer.java b/src/main/java/com/orangefunction/tomcat/redissessions/Serializer.java index b6d1161a..ffa22d76 100644 --- a/src/main/java/com/orangefunction/tomcat/redissessions/Serializer.java +++ b/src/main/java/com/orangefunction/tomcat/redissessions/Serializer.java @@ -1,6 +1,5 @@ package com.orangefunction.tomcat.redissessions; -import javax.servlet.http.HttpSession; import java.io.IOException; public interface Serializer { diff --git a/src/main/java/com/orangefunction/tomcat/redissessions/SessionSerializationMetadata.java b/src/main/java/com/orangefunction/tomcat/redissessions/SessionSerializationMetadata.java index 7d3d9276..7baad190 100644 --- a/src/main/java/com/orangefunction/tomcat/redissessions/SessionSerializationMetadata.java +++ b/src/main/java/com/orangefunction/tomcat/redissessions/SessionSerializationMetadata.java @@ -1,42 +1,42 @@ package com.orangefunction.tomcat.redissessions; -import java.io.*; - +import java.io.IOException; +import java.io.Serializable; public class SessionSerializationMetadata implements Serializable { - private byte[] sessionAttributesHash; - - public SessionSerializationMetadata() { - this.sessionAttributesHash = new byte[0]; - } - - public byte[] getSessionAttributesHash() { - return sessionAttributesHash; - } - - public void setSessionAttributesHash(byte[] sessionAttributesHash) { - this.sessionAttributesHash = sessionAttributesHash; - } - - public void copyFieldsFrom(SessionSerializationMetadata metadata) { - this.setSessionAttributesHash(metadata.getSessionAttributesHash()); - } - - private void writeObject(java.io.ObjectOutputStream out) throws IOException { - out.writeInt(sessionAttributesHash.length); - out.write(this.sessionAttributesHash); - } - - private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { - int hashLength = in.readInt(); - byte[] sessionAttributesHash = new byte[hashLength]; - in.read(sessionAttributesHash, 0, hashLength); - this.sessionAttributesHash = sessionAttributesHash; - } - - private void readObjectNoData() throws ObjectStreamException { - this.sessionAttributesHash = new byte[0]; - } + /** + * + */ + private static final long serialVersionUID = 1L; + private byte[] sessionAttributesHash; + + public SessionSerializationMetadata() { + this.sessionAttributesHash = new byte[0]; + } + + public byte[] getSessionAttributesHash() { + return sessionAttributesHash; + } + + public void setSessionAttributesHash(byte[] sessionAttributesHash) { + this.sessionAttributesHash = sessionAttributesHash; + } + + public void copyFieldsFrom(SessionSerializationMetadata metadata) { + this.setSessionAttributesHash(metadata.getSessionAttributesHash()); + } + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.writeInt(sessionAttributesHash.length); + out.write(this.sessionAttributesHash); + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + int hashLength = in.readInt(); + byte[] sessionAttributesHash = new byte[hashLength]; + in.read(sessionAttributesHash, 0, hashLength); + this.sessionAttributesHash = sessionAttributesHash; + } }