diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..5f6797b --- /dev/null +++ b/.classpath @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6143e53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/.project b/.project index 17b8606..4a6bc30 100644 --- a/.project +++ b/.project @@ -1,6 +1,6 @@ - TomcatRedisSessionManager + tomcat-cluster-redis-session-manager @@ -22,8 +22,8 @@ - org.eclipse.m2e.core.maven2Nature org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature org.eclipse.wst.common.project.facet.core.nature diff --git a/pom.xml b/pom.xml index f38516d..7060235 100644 --- a/pom.xml +++ b/pom.xml @@ -1,37 +1,26 @@ 4.0.0 - TomcatClusterEnabledRedisSessionManager - TomcatClusterEnabledRedisSessionManager - 1.0 - TomcatClusterEnabledRedisSessionManager - Tomcat 7 cluster enabled redis session manager. it supports Redis both single master and cluster - - src - - - resources - - **/*.java - - - - - - maven-compiler-plugin - 3.1 - - 1.7 - 1.7 - - - - + + tomcat-cluster-redis-session-manager + tomcat-cluster-redis-session-manager + 2.0 + jar + + tomcat-cluster-redis-session-manager + http://maven.apache.org + + + UTF-8 + UTF-8 + 1.7 + + redis.clients jedis - 2.8.0 + 2.9.0 org.apache.commons @@ -43,5 +32,37 @@ commons-logging 1.2 + + + + apache-tomcat + catalina + apache-tomcat-8.5.16 + + + apache-tomcat + servlet-api + apache-tomcat-8.5.16 + + + apache-tomcat + tomcat-api + apache-tomcat-8.5.16 + + - \ No newline at end of file + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + 1.7 + 1.7 + + + + + diff --git a/resources/ReadMe.txt b/resources/ReadMe.txt deleted file mode 100644 index d6af936..0000000 --- a/resources/ReadMe.txt +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Tomcat clustering implementation - * - * Redis session manager is the pluggable one. It uses to store session objects from Tomcat catalina to Redis data cache. - * - * @author Ranjith Manickam - * @since 1.0 - */ - -Supports: - * Apache Tomcat 8 - -Downloads: - Pre-requisite: - 1. jedis-2.8.0.jar - 2. commons-pool2-2.4.2.jar - 3. commons-logging-1.2.jar - -Tomcat Redis Cluster Enabled Session Manager jar is available in below location: -https://github.com/ran-jit/TomcatClusterRedisSessionManager/wiki - -Steps to be done, -1. Move the downloaded jars to tomcat/lib directory - * $catalina.home/lib/ - -2. Add tomcat system property "catalina.base" - * catalina.base="TOMCAT_LOCATION" - -3. Extract downloaded jar (TomcatClusterEnabledRedisSessionManager-1.0.jar) to configure Redis credentials in RedisDataCache.properties file and move the file to tomcat/conf directory - * tomcat/conf/RedisDataCache.properties - -4. Add the below two lines in tomcat/conf/context.xml - * - * - -5. Verify the session expiration time in tomcat/conf/web.xml - * - * 60 - * - -Note: - * The Redis session manager supports, both single redis master and redis cluster based on the redis.properties configuration. diff --git a/resources/RedisDataCache.properties b/resources/RedisDataCache.properties deleted file mode 100644 index b5e4abe..0000000 --- a/resources/RedisDataCache.properties +++ /dev/null @@ -1,14 +0,0 @@ -# redis hosts ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, .... -redis.hosts=127.0.0.1:6379 - -# Redis Password -redis.password= - -# set true to enable redis cluster mode -redis.cluster.enabled=false - -# Redis database (default 0) -#redis.database=0 - -# Redis connection timeout (default 2000) -#redis.timeout=2000 \ No newline at end of file diff --git a/src/com/r/tomcat/session/data/cache/IRequestSessionCacheUtils.java b/src/com/r/tomcat/session/data/cache/IRequestSessionCacheUtils.java deleted file mode 100644 index f52fdc1..0000000 --- a/src/com/r/tomcat/session/data/cache/IRequestSessionCacheUtils.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.r.tomcat.session.data.cache; - -/** - * Tomcat clustering implementation - * - * This interface holds cache implementation - * - * @author Ranjith Manickam - * @since 1.0 - */ -public interface IRequestSessionCacheUtils -{ - public boolean isAvailable(); - - public void setByteArray(String key, byte[] value); - - public byte[] getByteArray(String key); - - public void deleteKey(String key); - - public Long setStringIfKeyNotExists(byte[] key, byte[] value); - - public void expire(String key, int ttl); -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/data/cache/RedisCacheUtil.java b/src/com/r/tomcat/session/data/cache/RedisCacheUtil.java deleted file mode 100644 index 9d2ec67..0000000 --- a/src/com/r/tomcat/session/data/cache/RedisCacheUtil.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.r.tomcat.session.data.cache; - -import java.util.Properties; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import redis.clients.jedis.Jedis; -import redis.clients.jedis.exceptions.JedisConnectionException; - -/** - * Tomcat clustering implementation - * - * This cache util is uses to store and retrieve the session object in Redis data cache non cluster - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class RedisCacheUtil implements IRequestSessionCacheUtils -{ - private Log log = LogFactory.getLog(RedisCacheUtil.class); - - public boolean available = false; - - private static int numRetries = 3; - - private RedisManager manager = null; - - RedisCacheUtil(Properties properties) throws Exception { - try { - manager = RedisManager.createInstance(properties); - } catch (Exception e) { - this.available = false; - log.error("Exception initializing Redis: ", e); - } - this.available = true; - } - - @Override - public boolean isAvailable() { - return available; - } - - @Override - public void setByteArray(String key, byte[] value) { - int tries = 0; - boolean sucess = false; - do { - tries++; - try { - if (key != null && value != null) { - Jedis jedis = manager.getJedis(); - jedis.set(key.getBytes(), value); - jedis.close(); - } - sucess = true; - } catch (JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - } - } while (!sucess && tries <= numRetries); - } - - @Override - public Long setStringIfKeyNotExists(byte[] key, byte[] value) { - int tries = 0; - Long retVal = null; - boolean sucess = false; - do { - tries++; - try { - if (key != null && value != null) { - Jedis jedis = manager.getJedis(); - retVal = jedis.setnx(key, value); - jedis.close(); - } - sucess = true; - } catch (JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - } - } while (!sucess && tries <= numRetries); - return retVal; - } - - @Override - public void expire(String key, int ttl) { - int tries = 0; - boolean sucess = false; - do { - tries++; - try { - Jedis jedis = manager.getJedis(); - jedis.expire(key, ttl); - jedis.close(); - sucess = true; - } catch (JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - } - } while (!sucess && tries <= numRetries); - } - - @Override - public byte[] getByteArray(String key) { - int tries = 0; - boolean sucess = false; - byte[] array = new byte[1]; - do { - tries++; - try { - if (key != null) { - Jedis jedis = manager.getJedis(); - array = jedis.get(key.getBytes()); - jedis.close(); - } - sucess = true; - } catch (JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - } - } while (!sucess && tries <= numRetries); - return array; - } - - @Override - public void deleteKey(String key) { - int tries = 0; - boolean sucess = false; - do { - tries++; - try { - if (key != null) { - Jedis jedis = manager.getJedis(); - jedis.del(key); - jedis.close(); - } - sucess = true; - } catch (JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - } - } while (!sucess && tries <= numRetries); - } -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/data/cache/RedisClusterCacheUtil.java b/src/com/r/tomcat/session/data/cache/RedisClusterCacheUtil.java deleted file mode 100644 index 8187e31..0000000 --- a/src/com/r/tomcat/session/data/cache/RedisClusterCacheUtil.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.r.tomcat.session.data.cache; - -import java.util.Properties; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException; -import redis.clients.jedis.exceptions.JedisConnectionException; - -/** - * Tomcat clustering implementation - * - * This cache util is uses to store and retrieve the session object in Redis data cache cluster - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class RedisClusterCacheUtil implements IRequestSessionCacheUtils -{ - private Log log = LogFactory.getLog(RedisClusterCacheUtil.class); - - public boolean available = false; - - private static int numRetries = 30; - - private RedisClusterManager clusterManager = null; - - RedisClusterCacheUtil(Properties properties) throws Exception { - try { - clusterManager = RedisClusterManager.createInstance(properties); - } catch (Exception e) { - this.available = false; - log.error("Exception initializing Redis cluster: " + e); - } - this.available = true; - } - - @Override - public boolean isAvailable() { - return available; - } - - @Override - public void setByteArray(String key, byte[] value) { - int tries = 0; - boolean sucess = false; - do { - tries++; - try { - if (key != null && value != null) { - clusterManager.getJedis().set(key.getBytes(), value); - } - sucess = true; - } catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - waitforFailover(); - } - } while (!sucess && tries <= numRetries); - } - - @Override - public Long setStringIfKeyNotExists(byte[] key, byte[] value) { - int tries = 0; - Long retVal = null; - boolean sucess = false; - do { - tries++; - try { - if (key != null && value != null) { - retVal = clusterManager.getJedis().setnx(key, value); - } - sucess = true; - } catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - waitforFailover(); - } - } while (!sucess && tries <= numRetries); - return retVal; - } - - @Override - public void expire(String key, int ttl) { - int tries = 0; - boolean sucess = false; - do { - tries++; - try { - clusterManager.getJedis().expire(key, ttl); - sucess = true; - } catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - waitforFailover(); - } - } while (!sucess && tries <= numRetries); - } - - @Override - public byte[] getByteArray(String key) { - int tries = 0; - boolean sucess = false; - byte[] array = new byte[1]; - do { - tries++; - try { - if (key != null) { - array = clusterManager.getJedis().get(key.getBytes()); - } - sucess = true; - } catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - waitforFailover(); - } - } while (!sucess && tries <= numRetries); - return array; - } - - @Override - public void deleteKey(String key) { - int tries = 0; - boolean sucess = false; - do { - tries++; - try { - if (key != null) { - clusterManager.getJedis().del(key); - } - sucess = true; - } catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) { - log.error("Jedis connection failed, retrying..." + tries); - if (tries == numRetries) { - throw e; - } - waitforFailover(); - } - } while (!sucess && tries <= numRetries); - } - - private void waitforFailover() { - try { - Thread.sleep(4000); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/data/cache/RedisClusterManager.java b/src/com/r/tomcat/session/data/cache/RedisClusterManager.java deleted file mode 100644 index 001d372..0000000 --- a/src/com/r/tomcat/session/data/cache/RedisClusterManager.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.r.tomcat.session.data.cache; - -import java.util.HashSet; -import java.util.Properties; -import java.util.Set; - -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.JedisCluster; -import redis.clients.jedis.JedisPoolConfig; -import redis.clients.jedis.Protocol; - -import com.r.tomcat.session.data.cache.constants.RedisConstants; - -/** - * Tomcat clustering implementation - * - * This manager is uses to initialize the Redis data cache cluster client - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class RedisClusterManager -{ - private Properties properties; - - private static JedisCluster jedisCluster; - - private static RedisClusterManager instance; - - private RedisClusterManager(Properties properties) { - this.properties = properties; - } - - public static RedisClusterManager createInstance(Properties properties) { - instance = new RedisClusterManager(properties); - instance.connect(); - return instance; - } - - public final static RedisClusterManager getInstance() throws Exception { - return instance; - } - - public void connect() { - JedisPoolConfig poolConfig = new JedisPoolConfig(); - int maxActive = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE)); - poolConfig.setMaxTotal(maxActive); - boolean testOnBorrow = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONBORROW, RedisConstants.DEFAULT_TEST_ONBORROW_VALUE)); - poolConfig.setTestOnBorrow(testOnBorrow); - boolean testOnReturn = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONRETURN, RedisConstants.DEFAULT_TEST_ONRETURN_VALUE)); - poolConfig.setTestOnReturn(testOnReturn); - int maxIdle = Integer.parseInt(properties.getProperty(RedisConstants.MAX_IDLE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE)); - poolConfig.setMaxIdle(maxIdle); - int minIdle = Integer.parseInt(properties.getProperty(RedisConstants.MIN_IDLE, RedisConstants.DEFAULT_MIN_IDLE_VALUE)); - poolConfig.setMinIdle(minIdle); - boolean testWhileIdle = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_WHILEIDLE, RedisConstants.DEFAULT_TEST_WHILEIDLE_VALUE)); - poolConfig.setTestWhileIdle(testWhileIdle); - int testNumPerEviction = Integer.parseInt(properties.getProperty(RedisConstants.TEST_NUMPEREVICTION, RedisConstants.DEFAULT_TEST_NUMPEREVICTION_VALUE)); - poolConfig.setNumTestsPerEvictionRun(testNumPerEviction); - long timeBetweenEviction = Long.parseLong(properties.getProperty(RedisConstants.TIME_BETWEENEVICTION, RedisConstants.DEFAULT_TIME_BETWEENEVICTION_VALUE)); - poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEviction); - int timeout = Integer.parseInt(properties.getProperty(RedisConstants.TIMEOUT, String.valueOf(Protocol.DEFAULT_TIMEOUT))); - jedisCluster = new JedisCluster(getJedisClusterNodesSet(properties.getProperty(RedisConstants.HOSTS, Protocol.DEFAULT_HOST.concat(":").concat(String.valueOf(Protocol.DEFAULT_PORT)))), (timeout < Protocol.DEFAULT_TIMEOUT ? Protocol.DEFAULT_TIMEOUT : timeout), poolConfig); - } - - /** - * method to get the cluster nodes - * - * @param hosts - * @return - */ - private Set getJedisClusterNodesSet(String hosts) { - Set nodes = new HashSet(); - hosts = hosts.replaceAll("\\s", ""); - String[] hostPorts = hosts.split(","); - for (String hostPort : hostPorts) { - String[] hostPortArr = hostPort.split(":"); - nodes.add(new HostAndPort(hostPortArr[0], Integer.valueOf(hostPortArr[1]))); - } - return nodes; - } - - public JedisCluster getJedis() { - return jedisCluster; - } -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/data/cache/RedisManager.java b/src/com/r/tomcat/session/data/cache/RedisManager.java deleted file mode 100644 index b49ce56..0000000 --- a/src/com/r/tomcat/session/data/cache/RedisManager.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.r.tomcat.session.data.cache; - -import java.util.Properties; - -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; -import redis.clients.jedis.Protocol; - -import com.r.tomcat.session.data.cache.constants.RedisConstants; - -/** - * Tomcat clustering implementation - * - * This manager is uses to initialize the Redis data cache client - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class RedisManager -{ - private static RedisManager instance; - - private static JedisPool pool; - - private Properties properties; - - private RedisManager(Properties properties) { - this.properties = properties; - } - - public static RedisManager createInstance(Properties properties) { - instance = new RedisManager(properties); - instance.connect(); - return instance; - } - - public final static RedisManager getInstance() throws Exception { - return instance; - } - - public void connect() { - JedisPoolConfig poolConfig = new JedisPoolConfig(); - int maxActive = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE)); - poolConfig.setMaxTotal(maxActive); - boolean testOnBorrow = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONBORROW, RedisConstants.DEFAULT_TEST_ONBORROW_VALUE)); - poolConfig.setTestOnBorrow(testOnBorrow); - boolean testOnReturn = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONRETURN, RedisConstants.DEFAULT_TEST_ONRETURN_VALUE)); - poolConfig.setTestOnReturn(testOnReturn); - int maxIdle = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE)); - poolConfig.setMaxIdle(maxIdle); - int minIdle = Integer.parseInt(properties.getProperty(RedisConstants.MIN_IDLE, RedisConstants.DEFAULT_MIN_IDLE_VALUE)); - poolConfig.setMinIdle(minIdle); - boolean testWhileIdle = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_WHILEIDLE, RedisConstants.DEFAULT_TEST_WHILEIDLE_VALUE)); - poolConfig.setTestWhileIdle(testWhileIdle); - int testNumPerEviction = Integer.parseInt(properties.getProperty(RedisConstants.TEST_NUMPEREVICTION, RedisConstants.DEFAULT_TEST_NUMPEREVICTION_VALUE)); - poolConfig.setNumTestsPerEvictionRun(testNumPerEviction); - long timeBetweenEviction = Long.parseLong(properties.getProperty(RedisConstants.TIME_BETWEENEVICTION, RedisConstants.DEFAULT_TIME_BETWEENEVICTION_VALUE)); - poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEviction); - String hosts = properties.getProperty(RedisConstants.HOSTS, Protocol.DEFAULT_HOST.concat(":").concat(String.valueOf(Protocol.DEFAULT_PORT))); - String host = null; - int port = 0; - hosts = hosts.replaceAll("\\s", ""); - String[] hostPorts = hosts.split(","); - for (String hostPort : hostPorts) { - String[] hostPortArr = hostPort.split(":"); - host = hostPortArr[0]; - port = Integer.valueOf(hostPortArr[1]); - if (!host.isEmpty() && port != 0) { - break; - } - } - String password = properties.getProperty(RedisConstants.PASSWORD); - int database = Integer.parseInt(properties.getProperty(RedisConstants.DATABASE, String.valueOf(Protocol.DEFAULT_DATABASE))); - int timeout = Integer.parseInt(properties.getProperty(RedisConstants.TIMEOUT, String.valueOf(Protocol.DEFAULT_TIMEOUT))); - pool = new JedisPool(poolConfig, host, port, (timeout < Protocol.DEFAULT_TIMEOUT ? Protocol.DEFAULT_TIMEOUT : timeout), ((password == null || password == "" || password.isEmpty()) ? null : password), database); - } - - public Jedis getJedis() { - return pool.getResource(); - } -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/data/cache/RequestSessionCacheFactory.java b/src/com/r/tomcat/session/data/cache/RequestSessionCacheFactory.java deleted file mode 100644 index d8554df..0000000 --- a/src/com/r/tomcat/session/data/cache/RequestSessionCacheFactory.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.r.tomcat.session.data.cache; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import com.r.tomcat.session.data.cache.constants.RedisConstants; - -/** - * Tomcat clustering implementation - * - * This factory is uses to get the request session data cache - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class RequestSessionCacheFactory -{ - private static Log log = LogFactory.getLog(RequestSessionCacheFactory.class); - - protected static IRequestSessionCacheUtils requestCache; - - public static synchronized IRequestSessionCacheUtils getInstance() { - try { - if (requestCache == null) { - Properties properties = getRedisProperties(); - if (!Boolean.valueOf(properties.getProperty(RedisConstants.IS_CLUSTER_ENABLED, RedisConstants.DEFAULT_IS_CLUSTER_ENABLED))) { - requestCache = new RedisCacheUtil(properties); - } else { - requestCache = new RedisClusterCacheUtil(properties); - } - } - } catch (Exception e) { - log.error("Error occurred initializing redis", e); - } - return requestCache; - } - - private static Properties getRedisProperties() throws Exception { - Properties properties = null; - try { - if (properties == null || properties.isEmpty()) { - InputStream resourceStream = null; - try { - resourceStream = null; - properties = new Properties(); - File file = new File(System.getProperty("catalina.base").concat(File.separator).concat("conf").concat(File.separator).concat(RedisConstants.REDIS_DATA_CACHE_PROPERTIES_FILE)); - if (file.exists()) { - resourceStream = new FileInputStream(file); - } - if (resourceStream == null) { - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - resourceStream = loader.getResourceAsStream(RedisConstants.REDIS_DATA_CACHE_PROPERTIES_FILE); - } - properties.load(resourceStream); - } finally { - resourceStream.close(); - } - } - } catch (IOException e) { - log.error("Error occurred fetching redis informations", e); - } - return properties; - } -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/management/CustomRequestSession.java b/src/com/r/tomcat/session/management/CustomRequestSession.java deleted file mode 100644 index dc98602..0000000 --- a/src/com/r/tomcat/session/management/CustomRequestSession.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.r.tomcat.session.management; - -import java.io.IOException; -import java.security.Principal; -import java.util.Enumeration; -import java.util.HashMap; - -import org.apache.catalina.Manager; -import org.apache.catalina.session.StandardSession; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Tomcat clustering implementation - * - * This class is uses to store and retrieve the HTTP request session objects from catalina to data cache - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class CustomRequestSession extends StandardSession -{ - private static final long serialVersionUID = 8237845843135996014L; - - private final Log log = LogFactory.getLog(CustomRequestSession.class); - - protected Boolean dirty; - - protected HashMap changedAttributes; - - protected static Boolean manualDirtyTrackingSupportEnabled = false; - - protected static String manualDirtyTrackingAttributeKey = "__changed__"; - - public static void setManualDirtyTrackingSupportEnabled(Boolean enabled) { - manualDirtyTrackingSupportEnabled = enabled; - } - - public static void setManualDirtyTrackingAttributeKey(String key) { - manualDirtyTrackingAttributeKey = key; - } - - public CustomRequestSession(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 RequestSessionManager && ((RequestSessionManager) this.manager).getSaveOnChange()) { - try { - ((RequestSessionManager) this.manager).save(this, true); - } catch (Exception ex) { - log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage()); - } - } else { - changedAttributes.put(key, value); - } - } - } - - @Override - public Object getAttribute(String name) { - return super.getAttribute(name); - } - - @Override - public Enumeration getAttributeNames() { - return super.getAttributeNames(); - } - - @Override - public void removeAttribute(String name) { - super.removeAttribute(name); - if (this.manager instanceof RequestSessionManager && ((RequestSessionManager) this.manager).getSaveOnChange()) { - try { - ((RequestSessionManager) this.manager).save(this, true); - } catch (Exception ex) { - log.error("Error saving session on removeAttribute (triggered by saveOnChange=true): " + ex.getMessage()); - } - } else { - dirty = true; - } - } - - @Override - public void setId(String id) { - 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()); - } -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/management/DeserializedSessionContainer.java b/src/com/r/tomcat/session/management/DeserializedSessionContainer.java deleted file mode 100644 index afa9d72..0000000 --- a/src/com/r/tomcat/session/management/DeserializedSessionContainer.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.r.tomcat.session.management; - -/** - * Tomcat clustering implementation - * - * This class is uses to store and retrieve the HTTP request session objects from catalina to data cache - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class DeserializedSessionContainer -{ - public final CustomRequestSession session; - - public final SessionSerializationMetadata metadata; - - public DeserializedSessionContainer(CustomRequestSession session, SessionSerializationMetadata metadata) { - this.session = session; - this.metadata = metadata; - } -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/management/RequestSessionManager.java b/src/com/r/tomcat/session/management/RequestSessionManager.java deleted file mode 100644 index 6a41389..0000000 --- a/src/com/r/tomcat/session/management/RequestSessionManager.java +++ /dev/null @@ -1,348 +0,0 @@ -package com.r.tomcat.session.management; - -import java.io.IOException; -import java.util.Arrays; -import java.util.EnumSet; -import java.util.Iterator; - -import org.apache.catalina.Lifecycle; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.LifecycleListener; -import org.apache.catalina.LifecycleState; -import org.apache.catalina.Loader; -import org.apache.catalina.Session; -import org.apache.catalina.Valve; -import org.apache.catalina.connector.Request; -import org.apache.catalina.session.ManagerBase; -import org.apache.catalina.util.LifecycleSupport; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import com.r.tomcat.session.data.cache.IRequestSessionCacheUtils; -import com.r.tomcat.session.data.cache.RequestSessionCacheFactory; - -/** - * Tomcat clustering implementation - * - * This class is uses to store and retrieve the HTTP request session objects from catalina to data cache - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class RequestSessionManager extends ManagerBase implements Lifecycle -{ - private final Log log = LogFactory.getLog(RequestSessionManager.class); - - private IRequestSessionCacheUtils requestSessionCacheUtils; - - protected SessionDataSerializer serializer; - - protected RequestSessionHandlerValve handlerValve; - - protected byte[] NULL_SESSION = "null".getBytes(); - - protected LifecycleSupport lifecycle = new LifecycleSupport(this); - - protected ThreadLocal currentSessionId = new ThreadLocal<>(); - - protected ThreadLocal currentSession = new ThreadLocal<>(); - - protected ThreadLocal currentSessionIsPersisted = new ThreadLocal<>(); - - protected EnumSet sessionPersistPoliciesSet = EnumSet.of(SessionPersistPolicy.DEFAULT); - - protected ThreadLocal currentSessionSerializationMetadata = new ThreadLocal<>(); - - 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()) + "."); - } - } - - 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); - } - - @Override - public void addLifecycleListener(LifecycleListener listener) { - lifecycle.addLifecycleListener(listener); - } - - @Override - public LifecycleListener[] findLifecycleListeners() { - return lifecycle.findLifecycleListeners(); - } - - @Override - public void removeLifecycleListener(LifecycleListener listener) { - lifecycle.removeLifecycleListener(listener); - } - - @Override - protected synchronized void startInternal() throws LifecycleException { - super.startInternal(); - setState(LifecycleState.STARTING); - Boolean attachedToValve = false; - for (Valve valve : getContainer().getPipeline().getValves()) { - if (valve instanceof RequestSessionHandlerValve) { - this.handlerValve = (RequestSessionHandlerValve) valve; - this.handlerValve.setRedisSessionManager(this); - attachedToValve = true; - break; - } - } - if (!attachedToValve) { - throw new LifecycleException("Unable to attach to session handling valve; sessions cannot be saved after the request without the valve starting properly."); - } - try { - initializeSessionSerializer(); - requestSessionCacheUtils = RequestSessionCacheFactory.getInstance(); - } catch (Exception e) { - log.error("Error while initializing serializer/rediscache", e); - } - log.info("The sessions will expire after " + (getContext().getSessionTimeout() * 60) + " seconds"); - getContext().setDistributable(true); - } - - @Override - protected synchronized void stopInternal() throws LifecycleException { - setState(LifecycleState.STOPPING); - super.stopInternal(); - } - - @Override - public Session createSession(String requestedSessionId) { - CustomRequestSession customSession = null; - String sessionId = null; - if (requestedSessionId != null) { - sessionId = requestedSessionId; - if (requestSessionCacheUtils.setStringIfKeyNotExists(sessionId.getBytes(), NULL_SESSION) == 0L) { - sessionId = null; - } - } else { - do { - sessionId = generateSessionId(); - } while (requestSessionCacheUtils.setStringIfKeyNotExists(sessionId.getBytes(), NULL_SESSION) == 0L); // 1 = key set; 0 = key already existed - } - if (sessionId != null) { - customSession = (CustomRequestSession) createEmptySession(); - customSession.setNew(true); - customSession.setValid(true); - customSession.setCreationTime(System.currentTimeMillis()); - customSession.setMaxInactiveInterval((getContext().getSessionTimeout() * 60)); - customSession.setId(sessionId); - customSession.tellNew(); - } - currentSession.set(customSession); - currentSessionId.set(sessionId); - currentSessionIsPersisted.set(false); - currentSessionSerializationMetadata.set(new SessionSerializationMetadata()); - if (customSession != null) { - try { - save(customSession, true); - } catch (Exception e) { - log.error("Error saving newly created session", e); - currentSession.set(null); - currentSessionId.set(null); - customSession = null; - } - } - return customSession; - } - - @Override - public Session createEmptySession() { - return new CustomRequestSession(this); - } - - @Override - public void add(Session session) { - save(session, false); - } - - @Override - public Session findSession(String sessionId) throws IOException { - CustomRequestSession customSession = null; - if (sessionId == null) { - currentSessionIsPersisted.set(false); - currentSession.set(null); - currentSessionSerializationMetadata.set(null); - currentSessionId.set(null); - } else if (sessionId.equals(currentSessionId.get())) { - customSession = currentSession.get(); - } else { - byte[] data = requestSessionCacheUtils.getByteArray(sessionId); - if (data != null) { - DeserializedSessionContainer container = deserializeSessionData(sessionId, data); - customSession = (CustomRequestSession) container.session; - currentSession.set(customSession); - currentSessionSerializationMetadata.set(container.metadata); - currentSessionIsPersisted.set(true); - currentSessionId.set(sessionId); - } else { - currentSessionIsPersisted.set(false); - currentSession.set(null); - currentSessionSerializationMetadata.set(null); - currentSessionId.set(null); - } - } - return customSession; - } - - @Override - public void remove(Session session) { - remove(session, false); - } - - @Override - public void remove(Session session, boolean update) { - requestSessionCacheUtils.expire(session.getId(), 10); - } - - @Override - public void load() throws ClassNotFoundException, IOException { - // TODO Auto-generated method stub - } - - @Override - public void unload() throws IOException { - // TODO Auto-generated method stub - } - - /** - * method to deserialize session data - * - * @param id - * @param data - * @return - * @throws IOException - */ - public DeserializedSessionContainer deserializeSessionData(String id, byte[] data) throws IOException { - if (Arrays.equals(NULL_SESSION, data)) { - throw new IOException("Serialized session data was equal to NULL_SESSION"); - } - CustomRequestSession customSession = null; - SessionSerializationMetadata metadata = null; - try { - metadata = new SessionSerializationMetadata(); - customSession = (CustomRequestSession) createEmptySession(); - serializer.deserializeSessionData(data, customSession, metadata); - customSession.setId(id); - customSession.setNew(false); - customSession.setMaxInactiveInterval((getContext().getSessionTimeout() * 60)); - customSession.access(); - customSession.setValid(true); - customSession.resetDirtyTracking(); - } catch (Exception e) { - log.error("Unable to deserialize into session", e); - } - return new DeserializedSessionContainer(customSession, metadata); - } - - /** - * method to save session data to cache - * - * @param session - * @param forceSave - */ - public void save(Session session, boolean forceSave) { - Boolean isCurrentSessionPersisted; - try { - CustomRequestSession customSession = (CustomRequestSession) session; - SessionSerializationMetadata sessionSerializationMetadata = currentSessionSerializationMetadata.get(); - byte[] originalSessionAttributesHash = sessionSerializationMetadata.getSessionAttributesHash(); - byte[] sessionAttributesHash = null; - if (forceSave || customSession.isDirty() || (isCurrentSessionPersisted = this.currentSessionIsPersisted.get()) == null || !isCurrentSessionPersisted || !Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.getSessionAttributesHashCode(customSession)))) { - if (sessionAttributesHash == null) { - sessionAttributesHash = serializer.getSessionAttributesHashCode(customSession); - } - SessionSerializationMetadata updatedSerializationMetadata = new SessionSerializationMetadata(); - updatedSerializationMetadata.setSessionAttributesHash(sessionAttributesHash); - requestSessionCacheUtils.setByteArray(customSession.getId(), serializer.serializeSessionData(customSession, updatedSerializationMetadata)); - customSession.resetDirtyTracking(); - currentSessionSerializationMetadata.set(updatedSerializationMetadata); - currentSessionIsPersisted.set(true); - } - - int timeout = getContext().getSessionTimeout() * 60; - timeout = timeout < 1800 ? 1800 : timeout; - log.trace("Setting expire timeout on session [" + customSession.getId() + "] to " + timeout); - requestSessionCacheUtils.expire(customSession.getId(), timeout); - } catch (IOException e) { - log.error("Error occured while storing the session object into redis", e); - } - } - - public void afterRequest(Request request) { - CustomRequestSession customSession = currentSession.get(); - if (customSession != null) { - try { - if (customSession.isValid()) { - log.trace("Request with session completed, saving session " + customSession.getId()); - save(customSession, getAlwaysSaveAfterRequest()); - } else { - log.trace("HTTP Session has been invalidated, removing :" + customSession.getId()); - remove(customSession); - } - } catch (Exception e) { - log.error("Error storing/updating/removing session", e); - } finally { - currentSession.remove(); - currentSessionId.remove(); - currentSessionIsPersisted.remove(); - log.trace("Session removed from ThreadLocal :" + customSession.getIdInternal()); - } - } - } - - /** - * method to initialize custom session serializer - * - * @throws Exception - */ - private void initializeSessionSerializer() throws Exception { - serializer = new SessionDataSerializer(); - Loader loader = null; - if (getContext() != null) { - loader = getContext().getLoader(); - } - ClassLoader classLoader = null; - if (loader != null) { - classLoader = loader.getClassLoader(); - } - serializer.setClassLoader(classLoader); - } -} diff --git a/src/com/r/tomcat/session/management/SessionSerializationMetadata.java b/src/com/r/tomcat/session/management/SessionSerializationMetadata.java deleted file mode 100644 index d078aa6..0000000 --- a/src/com/r/tomcat/session/management/SessionSerializationMetadata.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.r.tomcat.session.management; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; - -/** - * Tomcat clustering implementation - * - * This class is uses to store and retrieve the HTTP request session objects from catalina to data cache - * - * @author Ranjith Manickam - * @since 1.0 - */ -public class SessionSerializationMetadata implements Serializable -{ - private static final long serialVersionUID = 124438185184833546L; - - 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(ObjectOutputStream out) throws IOException { - out.writeInt(sessionAttributesHash.length); - out.write(this.sessionAttributesHash); - } - - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - int hashLength = in.readInt(); - byte[] sessionAttributesHash = new byte[hashLength]; - in.read(sessionAttributesHash, 0, hashLength); - this.sessionAttributesHash = sessionAttributesHash; - } -} \ No newline at end of file diff --git a/src/com/r/tomcat/session/management/SessionDataSerializer.java b/src/main/java/tomcat/request/session/SerializationUtil.java similarity index 53% rename from src/com/r/tomcat/session/management/SessionDataSerializer.java rename to src/main/java/tomcat/request/session/SerializationUtil.java index dbb67ba..25f7f15 100644 --- a/src/com/r/tomcat/session/management/SessionDataSerializer.java +++ b/src/main/java/tomcat/request/session/SerializationUtil.java @@ -1,4 +1,4 @@ -package com.r.tomcat.session.management; +package tomcat.request.session; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -10,68 +10,79 @@ import java.io.ObjectOutputStream; import java.security.MessageDigest; import java.util.Enumeration; import java.util.HashMap; +import java.util.Map; import org.apache.catalina.util.CustomObjectInputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** - * Tomcat clustering implementation + * Tomcat clustering with Redis data-cache implementation. * - * This class is uses to store and retrieve the HTTP request session objects from catalina to data cache + * Session serialization utility. * * @author Ranjith Manickam - * @since 1.0 + * @since 2.0 */ -public class SessionDataSerializer -{ - private final Log log = LogFactory.getLog(SessionDataSerializer.class); +public class SerializationUtil { private ClassLoader loader; + private Log log = LogFactory.getLog(SerializationUtil.class); + + /** + * To set class loader + * + * @param loader + */ public void setClassLoader(ClassLoader loader) { this.loader = loader; } /** - * method to get session attributes hash code + * To get session attributes hash code * * @param session * @return * @throws IOException */ - public byte[] getSessionAttributesHashCode(CustomRequestSession session) throws IOException { + public byte[] getSessionAttributesHashCode(Session session) throws IOException { byte[] serialized = null; - HashMap attributes = new HashMap(); + Map attributes = new HashMap(); + for (Enumeration enumerator = session.getAttributeNames(); enumerator.hasMoreElements();) { String key = enumerator.nextElement(); attributes.put(key, session.getAttribute(key)); } - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) { + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) { oos.writeUnshared(attributes); oos.flush(); serialized = bos.toByteArray(); } + MessageDigest digester = null; try { digester = MessageDigest.getInstance("MD5"); - } catch (Exception e) { - log.error("Unable to get MessageDigest instance for MD5"); + } catch (Exception ex) { + log.error("Unable to get MessageDigest instance for MD5", ex); } return digester.digest(serialized); } /** - * method to serialize custom session data + * To serialize session object * * @param session * @param metadata * @return * @throws IOException */ - public byte[] serializeSessionData(CustomRequestSession session, SessionSerializationMetadata metadata) throws IOException { + public byte[] serializeSessionData(Session session, SessionMetadata metadata) throws IOException { byte[] serialized = null; - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) { oos.writeObject(metadata); session.writeObjectData(oos); oos.flush(); @@ -81,7 +92,7 @@ public class SessionDataSerializer } /** - * method to deserialize custom session data + * To de-serialize session object * * @param data * @param session @@ -89,9 +100,11 @@ public class SessionDataSerializer * @throws IOException * @throws ClassNotFoundException */ - public void deserializeSessionData(byte[] data, CustomRequestSession session, SessionSerializationMetadata metadata) throws IOException, ClassNotFoundException { - try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data)); ObjectInputStream ois = new CustomObjectInputStream(bis, loader);) { - SessionSerializationMetadata serializedMetadata = (SessionSerializationMetadata) ois.readObject(); + public void deserializeSessionData(byte[] data, Session session, SessionMetadata metadata) + throws IOException, ClassNotFoundException { + try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data)); + ObjectInputStream ois = new CustomObjectInputStream(bis, this.loader);) { + SessionMetadata serializedMetadata = (SessionMetadata) ois.readObject(); metadata.copyFieldsFrom(serializedMetadata); session.readObjectData(ois); } diff --git a/src/main/java/tomcat/request/session/Session.java b/src/main/java/tomcat/request/session/Session.java new file mode 100644 index 0000000..5645014 --- /dev/null +++ b/src/main/java/tomcat/request/session/Session.java @@ -0,0 +1,133 @@ +package tomcat.request.session; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.security.Principal; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.catalina.Manager; +import org.apache.catalina.session.StandardSession; + +import tomcat.request.session.redis.SessionManager; + +/** + * Tomcat clustering with Redis data-cache implementation. + * + * This class is uses to store and retrieve the HTTP request session objects. + * + * @author Ranjith Manickam + * @since 2.0 + */ +public class Session extends StandardSession { + + private static final long serialVersionUID = -6056744304016869278L; + + protected Boolean dirty; + + protected Map changedAttributes; + + protected static Boolean manualDirtyTrackingSupportEnabled = false; + + protected static String manualDirtyTrackingAttributeKey = "__changed__"; + + public Session(Manager manager) { + super(manager); + resetDirtyTracking(); + } + + public void resetDirtyTracking() { + this.changedAttributes = new HashMap<>(); + this.dirty = false; + } + + public static void setManualDirtyTrackingSupportEnabled(boolean enabled) { + manualDirtyTrackingSupportEnabled = enabled; + } + + public static void setManualDirtyTrackingAttributeKey(String key) { + manualDirtyTrackingAttributeKey = key; + } + + public Boolean isDirty() { + return this.dirty || !this.changedAttributes.isEmpty(); + } + + public Map getChangedAttributes() { + return this.changedAttributes; + } + + /** {@inheritDoc} */ + @Override + public void setId(String id) { + this.id = id; + } + + /** {@inheritDoc} */ + @Override + public void setAttribute(String key, Object value) { + if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) { + this.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 SessionManager && ((SessionManager) this.manager).getSaveOnChange()) { + ((SessionManager) this.manager).save(this, true); + } else { + this.changedAttributes.put(key, value); + } + } + } + + /** {@inheritDoc} */ + @Override + public Object getAttribute(String name) { + return super.getAttribute(name); + } + + /** {@inheritDoc} */ + @Override + public Enumeration getAttributeNames() { + return super.getAttributeNames(); + } + + /** {@inheritDoc} */ + @Override + public void removeAttribute(String name) { + super.removeAttribute(name); + if (this.manager instanceof SessionManager && ((SessionManager) this.manager).getSaveOnChange()) { + ((SessionManager) this.manager).save(this, true); + } else { + this.dirty = true; + } + } + + /** {@inheritDoc} */ + @Override + public void setPrincipal(Principal principal) { + super.setPrincipal(principal); + this.dirty = true; + } + + /** {@inheritDoc} */ + @Override + public void writeObjectData(ObjectOutputStream out) throws IOException { + super.writeObjectData(out); + out.writeLong(this.getCreationTime()); + } + + /** {@inheritDoc} */ + @Override + public void readObjectData(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readObjectData(in); + this.setCreationTime(in.readLong()); + } +} \ No newline at end of file diff --git a/src/main/java/tomcat/request/session/SessionConstants.java b/src/main/java/tomcat/request/session/SessionConstants.java new file mode 100644 index 0000000..2568009 --- /dev/null +++ b/src/main/java/tomcat/request/session/SessionConstants.java @@ -0,0 +1,18 @@ +package tomcat.request.session; + +/** + * Tomcat clustering with Redis data-cache implementation. + * + * Session constants. + * + * @author Ranjith Manickam + * @since 2.0 + */ +public class SessionConstants { + + public static final byte[] NULL_SESSION = "null".getBytes(); + + public static final String CATALINA_BASE = "catalina.base"; + + public static final String CONF = "conf"; +} \ No newline at end of file diff --git a/src/main/java/tomcat/request/session/SessionContext.java b/src/main/java/tomcat/request/session/SessionContext.java new file mode 100644 index 0000000..f333b0b --- /dev/null +++ b/src/main/java/tomcat/request/session/SessionContext.java @@ -0,0 +1,99 @@ +package tomcat.request.session; + +/** + * Tomcat clustering with Redis data-cache implementation. + * + * Session context uses to manage current session data. + * + * @author Ranjith Manickam + * @since 2.0 + */ +public class SessionContext { + + private String id; + + private Session session; + + private boolean persisted; + + private SessionMetadata metadata; + + /** + * To get session id + * + * @return + */ + public String getId() { + return id; + } + + /** + * To set session id + * + * @param id + */ + public void setId(String id) { + this.id = id; + } + + /** + * To get session + * + * @return + */ + public Session getSession() { + return session; + } + + /** + * To set session + * + * @param session + */ + public void setSession(Session session) { + this.session = session; + } + + /** + * To check session is persisted + * + * @return + */ + public boolean isPersisted() { + return persisted; + } + + /** + * To set session persisted + * + * @param persisted + */ + public void setPersisted(boolean persisted) { + this.persisted = persisted; + } + + /** + * To get session meta-data + * + * @return + */ + public SessionMetadata getMetadata() { + return metadata; + } + + /** + * To set session meta-data + * + * @param metadata + */ + public void setMetadata(SessionMetadata metadata) { + this.metadata = metadata; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return "SessionContext [id=" + id + "]"; + } + +} \ No newline at end of file diff --git a/src/main/java/tomcat/request/session/SessionMetadata.java b/src/main/java/tomcat/request/session/SessionMetadata.java new file mode 100644 index 0000000..a44f4c3 --- /dev/null +++ b/src/main/java/tomcat/request/session/SessionMetadata.java @@ -0,0 +1,78 @@ +package tomcat.request.session; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * Tomcat clustering with Redis data-cache implementation. + * + * This class is uses to store and retrieve the HTTP request session object + * meta-data. + * + * @author Ranjith Manickam + * @since 2.0 + */ +public class SessionMetadata implements Serializable { + + private static final long serialVersionUID = 124438185184833546L; + + private byte[] attributesHash; + + public SessionMetadata() { + this.attributesHash = new byte[0]; + } + + /** + * To get session meta-data hash + * + * @return + */ + public byte[] getAttributesHash() { + return this.attributesHash; + } + + /** + * To set session meta-data hash + * + * @param attributesHash + */ + public void setAttributesHash(byte[] attributesHash) { + this.attributesHash = attributesHash; + } + + /** + * To copy session meta-data + * + * @param metadata + */ + public void copyFieldsFrom(SessionMetadata metadata) { + this.setAttributesHash(metadata.getAttributesHash()); + } + + /** + * To write session meta-data to output stream + * + * @param out + * @throws IOException + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(this.attributesHash.length); + out.write(this.attributesHash); + } + + /** + * To read session meta-data from input stream + * + * @param in + * @throws IOException + * @throws ClassNotFoundException + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + int hashLength = in.readInt(); + byte[] attributesHash = new byte[hashLength]; + in.read(attributesHash, 0, hashLength); + this.attributesHash = attributesHash; + } +} \ No newline at end of file diff --git a/src/main/java/tomcat/request/session/data/cache/DataCache.java b/src/main/java/tomcat/request/session/data/cache/DataCache.java new file mode 100644 index 0000000..e304ebe --- /dev/null +++ b/src/main/java/tomcat/request/session/data/cache/DataCache.java @@ -0,0 +1,58 @@ +package tomcat.request.session.data.cache; + +/** + * Tomcat clustering with Redis data-cache implementation. + * + * API for Data cache. + * + * @author Ranjith Manickam + * @since 2.0 + */ +public interface DataCache { + + /** + * To set value in data-cache + * + * @param key + * @param value + * @return + */ + byte[] set(String key, byte[] value); + + /** + * To set value if key not exists in data-cache + * + * Returns If key exists = 0 else 1 + * + * @param key + * @param value + * @return + */ + Long setnx(String key, byte[] value); + + /** + * To expire the value based on key in data-cache + * + * @param key + * @param seconds + * @return + */ + Long expire(String key, int seconds); + + /** + * To get the value based on key from data-cache + * + * @param key + * @return + */ + byte[] get(String key); + + /** + * To delete the value based on key from data-cache + * + * @param key + * @return + */ + Long delete(String key); + +} \ No newline at end of file diff --git a/src/com/r/tomcat/session/data/cache/constants/RedisConstants.java b/src/main/java/tomcat/request/session/data/cache/impl/RedisConstants.java similarity index 69% rename from src/com/r/tomcat/session/data/cache/constants/RedisConstants.java rename to src/main/java/tomcat/request/session/data/cache/impl/RedisConstants.java index a42aa50..684b8f7 100644 --- a/src/com/r/tomcat/session/data/cache/constants/RedisConstants.java +++ b/src/main/java/tomcat/request/session/data/cache/impl/RedisConstants.java @@ -1,18 +1,22 @@ -package com.r.tomcat.session.data.cache.constants; +package tomcat.request.session.data.cache.impl; /** - * Tomcat clustering implementation + * Tomcat clustering with Redis data-cache implementation. + * + * Redis data-cache constants. * - * Redis data cache constants - * * @author Ranjith Manickam - * @since 1.0 + * @since 2.0 */ -public class RedisConstants -{ - public static final String REDIS_DATA_CACHE_PROPERTIES_FILE = "RedisDataCache.properties"; - - // Redis properties +public class RedisConstants { + + // redis properties file name + public static final String PROPERTIES_FILE = "redis-data-cache.properties"; + + // redis properties + public static final String HOSTS = "redis.hosts"; + public static final String CLUSTER_ENABLED = "redis.cluster.enabled"; + public static final String MAX_ACTIVE = "redis.max.active"; public static final String TEST_ONBORROW = "redis.test.onBorrow"; public static final String TEST_ONRETURN = "redis.test.onReturn"; @@ -21,13 +25,12 @@ public class RedisConstants public static final String TEST_WHILEIDLE = "redis.test.whileIdle"; public static final String TEST_NUMPEREVICTION = "redis.test.numPerEviction"; public static final String TIME_BETWEENEVICTION = "redis.time.betweenEviction"; - public static final String HOSTS = "redis.hosts"; + public static final String PASSWORD = "redis.password"; - public static final String IS_CLUSTER_ENABLED = "redis.cluster.enabled"; public static final String DATABASE = "redis.database"; public static final String TIMEOUT = "redis.timeout"; - // Redis property default values + // redis property default values public static final String DEFAULT_MAX_ACTIVE_VALUE = "10"; public static final String DEFAULT_TEST_ONBORROW_VALUE = "true"; public static final String DEFAULT_TEST_ONRETURN_VALUE = "true"; @@ -36,5 +39,7 @@ public class RedisConstants public static final String DEFAULT_TEST_WHILEIDLE_VALUE = "true"; public static final String DEFAULT_TEST_NUMPEREVICTION_VALUE = "10"; public static final String DEFAULT_TIME_BETWEENEVICTION_VALUE = "60000"; - public static final String DEFAULT_IS_CLUSTER_ENABLED = "false"; + public static final String DEFAULT_CLUSTER_ENABLED = "false"; + + public static final String CONN_FAILED_RETRY_MSG = "Jedis connection failed, retrying..."; } \ No newline at end of file diff --git a/src/main/java/tomcat/request/session/data/cache/impl/RedisDataCache.java b/src/main/java/tomcat/request/session/data/cache/impl/RedisDataCache.java new file mode 100644 index 0000000..3c69193 --- /dev/null +++ b/src/main/java/tomcat/request/session/data/cache/impl/RedisDataCache.java @@ -0,0 +1,492 @@ +package tomcat.request.session.data.cache.impl; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException; +import redis.clients.jedis.exceptions.JedisConnectionException; +import tomcat.request.session.SessionConstants; +import tomcat.request.session.data.cache.DataCache; + +/** + * Tomcat clustering with Redis data-cache implementation. + * + * Redis data-cache implementation to store/retrieve session objects. + * + * @author Ranjith Manickam + * @since 2.0 + */ +public class RedisDataCache implements DataCache { + + private static DataCache dataCache; + + private Log log = LogFactory.getLog(RedisDataCache.class); + + public RedisDataCache() { + initialize(); + } + + /** {@inheritDoc} */ + @Override + public byte[] set(String key, byte[] value) { + return dataCache.set(key, value); + } + + /** {@inheritDoc} */ + @Override + public Long setnx(String key, byte[] value) { + return dataCache.setnx(key, value); + } + + /** {@inheritDoc} */ + @Override + public Long expire(String key, int seconds) { + return dataCache.expire(key, seconds); + } + + /** {@inheritDoc} */ + @Override + public byte[] get(String key) { + return (key != null) ? dataCache.get(key) : null; + } + + /** {@inheritDoc} */ + @Override + public Long delete(String key) { + return dataCache.delete(key); + } + + /** + * To parse data-cache key + * + * @param key + * @return + */ + public static String parseDataCacheKey(String key) { + return key.replaceAll("\\s", "_"); + } + + /** + * To initialize the data-cache + * + * @param properties + * @param filePath + */ + @SuppressWarnings("unchecked") + private void initialize() { + if (dataCache != null) { + return; + } + Properties properties = loadProperties(); + + boolean clusterEnabled = Boolean.valueOf(properties.getProperty(RedisConstants.CLUSTER_ENABLED, RedisConstants.DEFAULT_CLUSTER_ENABLED)); + + String hosts = properties.getProperty(RedisConstants.HOSTS, Protocol.DEFAULT_HOST.concat(":").concat(String.valueOf(Protocol.DEFAULT_PORT))); + Collection extends Serializable> nodes = getJedisNodes(hosts, clusterEnabled); + + String password = properties.getProperty(RedisConstants.PASSWORD); + password = (password != null && !password.isEmpty()) ? password : null; + + int database = Integer.parseInt(properties.getProperty(RedisConstants.DATABASE, String.valueOf(Protocol.DEFAULT_DATABASE))); + + int timeout = Integer.parseInt(properties.getProperty(RedisConstants.TIMEOUT, String.valueOf(Protocol.DEFAULT_TIMEOUT))); + timeout = (timeout < Protocol.DEFAULT_TIMEOUT) ? Protocol.DEFAULT_TIMEOUT : timeout; + + if (clusterEnabled) { + dataCache = new RedisClusterCacheUtil((Set) nodes, timeout, getPoolConfig(properties)); + } else { + dataCache = new RedisCacheUtil(((List) nodes).get(0), + Integer.parseInt(((List) nodes).get(1)), password, database, timeout, getPoolConfig(properties)); + } + } + + /** + * To get jedis pool configuration + * + * @param properties + * @return + */ + private JedisPoolConfig getPoolConfig(Properties properties) { + JedisPoolConfig poolConfig = new JedisPoolConfig(); + int maxActive = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE)); + poolConfig.setMaxTotal(maxActive); + + boolean testOnBorrow = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONBORROW, RedisConstants.DEFAULT_TEST_ONBORROW_VALUE)); + poolConfig.setTestOnBorrow(testOnBorrow); + + boolean testOnReturn = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONRETURN, RedisConstants.DEFAULT_TEST_ONRETURN_VALUE)); + poolConfig.setTestOnReturn(testOnReturn); + + int maxIdle = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE)); + poolConfig.setMaxIdle(maxIdle); + + int minIdle = Integer.parseInt(properties.getProperty(RedisConstants.MIN_IDLE, RedisConstants.DEFAULT_MIN_IDLE_VALUE)); + poolConfig.setMinIdle(minIdle); + + boolean testWhileIdle = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_WHILEIDLE, RedisConstants.DEFAULT_TEST_WHILEIDLE_VALUE)); + poolConfig.setTestWhileIdle(testWhileIdle); + + int testNumPerEviction = Integer.parseInt(properties.getProperty(RedisConstants.TEST_NUMPEREVICTION, RedisConstants.DEFAULT_TEST_NUMPEREVICTION_VALUE)); + poolConfig.setNumTestsPerEvictionRun(testNumPerEviction); + + long timeBetweenEviction = Long.parseLong(properties.getProperty(RedisConstants.TIME_BETWEENEVICTION, RedisConstants.DEFAULT_TIME_BETWEENEVICTION_VALUE)); + poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEviction); + return poolConfig; + } + + /** + * To get jedis nodes + * + * @param hosts + * @param clusterEnabled + * @return + */ + private Collection extends Serializable> getJedisNodes(String hosts, boolean clusterEnabled) { + hosts = hosts.replaceAll("\\s", ""); + String[] hostPorts = hosts.split(","); + + List node = null; + Set nodes = null; + + for (String hostPort : hostPorts) { + String[] hostPortArr = hostPort.split(":"); + + if (clusterEnabled) { + nodes = (nodes == null) ? new HashSet() : nodes; + nodes.add(new HostAndPort(hostPortArr[0], Integer.valueOf(hostPortArr[1]))); + } else { + int port = Integer.valueOf(hostPortArr[1]); + if (!hostPortArr[0].isEmpty() && port > 0) { + node = (node == null) ? new ArrayList() : node; + node.add(hostPortArr[0]); + node.add(String.valueOf(port)); + break; + } + } + } + return clusterEnabled ? nodes : node; + } + + /** + * To load data-cache properties + * + * @param filePath + * @return + */ + private Properties loadProperties() { + Properties properties = new Properties(); + try { + String filePath = System.getProperty(SessionConstants.CATALINA_BASE).concat(File.separator) + .concat(SessionConstants.CONF).concat(File.separator).concat(RedisConstants.PROPERTIES_FILE); + + InputStream resourceStream = null; + try { + resourceStream = (filePath != null && !filePath.isEmpty() && new File(filePath).exists()) + ? new FileInputStream(filePath) : null; + + if (resourceStream == null) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + resourceStream = loader.getResourceAsStream(RedisConstants.PROPERTIES_FILE); + } + properties.load(resourceStream); + } finally { + resourceStream.close(); + } + } catch (IOException ex) { + log.error("Error while loading task scheduler properties", ex); + } + return properties; + } + + /** + * Tomcat clustering with Redis data-cache implementation. + * + * Redis stand-alone mode data-cache implementation. + * + * @author Ranjith Manickam + * @since 2.0 + */ + private class RedisCacheUtil implements DataCache { + + private JedisPool pool; + + private final int numRetries = 3; + + private Log log = LogFactory.getLog(RedisCacheUtil.class); + + public RedisCacheUtil(String host, int port, String password, int database, int timeout, + JedisPoolConfig poolConfig) { + pool = new JedisPool(poolConfig, host, port, timeout, password, database); + } + + /** {@inheritDoc} */ + @Override + public byte[] set(String key, byte[] value) { + int tries = 0; + boolean sucess = false; + String retVal = null; + do { + tries++; + try { + Jedis jedis = pool.getResource(); + retVal = jedis.set(key.getBytes(), value); + jedis.close(); + sucess = true; + } catch (JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) + throw ex; + } + } while (!sucess && tries <= numRetries); + return (retVal != null) ? retVal.getBytes() : null; + } + + /** {@inheritDoc} */ + @Override + public Long setnx(String key, byte[] value) { + int tries = 0; + boolean sucess = false; + Long retVal = null; + do { + tries++; + try { + Jedis jedis = pool.getResource(); + retVal = jedis.setnx(key.getBytes(), value); + jedis.close(); + sucess = true; + } catch (JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) + throw ex; + } + } while (!sucess && tries <= numRetries); + return retVal; + } + + /** {@inheritDoc} */ + @Override + public Long expire(String key, int seconds) { + int tries = 0; + boolean sucess = false; + Long retVal = null; + do { + tries++; + try { + Jedis jedis = pool.getResource(); + retVal = jedis.expire(key, seconds); + jedis.close(); + sucess = true; + } catch (JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) + throw ex; + } + } while (!sucess && tries <= numRetries); + return retVal; + } + + /** {@inheritDoc} */ + @Override + public byte[] get(String key) { + int tries = 0; + boolean sucess = false; + byte[] retVal = null; + do { + tries++; + try { + Jedis jedis = pool.getResource(); + retVal = jedis.get(key.getBytes()); + jedis.close(); + sucess = true; + } catch (JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) + throw ex; + } + } while (!sucess && tries <= numRetries); + return retVal; + } + + /** {@inheritDoc} */ + @Override + public Long delete(String key) { + int tries = 0; + boolean sucess = false; + Long retVal = null; + do { + tries++; + try { + Jedis jedis = pool.getResource(); + retVal = jedis.del(key); + jedis.close(); + sucess = true; + } catch (JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) + throw ex; + } + } while (!sucess && tries <= numRetries); + return retVal; + } + } + + /** + * Tomcat clustering with Redis data-cache implementation. + * + * Redis multiple node cluster data-cache implementation. + * + * @author Ranjith Manickam + * @since 2.0 + */ + private class RedisClusterCacheUtil implements DataCache { + + private JedisCluster cluster; + + private final int numRetries = 30; + + private Log log = LogFactory.getLog(RedisClusterCacheUtil.class); + + public RedisClusterCacheUtil(Set nodes, int timeout, JedisPoolConfig poolConfig) { + cluster = new JedisCluster(nodes, timeout, poolConfig); + } + + /** {@inheritDoc} */ + @Override + public byte[] set(String key, byte[] value) { + int tries = 0; + boolean sucess = false; + String retVal = null; + do { + tries++; + try { + retVal = cluster.set(key.getBytes(), value); + sucess = true; + } catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) { + throw ex; + } + waitforFailover(); + } + } while (!sucess && tries <= numRetries); + return (retVal != null) ? retVal.getBytes() : null; + } + + /** {@inheritDoc} */ + @Override + public Long setnx(String key, byte[] value) { + int tries = 0; + boolean sucess = false; + Long retVal = null; + do { + tries++; + try { + retVal = cluster.setnx(key.getBytes(), value); + sucess = true; + } catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) { + throw ex; + } + waitforFailover(); + } + } while (!sucess && tries <= numRetries); + return retVal; + } + + /** {@inheritDoc} */ + @Override + public Long expire(String key, int seconds) { + int tries = 0; + boolean sucess = false; + Long retVal = null; + do { + tries++; + try { + retVal = cluster.expire(key, seconds); + sucess = true; + } catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) { + throw ex; + } + waitforFailover(); + } + } while (!sucess && tries <= numRetries); + return retVal; + } + + /** {@inheritDoc} */ + @Override + public byte[] get(String key) { + int tries = 0; + boolean sucess = false; + byte[] retVal = null; + do { + tries++; + try { + retVal = cluster.get(key.getBytes()); + sucess = true; + } catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) { + throw ex; + } + waitforFailover(); + } + } while (!sucess && tries <= numRetries); + return retVal; + } + + /** {@inheritDoc} */ + @Override + public Long delete(String key) { + int tries = 0; + boolean sucess = false; + Long retVal = null; + do { + tries++; + try { + retVal = cluster.del(key); + sucess = true; + } catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) { + log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries); + if (tries == numRetries) { + throw ex; + } + waitforFailover(); + } + } while (!sucess && tries <= numRetries); + return retVal; + } + + /** + * To wait for handling redis fail-over + */ + private void waitforFailover() { + try { + Thread.sleep(4000); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } +} \ No newline at end of file diff --git a/src/com/r/tomcat/session/management/RequestSessionHandlerValve.java b/src/main/java/tomcat/request/session/redis/SessionHandlerValve.java similarity index 51% rename from src/com/r/tomcat/session/management/RequestSessionHandlerValve.java rename to src/main/java/tomcat/request/session/redis/SessionHandlerValve.java index 0c90e67..3b9ee7a 100644 --- a/src/com/r/tomcat/session/management/RequestSessionHandlerValve.java +++ b/src/main/java/tomcat/request/session/redis/SessionHandlerValve.java @@ -1,4 +1,4 @@ -package com.r.tomcat.session.management; +package tomcat.request.session.redis; import java.io.IOException; @@ -9,21 +9,28 @@ import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase; /** - * Tomcat clustering implementation + * Tomcat clustering with Redis data-cache implementation. * - * This class is uses to store and retrieve the HTTP request session objects from catalina to data cache + * Valve that implements per-request session persistence. It is intended to be + * used with non-sticky load-balancers. * * @author Ranjith Manickam - * @since 1.0 + * @since 2.0 */ -public class RequestSessionHandlerValve extends ValveBase -{ - private RequestSessionManager manager; +public class SessionHandlerValve extends ValveBase { - public void setRedisSessionManager(RequestSessionManager manager) { + private SessionManager manager; + + /** + * To set session manager + * + * @param manager + */ + public void setSessionManager(SessionManager manager) { this.manager = manager; } + /** {@inheritDoc} */ @Override public void invoke(Request request, Response response) throws IOException, ServletException { try { diff --git a/src/main/java/tomcat/request/session/redis/SessionManager.java b/src/main/java/tomcat/request/session/redis/SessionManager.java new file mode 100644 index 0000000..e1cdc29 --- /dev/null +++ b/src/main/java/tomcat/request/session/redis/SessionManager.java @@ -0,0 +1,389 @@ +package tomcat.request.session.redis; + +import java.io.IOException; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.catalina.session.ManagerBase; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import tomcat.request.session.SerializationUtil; +import tomcat.request.session.Session; +import tomcat.request.session.SessionConstants; +import tomcat.request.session.SessionContext; +import tomcat.request.session.SessionMetadata; +import tomcat.request.session.data.cache.DataCache; +import tomcat.request.session.data.cache.impl.RedisDataCache; + +/** + * Tomcat clustering with Redis data-cache implementation. + * + * Manager that implements per-request session persistence. It is intended to be + * used with non-sticky load-balancers. + * + * @author Ranjith Manickam + * @since 2.0 + */ +public class SessionManager extends ManagerBase implements Lifecycle { + + private DataCache dataCache; + + protected SerializationUtil serializer; + + protected ThreadLocal sessionContext = new ThreadLocal<>(); + + protected SessionHandlerValve handlerValve; + + protected Set sessionPolicy = EnumSet.of(SessionPolicy.DEFAULT); + + private Log log = LogFactory.getLog(SessionManager.class); + + enum SessionPolicy { + DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST; + + static SessionPolicy fromName(String name) { + for (SessionPolicy policy : SessionPolicy.values()) { + if (policy.name().equalsIgnoreCase(name)) { + return policy; + } + } + throw new IllegalArgumentException("Invalid session policy [" + name + "]"); + } + } + + /** + * To get session persist policies + * + * @return + */ + public String getSessionPersistPolicies() { + String policyStr = null; + for (SessionPolicy policy : this.sessionPolicy) { + policyStr = (policyStr == null) ? policy.name() : policyStr.concat(",").concat(policy.name()); + } + return policyStr; + } + + /** + * To set session persist policies + * + * @param policyStr + */ + public void setSessionPersistPolicies(String policyStr) { + Set policySet = EnumSet.of(SessionPolicy.DEFAULT); + String[] policyArray = policyStr.split(","); + + for (String policy : policyArray) { + policySet.add(SessionPolicy.fromName(policy)); + } + this.sessionPolicy = policySet; + } + + /** + * @return + */ + public boolean getSaveOnChange() { + return this.sessionPolicy.contains(SessionPolicy.SAVE_ON_CHANGE); + } + + /** + * @return + */ + public boolean getAlwaysSaveAfterRequest() { + return this.sessionPolicy.contains(SessionPolicy.ALWAYS_SAVE_AFTER_REQUEST); + } + + /** {@inheritDoc} */ + @Override + public void addLifecycleListener(LifecycleListener listener) { + super.addLifecycleListener(listener); + } + + /** {@inheritDoc} */ + @Override + public LifecycleListener[] findLifecycleListeners() { + return super.findLifecycleListeners(); + } + + /** {@inheritDoc} */ + @Override + public void removeLifecycleListener(LifecycleListener listener) { + super.removeLifecycleListener(listener); + } + + /** {@inheritDoc} */ + @Override + protected synchronized void startInternal() throws LifecycleException { + super.startInternal(); + super.setState(LifecycleState.STARTING); + + boolean initializedValve = false; + for (Valve valve : getContext().getPipeline().getValves()) { + if (valve instanceof SessionHandlerValve) { + this.handlerValve = (SessionHandlerValve) valve; + this.handlerValve.setSessionManager(this); + initializedValve = true; + break; + } + } + + if (!initializedValve) + throw new LifecycleException("Session handling valve is not initialized.."); + + initialize(); + + log.info("The sessions will expire after " + (getSessionTimeout()) + " seconds."); + getContext().setDistributable(true); + } + + /** {@inheritDoc} */ + @Override + protected synchronized void stopInternal() throws LifecycleException { + super.setState(LifecycleState.STOPPING); + super.stopInternal(); + } + + /** {@inheritDoc} */ + @Override + public Session createSession(String sessionId) { + if (sessionId != null) { + sessionId = (this.dataCache.setnx(sessionId, SessionConstants.NULL_SESSION) == 0L) ? null : sessionId; + } else { + do { + sessionId = generateSessionId(); + } while (this.dataCache.setnx(sessionId, SessionConstants.NULL_SESSION) == 0L); + } + + Session session = (sessionId != null) ? (Session) createEmptySession() : null; + if (session != null) { + session.setId(sessionId); + session.setNew(true); + session.setValid(true); + session.setCreationTime(System.currentTimeMillis()); + session.setMaxInactiveInterval(getSessionTimeout()); + session.tellNew(); + } + setValues(sessionId, session, false, new SessionMetadata()); + + if (session != null) { + try { + save(session, true); + } catch (Exception ex) { + log.error("Error occured while creating session..", ex); + setValues(null, null); + session = null; + } + } + return session; + } + + /** {@inheritDoc} */ + @Override + public Session createEmptySession() { + return new Session(this); + } + + /** {@inheritDoc} */ + @Override + public void add(org.apache.catalina.Session session) { + save(session, false); + } + + /** {@inheritDoc} */ + @Override + public Session findSession(String sessionId) throws IOException { + Session session = null; + if (sessionId != null && this.sessionContext.get() != null + && sessionId.equals(this.sessionContext.get().getId())) { + session = this.sessionContext.get().getSession(); + } else { + byte[] data = this.dataCache.get(sessionId); + + boolean isPersisted = false; + SessionMetadata metadata = null; + if (data == null) { + session = null; + metadata = null; + sessionId = null; + isPersisted = false; + } else { + if (Arrays.equals(SessionConstants.NULL_SESSION, data)) { + throw new IOException("NULL session data"); + } + try { + metadata = new SessionMetadata(); + Session newSession = (Session) createEmptySession(); + this.serializer.deserializeSessionData(data, newSession, metadata); + + newSession.setId(sessionId); + newSession.access(); + newSession.setNew(false); + newSession.setValid(true); + newSession.resetDirtyTracking(); + newSession.setMaxInactiveInterval(getSessionTimeout()); + + session = newSession; + isPersisted = true; + } catch (Exception ex) { + log.error("Error occured while de-serializing the session object..", ex); + } + } + setValues(sessionId, session, isPersisted, metadata); + } + return session; + } + + /** {@inheritDoc} */ + @Override + public void remove(org.apache.catalina.Session session) { + remove(session, false); + } + + /** {@inheritDoc} */ + @Override + public void remove(org.apache.catalina.Session session, boolean update) { + this.dataCache.expire(session.getId(), 10); + } + + /** {@inheritDoc} */ + @Override + public void load() throws ClassNotFoundException, IOException { + // Auto-generated method stub + } + + /** {@inheritDoc} */ + @Override + public void unload() throws IOException { + // Auto-generated method stub + } + + /** + * To initialize the session manager + */ + private void initialize() { + try { + this.dataCache = new RedisDataCache(); + + this.serializer = new SerializationUtil(); + ClassLoader loader = (getContext() != null && getContext().getLoader() != null) + ? getContext().getLoader().getClassLoader() + : null; + this.serializer.setClassLoader(loader); + } catch (Exception ex) { + log.error("Error occured while initializing the session manager..", ex); + throw ex; + } + } + + /** + * To save session object to data-cache + * + * @param session + * @param forceSave + */ + public void save(org.apache.catalina.Session session, boolean forceSave) { + try { + Boolean isPersisted; + Session newSession = (Session) session; + byte[] hash = (this.sessionContext.get() != null && this.sessionContext.get().getMetadata() != null) + ? this.sessionContext.get().getMetadata().getAttributesHash() + : null; + byte[] currentHash = serializer.getSessionAttributesHashCode(newSession); + + if (forceSave || newSession.isDirty() + || (isPersisted = (this.sessionContext.get() != null) ? this.sessionContext.get().isPersisted() + : null) == null + || !isPersisted || !Arrays.equals(hash, currentHash)) { + + SessionMetadata metadata = new SessionMetadata(); + metadata.setAttributesHash(currentHash); + + this.dataCache.set(newSession.getId(), serializer.serializeSessionData(newSession, metadata)); + newSession.resetDirtyTracking(); + setValues(true, metadata); + } + + int timeout = getSessionTimeout(); + this.dataCache.expire(newSession.getId(), timeout); + log.trace("Session [" + newSession.getId() + "] expire in [" + timeout + "] seconds."); + + } catch (IOException ex) { + log.error("Error occured while saving the session object in data cache..", ex); + } + } + + /** + * To process post request process + * + * @param request + */ + public void afterRequest(Request request) { + Session session = null; + try { + session = (this.sessionContext.get() != null) ? this.sessionContext.get().getSession() : null; + if (session != null) { + if (session.isValid()) + save(session, getAlwaysSaveAfterRequest()); + else + remove(session); + log.trace("Session object " + (session.isValid() ? "saved: " : "removed: ") + session.getId()); + } + } catch (Exception ex) { + log.error("Error occured while processing post request process..", ex); + } finally { + this.sessionContext.remove(); + log.trace("Session removed from ThreadLocal:" + ((session != null) ? session.getIdInternal() : "")); + } + } + + /** + * @return + */ + private int getSessionTimeout() { + int timeout = getContext().getSessionTimeout() * 60; + return (timeout < 1800) ? 1800 : timeout; + } + + /** + * @param sessionId + * @param session + */ + private void setValues(String sessionId, Session session) { + if (this.sessionContext.get() == null) { + this.sessionContext.set(new SessionContext()); + } + this.sessionContext.get().setId(sessionId); + this.sessionContext.get().setSession(session); + } + + /** + * @param isPersisted + * @param metadata + */ + private void setValues(boolean isPersisted, SessionMetadata metadata) { + if (this.sessionContext.get() == null) { + this.sessionContext.set(new SessionContext()); + } + this.sessionContext.get().setMetadata(metadata); + this.sessionContext.get().setPersisted(isPersisted); + } + + /** + * @param sessionId + * @param session + * @param isPersisted + * @param metadata + */ + private void setValues(String sessionId, Session session, boolean isPersisted, SessionMetadata metadata) { + setValues(sessionId, session); + setValues(isPersisted, metadata); + } +} diff --git a/resources/Session objects in Redis.jpg b/src/main/resources/Session-objects-in-Redis.jpg similarity index 100% rename from resources/Session objects in Redis.jpg rename to src/main/resources/Session-objects-in-Redis.jpg diff --git a/resources/Tomcat Clustering - Redis Session Manager.jpg b/src/main/resources/Tomcat-Clustering-Redis-Session-Manager.jpg similarity index 100% rename from resources/Tomcat Clustering - Redis Session Manager.jpg rename to src/main/resources/Tomcat-Clustering-Redis-Session-Manager.jpg diff --git a/src/main/resources/readMe.txt b/src/main/resources/readMe.txt new file mode 100644 index 0000000..983891b --- /dev/null +++ b/src/main/resources/readMe.txt @@ -0,0 +1,41 @@ +/** + * Tomcat clustering with Redis data-cache implementation. + * + * Tomcat clustering with Redis is the plugable one. It uses to store session objects to Redis data cache. + * + * @author Ranjith Manickam + * @since 2.0 + */ + +Supports: + * Apache Tomcat 7 + * Apache Tomcat 8 + +Pre-requisite: + 1. jedis.jar + 2. commons-pool2.jar + 3. commons-logging.jar + +more details.. https://github.com/ran-jit/TomcatClusterRedisSessionManager/wiki + +Steps to be done, + 1. Move the downloaded jars to tomcat/lib directory + * $catalina.home/lib/ + + 2. Add tomcat system property "catalina.base" + * catalina.base="TOMCAT_LOCATION" + + 3. Extract downloaded package (tomcat-cluster-redis-session-manager.zip) to configure Redis credentials in redis-data-cache.properties file and move the file to tomcat/conf directory + * tomcat/conf/redis-data-cache.properties + + 4. Add the below two lines in tomcat/conf/context.xml + + + + 5. Verify the session expiration time (minutes) in tomcat/conf/web.xml + + 60 + + +Note: + * This supports, both redis stand-alone and multiple node cluster based on the redis-data-cache.properties configuration. diff --git a/src/main/resources/redis-data-cache.properties b/src/main/resources/redis-data-cache.properties new file mode 100644 index 0000000..fa923b0 --- /dev/null +++ b/src/main/resources/redis-data-cache.properties @@ -0,0 +1,16 @@ +#-- Redis data-cache configuration + +#- redis hosts ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, .... +redis.hosts=127.0.0.1:6379 + +#- redis password (for stand-alone mode) +#redis.password= + +#- set true to enable redis cluster mode +redis.cluster.enabled=false + +#- redis database (default 0) +#redis.database=0 + +#- redis connection timeout (default 2000) +#redis.timeout=2000 \ No newline at end of file diff --git a/src/test/java/tomcat/request/session/redis/SessionManagerTest.java b/src/test/java/tomcat/request/session/redis/SessionManagerTest.java new file mode 100644 index 0000000..d5f2149 --- /dev/null +++ b/src/test/java/tomcat/request/session/redis/SessionManagerTest.java @@ -0,0 +1,154 @@ +package tomcat.request.session.redis; + +import java.io.IOException; +import java.util.Date; +import java.util.Enumeration; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +@WebServlet("/") +public class SessionManagerTest extends HttpServlet { + + private static final long serialVersionUID = 7464510533820701851L; + + /** + * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse + * response) + */ + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String action = request.getParameter("action"); + action = (action == null) ? "" : action; + String responseData = null; + + switch (action.toUpperCase()) { + case "SET": + responseData = setSessionValues(request.getSession()); + break; + case "GET": + responseData = getSessionValues(request.getSession(), action); + break; + default: + responseData = getActions(request); + break; + } + + sendResponse(response, responseData); + } + + /** + * method to send response + * + * @param response + * @param responseData + * @throws IOException + */ + private void sendResponse(HttpServletResponse response, String responseData) throws IOException { + response.setContentType("text/html"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().println(responseData); + } + + /** + * method to get actions + * + * @param request + * @return + */ + private String getActions(HttpServletRequest request) { + StringBuffer xml = new StringBuffer(); + + xml.append(""); + xml.append(""); + xml.append(""); + xml.append(""); + xml.append("tomcat-cluster-redis-session-manager-test"); + xml.append(""); + xml.append(""); + xml.append("tomcat-cluster-redis-session-manager-test"); + xml.append("actions"); + + String url = request.getRequestURL().toString(); + url = (url.contains("?action=") ? (url.substring(0, url.indexOf("?action="))) : url).concat("?action="); + + xml.append("SET"); + xml.append(""); + xml.append("GET"); + + xml.append(""); + xml.append(""); + + return xml.toString(); + } + + /** + * method to set session values + * + * @param session + * @return + */ + private String setSessionValues(HttpSession session) { + for (int i = 0; i < 10; i++) { + session.setAttribute("test-" + i, "test-" + new Date().getTime()); + } + return getSessionValues(session, "SET"); + } + + /** + * method to get session values + * + * @param session + * @param action + * @return + */ + private String getSessionValues(HttpSession session, String action) { + StringBuffer xml = new StringBuffer(); + xml.append(""); + xml.append(""); + xml.append(""); + xml.append(""); + xml.append("tomcat-cluster-redis-session-manager-test"); + xml.append(""); + xml.append(""); + xml.append("tomcat-cluster-redis-session-manager-test-results"); + xml.append("action: "); + xml.append(action); + xml.append(""); + xml.append(""); + xml.append(""); + xml.append("Key"); + xml.append("Value"); + xml.append(""); + + Enumeration names = session.getAttributeNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + xml.append(""); + xml.append(""); + xml.append(name); + xml.append(""); + xml.append(""); + xml.append(session.getAttribute(name)); + xml.append(""); + xml.append(""); + } + + xml.append(""); + xml.append(""); + xml.append(""); + + return xml.toString(); + } +} \ No newline at end of file