Tomcat 7 redis cluster session manager

Tomcat 7 redis cluster session manager
pull/1/merge
Ranjith 2016-03-19 20:49:10 +05:30
parent 119a084815
commit cee0ead739
19 changed files with 1459 additions and 0 deletions

23
.classpath Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry including="**/*.java" kind="src" path="resources"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="lib" path="lib/catalina.jar"/>
<classpathentry kind="lib" path="lib/servlet-api.jar"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

29
.project Normal file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>TomcatRedisSessionManager</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
</projectDescription>

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# Tomcat-Redis-Cluster-Enabled-Session-Manager
Redis session manager is pluggable one. It uses to store sessions into Redis for easy distribution of HTTP Requests across a cluster of Tomcat servers. Sessions are implemented as as non-sticky i.e, each request is forwarded to any server in round-robin manner.
The HTTP Requests session setAttribute(name, value) method stores the session into Redis (must be Serializable) immediately and the session getAttribute(name) method request directly from Redis. Also, the inactive sessions has been removed based on the session time-out configuration.
It supports, both single redis master and redis cluster based on the RedisDataCache.properties configuration.
Going forward, we no need to enable sticky session (JSESSIONID) in Load balancer.
## Supports:
* Apache Tomcat 7
## 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/TomcatRedisClusterEnabledSessionManager/releases/download/1.0/TomcatClusterEnabledRedisSessionManager-1.0.zip
####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
* **&#60;Valve className="com.r.tomcat.session.management.RequestSessionHandlerValve" &#47;&#62;**
* **&#60;Manager className="com.r.tomcat.session.management.RequestSessionManager" &#47;&#62;**
5. Verify the session expiration time in tomcat/conf/web.xml
* **&#60;session-config&#62;**
* **&#60;session-timeout&#62;60&#60;&#47;session-timeout&#62;**
* **&#60;&#47;session-config&#62;**
###Note:
* The Redis session manager supports, both single redis master and redis cluster based on the redis.properties configuration.

47
pom.xml Normal file
View File

@ -0,0 +1,47 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>TomcatClusterEnabledRedisSessionManager</groupId>
<artifactId>TomcatClusterEnabledRedisSessionManager</artifactId>
<version>1.0</version>
<name>TomcatClusterEnabledRedisSessionManager</name>
<description>Tomcat 7 cluster enabled redis session manager. it supports Redis both single master and cluster</description>
<build>
<sourceDirectory>src</sourceDirectory>
<resources>
<resource>
<directory>resources</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</project>

46
resources/ReadMe.txt Normal file
View File

@ -0,0 +1,46 @@
/**
* 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
*/
Pre-requisite:
--------------
1. jedis-2.8.0.jar (Available in - http://central.maven.org/maven2/redis/clients/jedis/2.8.0/jedis-2.8.0.jar)
2. commons-pool2-2.2.jar (Available in - http://central.maven.org/maven2/org/apache/commons/commons-pool2/2.2/commons-pool2-2.2.jar)
3. commons-logging-1.1.jar (Available in - http://central.maven.org/maven2/commons-logging/commons-logging/1.1.1/commons-logging-1.1.1.jar)
Note: Download all the above three jars and move it into tomcat/lib directory
Steps to be done,
-----------------
1. Add Tomcat system property "catalina.home"
catalina.home="TOMCAT_ROOT_LOCATION"
example: c:/ranjith/apache-tomcat-8.0.32
2. Download the TomcatRedisSessionManager-1.1.jar
Available in -
3. Move the downloaded jar (TomcatRedisSessionManager-1.1.jar) to tomcat/lib directory
$catalina.home/lib/TomcatRedisSessionManager-1.1.jar
example: c:/ranjith/apache-tomcat-8.0.32/lib/TomcatRedisSessionManager-1.1.jar
4. Extract downloaded jar (TomcatRedisSessionManager-1.1.jar) to configure redis data cache credentials in "RedisDataCache.properties" file and move the file to $catalina.home/conf directory
$catalina.home/conf/RedisDataCache.properties
example: c:/ranjith/apache-tomcat-8.0.32/conf/RedisDataCache.properties
5. Add the below two lines in $catalina.home/conf/context.xml
<Valve className="com.r.tomcat.session.management.RequestSessionHandlerValve" />
<Manager className="com.r.tomcat.session.management.RequestSessionManager" />
6. Verify the session expiration time in $catalina.home/conf/web.xml
<session-config>
<session-timeout>EXPIRATION TIME IN MINUTES</session-timeout>
</session-config>
Note:
-----
* This clustering implementation supports, both redis single node and cluster environment based on the RedisDataCache.properties configuration.

View File

@ -0,0 +1,6 @@
# 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

View File

@ -0,0 +1,24 @@
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);
}

View File

@ -0,0 +1,140 @@
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);
}
} 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);
}
} 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);
}
} 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);
}
} 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);
}
} while (!sucess && tries <= numRetries);
}
}

View File

@ -0,0 +1,143 @@
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);
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);
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);
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);
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);
waitforFailover();
}
} while (!sucess && tries <= numRetries);
}
private void waitforFailover() {
try {
Thread.sleep(4000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}

View File

@ -0,0 +1,85 @@
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);
jedisCluster = new JedisCluster(getJedisClusterNodesSet(properties.getProperty(RedisConstants.HOSTS, Protocol.DEFAULT_HOST.concat(":").concat(String.valueOf(Protocol.DEFAULT_PORT)))), poolConfig);
}
/**
* method to get the cluster nodes
*
* @param hosts
* @return
*/
private Set<HostAndPort> getJedisClusterNodesSet(String hosts) {
Set<HostAndPort> nodes = new HashSet<HostAndPort>();
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;
}
}

View File

@ -0,0 +1,84 @@
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);
if (password == null || password == "" || password.isEmpty()) {
pool = new JedisPool(poolConfig, host, port);
} else {
pool = new JedisPool(poolConfig, host, port, Protocol.DEFAULT_TIMEOUT, password);
}
}
public Jedis getJedis() {
return pool.getResource();
}
}

View File

@ -0,0 +1,70 @@
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;
}
}

View File

@ -0,0 +1,38 @@
package com.r.tomcat.session.data.cache.constants;
/**
* Tomcat clustering implementation
*
* Redis data cache constants
*
* @author Ranjith Manickam
* @since 1.0
*/
public class RedisConstants
{
public static final String REDIS_DATA_CACHE_PROPERTIES_FILE = "RedisDataCache.properties";
// Redis properties
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";
public static final String MAX_IDLE = "redis.max.idle";
public static final String MIN_IDLE = "redis.min.idle";
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";
// 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";
public static final String DEFAULT_MAX_IDLE_VALUE = "5";
public static final String DEFAULT_MIN_IDLE_VALUE = "1";
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";
}

View File

@ -0,0 +1,129 @@
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<String, Object> 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<String, Object> 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<String> 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());
}
}

View File

@ -0,0 +1,21 @@
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;
}
}

View File

@ -0,0 +1,35 @@
package com.r.tomcat.session.management;
import java.io.IOException;
import javax.servlet.ServletException;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
/**
* 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 RequestSessionHandlerValve extends ValveBase
{
private RequestSessionManager manager;
public void setRedisSessionManager(RequestSessionManager manager) {
this.manager = manager;
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
try {
getNext().invoke(request, response);
} finally {
manager.afterRequest(request);
}
}
}

View File

@ -0,0 +1,345 @@
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<String> currentSessionId = new ThreadLocal<>();
protected ThreadLocal<CustomRequestSession> currentSession = new ThreadLocal<>();
protected ThreadLocal<Boolean> currentSessionIsPersisted = new ThreadLocal<>();
protected EnumSet<SessionPersistPolicy> sessionPersistPoliciesSet = EnumSet.of(SessionPersistPolicy.DEFAULT);
protected ThreadLocal<SessionSerializationMetadata> 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<SessionPersistPolicy> 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<SessionPersistPolicy> 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 " + getMaxInactiveInterval() + " seconds");
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(getMaxInactiveInterval());
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.deleteKey(session.getId());
}
@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(getMaxInactiveInterval());
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);
}
log.trace("Setting expire timeout on session [" + customSession.getId() + "] to " + getMaxInactiveInterval());
requestSessionCacheUtils.expire(customSession.getId(), getMaxInactiveInterval());
} 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 (getContainer() != null) {
loader = getContainer().getLoader();
}
ClassLoader classLoader = null;
if (loader != null) {
classLoader = loader.getClassLoader();
}
serializer.setClassLoader(classLoader);
}
}

View File

@ -0,0 +1,99 @@
package com.r.tomcat.session.management;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.MessageDigest;
import java.util.Enumeration;
import java.util.HashMap;
import org.apache.catalina.util.CustomObjectInputStream;
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 SessionDataSerializer
{
private final Log log = LogFactory.getLog(SessionDataSerializer.class);
private ClassLoader loader;
public void setClassLoader(ClassLoader loader) {
this.loader = loader;
}
/**
* method to get session attributes hash code
*
* @param session
* @return
* @throws IOException
*/
public byte[] getSessionAttributesHashCode(CustomRequestSession session) throws IOException {
byte[] serialized = null;
HashMap<String, Object> attributes = new HashMap<String, Object>();
for (Enumeration<String> 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));) {
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");
}
return digester.digest(serialized);
}
/**
* method to serialize custom session data
*
* @param session
* @param metadata
* @return
* @throws IOException
*/
public byte[] serializeSessionData(CustomRequestSession session, SessionSerializationMetadata metadata) throws IOException {
byte[] serialized = null;
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) {
oos.writeObject(metadata);
session.writeObjectData(oos);
oos.flush();
serialized = bos.toByteArray();
}
return serialized;
}
/**
* method to deserialize custom session data
*
* @param data
* @param session
* @param metadata
* @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();
metadata.copyFieldsFrom(serializedMetadata);
session.readObjectData(ois);
}
}
}

View File

@ -0,0 +1,49 @@
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;
}
}