Bug fixes and added redis sentinel supportability changes.

pull/27/head
Ranjith Manickam 2018-12-02 23:18:28 +05:30
parent 9069315d96
commit d562a03d63
21 changed files with 1254 additions and 1411 deletions

134
pom.xml
View File

@ -1,68 +1,82 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <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"> 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> <modelVersion>4.0.0</modelVersion>
<groupId>tomcat-cluster-redis-session-manager</groupId> <groupId>tomcat-cluster-redis-session-manager</groupId>
<artifactId>tomcat-cluster-redis-session-manager</artifactId> <artifactId>tomcat-cluster-redis-session-manager</artifactId>
<version>2.0.5</version> <version>2.0.5</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>tomcat-cluster-redis-session-manager</name> <name>tomcat-cluster-redis-session-manager</name>
<url>http://maven.apache.org</url> <url>http://maven.apache.org</url>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.7</java.version> <java.version>1.7</java.version>
</properties>
<dependencies> <jedis.version>2.9.0</jedis.version>
<dependency> <commons-pool2.version>2.4.2</commons-pool2.version>
<groupId>redis.clients</groupId> <commons-logging.version>1.2</commons-logging.version>
<artifactId>jedis</artifactId>
<version>2.9.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>
<!-- For local development --> <maven-compiler.version>3.6.1</maven-compiler.version>
<!-- <dependency> <source-java.version>1.7</source-java.version>
<groupId>apache-tomcat</groupId> <target-java.version>${source-java.version}</target-java.version>
<artifactId>catalina</artifactId>
<version>apache-tomcat-8.5.20</version>
</dependency>
<dependency>
<groupId>apache-tomcat</groupId>
<artifactId>servlet-api</artifactId>
<version>apache-tomcat-8.5.20</version>
</dependency>
<dependency>
<groupId>apache-tomcat</groupId>
<artifactId>tomcat-api</artifactId>
<version>apache-tomcat-8.5.20</version>
</dependency> -->
<!-- For local development end.. -->
</dependencies>
<build> <!-- For local development properties begins.. -->
<plugins> <tomcat-catalina.version>apache-tomcat-8.5.32</tomcat-catalina.version>
<plugin> <tomcat-servlet-api.version>apache-tomcat-8.5.32</tomcat-servlet-api.version>
<groupId>org.apache.maven.plugins</groupId> <tomcat-api.version>apache-tomcat-8.5.32</tomcat-api.version>
<artifactId>maven-compiler-plugin</artifactId> <!-- For local development properties end.. -->
<version>3.6.1</version> </properties>
<configuration>
<source>1.7</source> <dependencies>
<target>1.7</target> <dependency>
</configuration> <groupId>redis.clients</groupId>
</plugin> <artifactId>jedis</artifactId>
</plugins> <version>${jedis.version}</version>
</build> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${commons-pool2.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons-logging.version}</version>
</dependency>
<!-- For local development dependency begins.. -->
<dependency>
<groupId>apache-tomcat</groupId>
<artifactId>catalina</artifactId>
<version>${tomcat-catalina.version}</version>
</dependency>
<dependency>
<groupId>apache-tomcat</groupId>
<artifactId>servlet-api</artifactId>
<version>${tomcat-servlet-api.version}</version>
</dependency>
<dependency>
<groupId>apache-tomcat</groupId>
<artifactId>tomcat-api</artifactId>
<version>${tomcat-api.version}</version>
</dependency>
<!-- For local development dependency end.. -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler.version}</version>
<configuration>
<source>${source-java.version}</source>
<target>${target-java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View File

@ -1,5 +1,7 @@
package tomcat.request.session; package tomcat.request.session;
import org.apache.catalina.util.CustomObjectInputStream;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -11,81 +13,66 @@ import java.security.MessageDigest;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.apache.catalina.util.CustomObjectInputStream;
/** /** author: Ranjith Manickam @ 12 Jul' 2018 */
* Tomcat clustering with Redis data-cache implementation.
*
* Session serialization utility.
*
* @author Ranjith Manickam
* @since 2.0
*/
public class SerializationUtil { public class SerializationUtil {
private ClassLoader loader; private ClassLoader loader;
/** /** To set class loader. */
* To set class loader public void setClassLoader(ClassLoader loader) {
*/ this.loader = loader;
public void setClassLoader(ClassLoader loader) {
this.loader = loader;
}
/**
* To get session attributes hash code
*/
public byte[] getSessionAttributesHashCode(Session session) throws IOException {
byte[] serialized;
Map<String, Object> attributes = new HashMap<>();
for (Enumeration<String> enumerator = session.getAttributeNames();
enumerator.hasMoreElements(); ) {
String key = enumerator.nextElement();
attributes.put(key, session.getAttribute(key));
} }
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); /** To get session attributes hash code. */
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) { public byte[] getSessionAttributesHashCode(Session session) throws IOException {
oos.writeUnshared(attributes); byte[] serialized;
oos.flush(); Map<String, Object> attributes = new HashMap<>();
serialized = bos.toByteArray();
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;
try {
digester = MessageDigest.getInstance("MD5");
} catch (Exception ex) {
throw new RuntimeException("Unable to get MessageDigest instance for MD5", ex);
}
return digester.digest(serialized);
} }
MessageDigest digester; /** To serialize session object. */
try { public byte[] serializeSessionData(Session session, SessionMetadata metadata) throws IOException {
digester = MessageDigest.getInstance("MD5"); byte[] serialized;
} catch (Exception ex) { try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
throw new RuntimeException("Unable to get MessageDigest instance for MD5", ex); ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) {
oos.writeObject(metadata);
session.writeObjectData(oos);
oos.flush();
serialized = bos.toByteArray();
}
return serialized;
} }
return digester.digest(serialized);
}
/** /** To de-serialize session object. */
* To serialize session object public void deserializeSessionData(byte[] data, Session session, SessionMetadata metadata)
*/ throws IOException, ClassNotFoundException {
public byte[] serializeSessionData(Session session, SessionMetadata metadata) throws IOException { try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data));
byte[] serialized; ObjectInputStream ois = new CustomObjectInputStream(bis, this.loader)) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); SessionMetadata serializedMetadata = (SessionMetadata) ois.readObject();
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) { metadata.copyFieldsFrom(serializedMetadata);
oos.writeObject(metadata); session.readObjectData(ois);
session.writeObjectData(oos); }
oos.flush();
serialized = bos.toByteArray();
} }
return serialized;
}
/** }
* To de-serialize session object
*/
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);
}
}
}

View File

@ -13,147 +13,126 @@ import org.apache.catalina.session.StandardSession;
import tomcat.request.session.redis.SessionManager; import tomcat.request.session.redis.SessionManager;
/** /** author: Ranjith Manickam @ 12 Jul' 2018 */
* 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 { public class Session extends StandardSession {
private static final long serialVersionUID = -6056744304016869278L; private static final long serialVersionUID = -6056744304016869278L;
protected Boolean dirty; protected Boolean dirty;
protected Map<String, Object> changedAttributes;
protected Map<String, Object> changedAttributes; protected static Boolean manualDirtyTrackingSupportEnabled = false;
protected static String manualDirtyTrackingAttributeKey = "__changed__";
protected static Boolean manualDirtyTrackingSupportEnabled = false; public Session(Manager manager) {
super(manager);
protected static String manualDirtyTrackingAttributeKey = "__changed__"; resetDirtyTracking();
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<String, Object> 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); /** To reset dirty tracking. */
super.setAttribute(key, value); public void resetDirtyTracking() {
this.changedAttributes = new HashMap<>();
if ((value != null || oldValue != null) this.dirty = false;
&& (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);
}
} }
}
/** /** To set manual dirty tracking support enabled. */
* {@inheritDoc} public static void setManualDirtyTrackingSupportEnabled(boolean enabled) {
*/ manualDirtyTrackingSupportEnabled = enabled;
@Override
public Object getAttribute(String name) {
return super.getAttribute(name);
}
/**
* {@inheritDoc}
*/
@Override
public Enumeration<String> 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;
} }
}
/** /** To set manual dirty tracking attribute key. */
* {@inheritDoc} public static void setManualDirtyTrackingAttributeKey(String key) {
*/ manualDirtyTrackingAttributeKey = key;
@Override }
public void setPrincipal(Principal principal) {
super.setPrincipal(principal);
this.dirty = true;
}
/** /** Returns dirty check. */
* {@inheritDoc} public Boolean isDirty() {
*/ return this.dirty || !this.changedAttributes.isEmpty();
@Override }
public void writeObjectData(ObjectOutputStream out) throws IOException {
super.writeObjectData(out);
out.writeLong(this.getCreationTime());
}
/** /** To get changed attributes. */
* {@inheritDoc} public Map<String, Object> getChangedAttributes() {
*/ return this.changedAttributes;
@Override }
public void readObjectData(ObjectInputStream in) throws IOException, ClassNotFoundException {
super.readObjectData(in);
this.setCreationTime(in.readLong());
}
/** /** {@inheritDoc} */
* {@inheritDoc} @Override
*/ public void setId(String id) {
@Override this.id = id;
public void invalidate() { }
super.invalidate();
} /** {@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<String> 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());
}
/** {@inheritDoc} */
@Override
public void invalidate() {
super.invalidate();
}
}

View File

@ -1,18 +1,22 @@
package tomcat.request.session; package tomcat.request.session;
/** /** author: Ranjith Manickam @ 12 Jul' 2018 */
* Tomcat clustering with Redis data-cache implementation.
*
* Session constants.
*
* @author Ranjith Manickam
* @since 2.0
*/
public interface SessionConstants { public interface SessionConstants {
byte[] NULL_SESSION = "null".getBytes();
String CATALINA_BASE = "catalina.base";
String CONF = "conf";
byte[] NULL_SESSION = "null".getBytes(); enum SessionPolicy {
DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST;
String CATALINA_BASE = "catalina.base"; public static SessionPolicy fromName(String name) {
for (SessionPolicy policy : SessionPolicy.values()) {
if (policy.name().equalsIgnoreCase(name)) {
return policy;
}
}
throw new IllegalArgumentException("Invalid session policy [" + name + "]");
}
}
String CONF = "conf"; }
}

View File

@ -1,85 +1,57 @@
package tomcat.request.session; package tomcat.request.session;
/** /** author: Ranjith Manickam @ 12 Jul' 2018 */
* Tomcat clustering with Redis data-cache implementation.
*
* Session context uses to manage current session data.
*
* @author Ranjith Manickam
* @since 2.0
*/
public class SessionContext { public class SessionContext {
private String id; private String id;
private Session session;
private boolean persisted;
private SessionMetadata metadata;
private Session session; /** To get session id. */
public String getId() {
return id;
}
private boolean persisted; /** To set session id. */
public void setId(String id) {
this.id = id;
}
private SessionMetadata metadata; /** To get session. */
public Session getSession() {
return session;
}
/** /** To set session. */
* To get session id public void setSession(Session session) {
*/ this.session = session;
public String getId() { }
return id;
}
/** /** To check session is persisted. */
* To set session id public boolean isPersisted() {
*/ return persisted;
public void setId(String id) { }
this.id = id;
}
/** /** To set session persisted. */
* To get session public void setPersisted(boolean persisted) {
*/ this.persisted = persisted;
public Session getSession() { }
return session;
}
/** /** To get session meta-data. */
* To set session public SessionMetadata getMetadata() {
*/ return metadata;
public void setSession(Session session) { }
this.session = session;
}
/** /** To set session meta-data. */
* To check session is persisted public void setMetadata(SessionMetadata metadata) {
*/ this.metadata = metadata;
public boolean isPersisted() { }
return persisted;
}
/** /** {@inheritDoc} */
* To set session persisted @Override
*/ public String toString() {
public void setPersisted(boolean persisted) { return "SessionContext [id=" + id + "]";
this.persisted = persisted; }
}
/** }
* To get session meta-data
*/
public SessionMetadata getMetadata() {
return metadata;
}
/**
* To set session meta-data
*/
public void setMetadata(SessionMetadata metadata) {
this.metadata = metadata;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "SessionContext [id=" + id + "]";
}
}

View File

@ -5,60 +5,44 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable; import java.io.Serializable;
/** /** author: Ranjith Manickam @ 12 Jul' 2018 */
* 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 { public class SessionMetadata implements Serializable {
private static final long serialVersionUID = 124438185184833546L; private static final long serialVersionUID = 124438185184833546L;
private byte[] attributesHash; private byte[] attributesHash;
public SessionMetadata() { public SessionMetadata() {
this.attributesHash = new byte[0]; this.attributesHash = new byte[0];
} }
/** /** To get session meta-data hash. */
* To get session meta-data hash public byte[] getAttributesHash() {
*/ return this.attributesHash;
public byte[] getAttributesHash() { }
return this.attributesHash;
}
/** /** To set session meta-data hash. */
* To set session meta-data hash public void setAttributesHash(byte[] attributesHash) {
*/ this.attributesHash = attributesHash;
public void setAttributesHash(byte[] attributesHash) { }
this.attributesHash = attributesHash;
}
/** /** To copy session meta-data. */
* To copy session meta-data public void copyFieldsFrom(SessionMetadata metadata) {
*/ this.setAttributesHash(metadata.getAttributesHash());
public void copyFieldsFrom(SessionMetadata metadata) { }
this.setAttributesHash(metadata.getAttributesHash());
}
/** /** To write session meta-data to output stream. */
* To write session meta-data to output stream private void writeObject(ObjectOutputStream out) throws IOException {
*/ out.writeInt(this.attributesHash.length);
private void writeObject(ObjectOutputStream out) throws IOException { out.write(this.attributesHash);
out.writeInt(this.attributesHash.length); }
out.write(this.attributesHash);
}
/** /** To read session meta-data from input stream. */
* To read session meta-data from input stream private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
*/ int hashLength = in.readInt();
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { byte[] attributesHash = new byte[hashLength];
int hashLength = in.readInt(); in.read(attributesHash, 0, hashLength);
byte[] attributesHash = new byte[hashLength]; this.attributesHash = attributesHash;
in.read(attributesHash, 0, hashLength); }
this.attributesHash = attributesHash;
} }
}

View File

@ -1,40 +1,49 @@
package tomcat.request.session.data.cache; package tomcat.request.session.data.cache;
/** /** author: Ranjith Manickam @ 12 Jul' 2018 */
* Tomcat clustering with Redis data-cache implementation.
*
* API for Data cache.
*
* @author Ranjith Manickam
* @since 2.0
*/
public interface DataCache { public interface DataCache {
/** /**
* To set value in data-cache * Set value in data-cache.
*/ *
byte[] set(String key, byte[] value); * @param key - key with which the specified value is to be associated.
* @param value - value to be associated with the specified key.
* @return - Returns the value.
*/
byte[] set(String key, byte[] value);
/** /**
* To set value if key not exists in data-cache * Set value if key not exists in data-cache.
* *
* Returns If key exists = 0 else 1 * @param key - key with which the specified value is to be associated.
*/ * @param value - value to be associated with the specified key.
Long setnx(String key, byte[] value); * @return - Returns '0' if key already exists else '1'.
*/
Long setnx(String key, byte[] value);
/** /**
* To expire the value based on key in data-cache * Set expiry in data-cache.
*/ *
Long expire(String key, int seconds); * @param key - key with which the specified value is to be associated.
* @param seconds - expiration time in seconds.
* @return - Returns the expiration time in seconds.
*/
Long expire(String key, int seconds);
/** /**
* To get the value based on key from data-cache * Get value from data-cache.
*/ *
byte[] get(String key); * @param key - key with which the specified value is to be associated.
* @return - Returns the value.
*/
byte[] get(String key);
/** /**
* To delete the value based on key from data-cache * Delete value from data-cache.
*/ *
Long delete(String key); * @param key - key with which the specified value is to be associated.
* @return - Returns the number of keys that were removed.
*/
Long delete(String key);
} }

View File

@ -1,147 +0,0 @@
package tomcat.request.session.data.cache.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.exceptions.JedisConnectionException;
import tomcat.request.session.data.cache.DataCache;
/**
* Tomcat clustering with Redis data-cache implementation.
*
* Redis stand-alone mode data-cache implementation.
*
* @author Ranjith Manickam
* @since 2.0
*/
class RedisCacheUtil implements DataCache {
private JedisPool pool;
private static final int NUM_RETRIES = 3;
private Log log = LogFactory.getLog(RedisCacheUtil.class);
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);
sucess = true;
} catch (JedisConnectionException ex) {
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
if (tries == NUM_RETRIES) {
throw ex;
}
}
} while (!sucess && tries <= NUM_RETRIES);
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);
sucess = true;
} catch (JedisConnectionException ex) {
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
if (tries == NUM_RETRIES) {
throw ex;
}
}
} while (!sucess && tries <= NUM_RETRIES);
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);
sucess = true;
} catch (JedisConnectionException ex) {
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
if (tries == NUM_RETRIES) {
throw ex;
}
}
} while (!sucess && tries <= NUM_RETRIES);
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());
sucess = true;
} catch (JedisConnectionException ex) {
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
if (tries == NUM_RETRIES) {
throw ex;
}
}
} while (!sucess && tries <= NUM_RETRIES);
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);
sucess = true;
} catch (JedisConnectionException ex) {
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
if (tries == NUM_RETRIES) {
throw ex;
}
}
} while (!sucess && tries <= NUM_RETRIES);
return retVal;
}
}

View File

@ -1,169 +0,0 @@
package tomcat.request.session.data.cache.impl;
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.JedisCluster;
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.data.cache.DataCache;
/**
* Tomcat clustering with Redis data-cache implementation.
*
* Redis multiple node cluster data-cache implementation.
*
* @author Ranjith Manickam
* @since 2.0
*/
class RedisClusterCacheUtil implements DataCache {
private JedisCluster cluster;
private static final int NUM_RETRIES = 30;
private static final int DEFAULT_MAX_REDIRECTIONS = 5;
private Log log = LogFactory.getLog(RedisClusterCacheUtil.class);
RedisClusterCacheUtil(Set<HostAndPort> nodes, String password, int timeout,
JedisPoolConfig poolConfig) {
cluster = new JedisCluster(nodes, timeout, Protocol.DEFAULT_TIMEOUT, DEFAULT_MAX_REDIRECTIONS,
password, 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 == NUM_RETRIES) {
throw ex;
}
waitforFailover();
}
} while (!sucess && tries <= NUM_RETRIES);
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 == NUM_RETRIES) {
throw ex;
}
waitforFailover();
}
} while (!sucess && tries <= NUM_RETRIES);
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 == NUM_RETRIES) {
throw ex;
}
waitforFailover();
}
} while (!sucess && tries <= NUM_RETRIES);
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 == NUM_RETRIES) {
throw ex;
}
waitforFailover();
}
} while (!sucess && tries <= NUM_RETRIES);
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 == NUM_RETRIES) {
throw ex;
}
waitforFailover();
}
} while (!sucess && tries <= NUM_RETRIES);
return retVal;
}
/**
* To wait for handling redis fail-over
*/
private void waitforFailover() {
try {
Thread.sleep(4000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}

View File

@ -1,45 +0,0 @@
package tomcat.request.session.data.cache.impl;
/**
* Tomcat clustering with Redis data-cache implementation.
*
* Redis data-cache constants.
*
* @author Ranjith Manickam
* @since 2.0
*/
interface RedisConstants {
// redis properties file name
String PROPERTIES_FILE = "redis-data-cache.properties";
// redis properties
String HOSTS = "redis.hosts";
String CLUSTER_ENABLED = "redis.cluster.enabled";
String MAX_ACTIVE = "redis.max.active";
String TEST_ONBORROW = "redis.test.onBorrow";
String TEST_ONRETURN = "redis.test.onReturn";
String MAX_IDLE = "redis.max.idle";
String MIN_IDLE = "redis.min.idle";
String TEST_WHILEIDLE = "redis.test.whileIdle";
String TEST_NUMPEREVICTION = "redis.test.numPerEviction";
String TIME_BETWEENEVICTION = "redis.time.betweenEviction";
String PASSWORD = "redis.password";
String DATABASE = "redis.database";
String TIMEOUT = "redis.timeout";
// redis property default values
String DEFAULT_MAX_ACTIVE_VALUE = "10";
String DEFAULT_TEST_ONBORROW_VALUE = "true";
String DEFAULT_TEST_ONRETURN_VALUE = "true";
String DEFAULT_MAX_IDLE_VALUE = "5";
String DEFAULT_MIN_IDLE_VALUE = "1";
String DEFAULT_TEST_WHILEIDLE_VALUE = "true";
String DEFAULT_TEST_NUMPEREVICTION_VALUE = "10";
String DEFAULT_TIME_BETWEENEVICTION_VALUE = "60000";
String DEFAULT_CLUSTER_ENABLED = "false";
String CONN_FAILED_RETRY_MSG = "Jedis connection failed, retrying...";
}

View File

@ -1,226 +0,0 @@
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.JedisPoolConfig;
import redis.clients.jedis.Protocol;
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
*/
public static String parseDataCacheKey(String key) {
return key.replaceAll("\\s", "_");
}
/**
* To initialize the data-cache
*/
@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<HostAndPort>) nodes, password, timeout,
getPoolConfig(properties));
} else {
dataCache = new RedisCacheUtil(((List<String>) nodes).get(0),
Integer.parseInt(((List<String>) nodes).get(1)), password, database, timeout,
getPoolConfig(properties));
}
}
/**
* To get jedis pool configuration
*/
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
*/
private Collection<? extends Serializable> getJedisNodes(String hosts, boolean clusterEnabled) {
hosts = hosts.replaceAll("\\s", "");
String[] hostPorts = hosts.split(",");
List<String> node = null;
Set<HostAndPort> nodes = null;
for (String hostPort : hostPorts) {
String[] hostPortArr = hostPort.split(":");
if (clusterEnabled) {
nodes = (nodes == null) ? new HashSet<HostAndPort>() : 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<String>() : node;
node.add(hostPortArr[0]);
node.add(String.valueOf(port));
break;
}
}
}
return clusterEnabled ? nodes : node;
}
/**
* To load data-cache properties
*/
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 {
if (resourceStream != null) {
resourceStream.close();
}
}
} catch (IOException ex) {
log.error("Error while loading task scheduler properties", ex);
}
return properties;
}
}

View File

@ -0,0 +1,132 @@
package tomcat.request.session.data.cache.impl.redis;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.util.Pool;
import tomcat.request.session.data.cache.DataCache;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
public abstract class AbstractRedisUtil implements DataCache {
private static final int NUM_RETRIES = 3;
private static final Log LOGGER = LogFactory.getLog(RedisUtil.class);
private final Pool<Jedis> pool;
private final long failiureWaitTime;
AbstractRedisUtil(Pool<Jedis> pool, long failiureWaitTime) {
this.pool = pool;
this.failiureWaitTime = failiureWaitTime;
}
/** {@inheritDoc} */
@Override
public byte[] set(String key, byte[] value) {
int tries = 0;
boolean retry = true;
String retVal = null;
do {
tries++;
try (Jedis jedis = this.pool.getResource()) {
retVal = jedis.set(key.getBytes(), value);
retry = false;
} catch (JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return (retVal != null) ? retVal.getBytes() : null;
}
/** {@inheritDoc} */
@Override
public Long setnx(String key, byte[] value) {
int tries = 0;
boolean retry = true;
Long retVal = null;
do {
tries++;
try (Jedis jedis = this.pool.getResource()) {
retVal = jedis.setnx(key.getBytes(), value);
retry = false;
} catch (JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/** {@inheritDoc} */
@Override
public Long expire(String key, int seconds) {
int tries = 0;
boolean retry = true;
Long retVal = null;
do {
tries++;
try (Jedis jedis = this.pool.getResource()) {
retVal = jedis.expire(key, seconds);
retry = false;
} catch (JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/** {@inheritDoc} */
@Override
public byte[] get(String key) {
int tries = 0;
boolean retry = true;
byte[] retVal = null;
do {
tries++;
try (Jedis jedis = this.pool.getResource()) {
retVal = jedis.get(key.getBytes());
retry = false;
} catch (JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/** {@inheritDoc} */
@Override
public Long delete(String key) {
int tries = 0;
boolean retry = true;
Long retVal = null;
do {
tries++;
try (Jedis jedis = this.pool.getResource()) {
retVal = jedis.del(key);
retry = false;
} catch (JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/**
* To handle jedis exception.
*
* @param tries - exception occurred in tries.
* @param ex - jedis exception.
*/
void handleException(int tries, RuntimeException ex) {
LOGGER.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
if (tries == NUM_RETRIES) {
throw ex;
}
try {
Thread.sleep(this.failiureWaitTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

View File

@ -0,0 +1,206 @@
package tomcat.request.session.data.cache.impl.redis;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
import tomcat.request.session.SessionConstants;
import tomcat.request.session.data.cache.DataCache;
import tomcat.request.session.data.cache.impl.redis.RedisConstants.RedisConfigType;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
public class RedisCache implements DataCache {
private static DataCache dataCache;
private static final Log LOGGER = LogFactory.getLog(RedisCache.class);
public RedisCache() {
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 dataCache.get(key);
}
/** {@inheritDoc} */
@Override
public Long delete(String key) {
return dataCache.delete(key);
}
/** To initialize the redis data cache. */
private void initialize() {
if (dataCache != null) {
return;
}
Properties properties = getProperties();
RedisConfigType configType;
if (Boolean.valueOf(properties.getProperty(RedisConstants.CLUSTER_ENABLED))) {
configType = RedisConfigType.CLUSTER;
} else if (Boolean.valueOf(properties.getProperty(RedisConstants.SENTINEL_ENABLED))) {
configType = RedisConfigType.SENTINEL;
} else {
configType = RedisConfigType.DEFAULT;
}
String hosts = properties.getProperty(RedisConstants.HOSTS, Protocol.DEFAULT_HOST.concat(":").concat(String.valueOf(Protocol.DEFAULT_PORT)));
Collection<?> nodes = getJedisNodes(hosts, configType);
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;
JedisPoolConfig poolConfig = getPoolConfig(properties);
switch (configType) {
case CLUSTER:
dataCache = new RedisClusterUtil((Set<HostAndPort>) nodes, password, timeout, poolConfig);
break;
case SENTINEL:
String masterName = String.valueOf(properties.getProperty(RedisConstants.SENTINEL_MASTER, RedisConstants.DEFAULT_SENTINEL_MASTER));
dataCache = new RedisSentinelUtil((Set<String>) nodes, masterName, password, database, timeout, poolConfig);
break;
default:
dataCache = new RedisUtil(((List<String>) nodes).get(0), Integer.parseInt(((List<String>) nodes).get(1)), password, database, timeout, poolConfig);
break;
}
}
/**
* To get redis pool config.
*
* @param properties - Redis data cache properties.
* @return - Returns the redis pool config.
*/
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 redis data cache nodes.
*
* @param hosts - redis server hosts.
* @param configType - redis data config type.
* @return - Returns the redis nodes.
*/
private Collection<?> getJedisNodes(String hosts, RedisConfigType configType) {
hosts = hosts.replaceAll("\\s", "");
String[] hostPorts = hosts.split(",");
Set<Object> nodes = null;
for (String hostPort : hostPorts) {
String[] hostPortArr = hostPort.split(":");
switch (configType) {
case CLUSTER:
nodes = (nodes == null) ? new HashSet<>() : nodes;
nodes.add(new HostAndPort(hostPortArr[0], Integer.valueOf(hostPortArr[1])));
break;
case SENTINEL:
nodes = (nodes == null) ? new HashSet<>() : nodes;
nodes.add(new HostAndPort(hostPortArr[0], Integer.valueOf(hostPortArr[1])).toString());
break;
default:
int port = Integer.valueOf(hostPortArr[1]);
if (!hostPortArr[0].isEmpty() && port > 0) {
List<String> node = new ArrayList<>();
node.add(hostPortArr[0]);
node.add(String.valueOf(port));
return node;
}
}
}
return nodes;
}
/** To get redis data cache properties. */
private Properties getProperties() {
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.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 {
if (resourceStream != null) {
resourceStream.close();
}
}
} catch (IOException ex) {
LOGGER.error("Error while loading task scheduler properties", ex);
}
return properties;
}
}

View File

@ -0,0 +1,120 @@
package tomcat.request.session.data.cache.impl.redis;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.util.Set;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
public class RedisClusterUtil extends AbstractRedisUtil {
private final JedisCluster cluster;
private static final int NUM_RETRIES = 30;
private static final int DEFAULT_MAX_REDIRECTIONS = 5;
private static final long FAILIURE_WAIT_TIME = 4000L;
RedisClusterUtil(Set<HostAndPort> nodes,
String password,
int timeout,
JedisPoolConfig poolConfig) {
super(null, FAILIURE_WAIT_TIME);
this.cluster = new JedisCluster(nodes, timeout, Protocol.DEFAULT_TIMEOUT, DEFAULT_MAX_REDIRECTIONS,
password, poolConfig);
}
/** {@inheritDoc} */
@Override
public byte[] set(String key, byte[] value) {
int tries = 0;
boolean retry = true;
String retVal = null;
do {
tries++;
try {
retVal = this.cluster.set(key.getBytes(), value);
retry = false;
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return (retVal != null) ? retVal.getBytes() : null;
}
/** {@inheritDoc} */
@Override
public Long setnx(String key, byte[] value) {
int tries = 0;
boolean retry = true;
Long retVal = null;
do {
tries++;
try {
retVal = this.cluster.setnx(key.getBytes(), value);
retry = false;
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/** {@inheritDoc} */
@Override
public Long expire(String key, int seconds) {
int tries = 0;
boolean retry = true;
Long retVal = null;
do {
tries++;
try {
retVal = this.cluster.expire(key, seconds);
retry = false;
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/** {@inheritDoc} */
@Override
public byte[] get(String key) {
int tries = 0;
boolean retry = true;
byte[] retVal = null;
do {
tries++;
try {
retVal = this.cluster.get(key.getBytes());
retry = false;
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/** {@inheritDoc} */
@Override
public Long delete(String key) {
int tries = 0;
boolean retry = true;
Long retVal = null;
do {
tries++;
try {
retVal = this.cluster.del(key);
retry = false;
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
}

View File

@ -0,0 +1,48 @@
package tomcat.request.session.data.cache.impl.redis;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
interface RedisConstants {
// redis properties file name
String PROPERTIES_FILE = "redis-data-cache.properties";
// redis properties
String HOSTS = "redis.hosts";
String CLUSTER_ENABLED = "redis.cluster.enabled";
String SENTINEL_ENABLED = "redis.sentinel.enabled";
String MAX_ACTIVE = "redis.max.active";
String TEST_ONBORROW = "redis.test.onBorrow";
String TEST_ONRETURN = "redis.test.onReturn";
String MAX_IDLE = "redis.max.idle";
String MIN_IDLE = "redis.min.idle";
String TEST_WHILEIDLE = "redis.test.whileIdle";
String TEST_NUMPEREVICTION = "redis.test.numPerEviction";
String TIME_BETWEENEVICTION = "redis.time.betweenEviction";
String PASSWORD = "redis.password";
String DATABASE = "redis.database";
String TIMEOUT = "redis.timeout";
String SENTINEL_MASTER = "redis.sentinel.master";
String DEFAULT_SENTINEL_MASTER = "mymaster";
// redis property default values
String DEFAULT_MAX_ACTIVE_VALUE = "10";
String DEFAULT_TEST_ONBORROW_VALUE = "true";
String DEFAULT_TEST_ONRETURN_VALUE = "true";
String DEFAULT_MAX_IDLE_VALUE = "5";
String DEFAULT_MIN_IDLE_VALUE = "1";
String DEFAULT_TEST_WHILEIDLE_VALUE = "true";
String DEFAULT_TEST_NUMPEREVICTION_VALUE = "10";
String DEFAULT_TIME_BETWEENEVICTION_VALUE = "60000";
String CONN_FAILED_RETRY_MSG = "Jedis connection failed, retrying...";
enum RedisConfigType {
DEFAULT,
SENTINEL,
CLUSTER
}
}

View File

@ -0,0 +1,22 @@
package tomcat.request.session.data.cache.impl.redis;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import java.util.Set;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
class RedisSentinelUtil extends AbstractRedisUtil {
private static final long FAILIURE_WAIT_TIME = 2000L;
RedisSentinelUtil(Set<String> nodes,
String masterName,
String password,
int database,
int timeout,
JedisPoolConfig poolConfig) {
super(new JedisSentinelPool(masterName, nodes, poolConfig, timeout, password, database), FAILIURE_WAIT_TIME);
}
}

View File

@ -0,0 +1,20 @@
package tomcat.request.session.data.cache.impl.redis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
class RedisUtil extends AbstractRedisUtil {
private static final long FAILIURE_WAIT_TIME = 2000L;
RedisUtil(String host,
int port,
String password,
int database,
int timeout,
JedisPoolConfig poolConfig) {
super(new JedisPool(poolConfig, host, port, timeout, password, database), FAILIURE_WAIT_TIME);
}
}

View File

@ -8,35 +8,24 @@ import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response; import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase; import org.apache.catalina.valves.ValveBase;
/** /** author: Ranjith Manickam @ 12 Jul' 2018 */
* Tomcat clustering with Redis data-cache implementation.
*
* Valve 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 SessionHandlerValve extends ValveBase { public class SessionHandlerValve extends ValveBase {
private SessionManager manager; private SessionManager manager;
/** /** To set session manager */
* To set session manager public void setSessionManager(SessionManager manager) {
*/ this.manager = manager;
public void setSessionManager(SessionManager manager) {
this.manager = manager;
}
/**
* {@inheritDoc}
*/
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
try {
getNext().invoke(request, response);
} finally {
manager.afterRequest(request);
} }
}
} /** {@inheritDoc} */
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
try {
getNext().invoke(request, response);
} finally {
manager.afterRequest(request);
}
}
}

View File

@ -1,11 +1,5 @@
package tomcat.request.session.redis; package tomcat.request.session.redis;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Set;
import org.apache.catalina.Context; import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle; import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleException;
@ -16,396 +10,343 @@ import org.apache.catalina.connector.Request;
import org.apache.catalina.session.ManagerBase; import org.apache.catalina.session.ManagerBase;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import tomcat.request.session.SerializationUtil; import tomcat.request.session.SerializationUtil;
import tomcat.request.session.Session; import tomcat.request.session.Session;
import tomcat.request.session.SessionConstants; import tomcat.request.session.SessionConstants;
import tomcat.request.session.SessionConstants.SessionPolicy;
import tomcat.request.session.SessionContext; import tomcat.request.session.SessionContext;
import tomcat.request.session.SessionMetadata; import tomcat.request.session.SessionMetadata;
import tomcat.request.session.data.cache.DataCache; import tomcat.request.session.data.cache.DataCache;
import tomcat.request.session.data.cache.impl.RedisDataCache; import tomcat.request.session.data.cache.impl.redis.RedisCache;
/** import java.io.IOException;
* Tomcat clustering with Redis data-cache implementation. import java.lang.reflect.Method;
* import java.util.Arrays;
* Manager that implements per-request session persistence. It is intended to be used with import java.util.EnumSet;
* non-sticky load-balancers. import java.util.Set;
*
* @author Ranjith Manickam /** author: Ranjith Manickam @ 12 Jul' 2018 */
* @since 2.0
*/
public class SessionManager extends ManagerBase implements Lifecycle { public class SessionManager extends ManagerBase implements Lifecycle {
private DataCache dataCache; private DataCache dataCache;
protected SerializationUtil serializer;
protected SessionHandlerValve handlerValve;
protected SerializationUtil serializer; protected ThreadLocal<SessionContext> sessionContext = new ThreadLocal<>();
protected Set<SessionPolicy> sessionPolicy = EnumSet.of(SessionPolicy.DEFAULT);
protected ThreadLocal<SessionContext> sessionContext = new ThreadLocal<>(); private static final Log LOGGER = LogFactory.getLog(SessionManager.class);
protected SessionHandlerValve handlerValve; /** To get session persist policies */
public String getSessionPersistPolicies() {
protected Set<SessionPolicy> sessionPolicy = EnumSet.of(SessionPolicy.DEFAULT); String policyStr = null;
for (SessionPolicy policy : this.sessionPolicy) {
private Log log = LogFactory.getLog(SessionManager.class); policyStr = (policyStr == null) ? policy.name() : policyStr.concat(",").concat(policy.name());
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;
} }
} return policyStr;
throw new IllegalArgumentException("Invalid session policy [" + name + "]");
}
}
/**
* To get session persist policies
*/
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
*/
public void setSessionPersistPolicies(String policyStr) {
Set<SessionPolicy> policySet = EnumSet.of(SessionPolicy.DEFAULT);
String[] policyArray = policyStr.split(",");
for (String policy : policyArray) {
policySet.add(SessionPolicy.fromName(policy));
}
this.sessionPolicy = policySet;
}
public boolean getSaveOnChange() {
return this.sessionPolicy.contains(SessionPolicy.SAVE_ON_CHANGE);
}
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;
Context context = getContextIns();
for (Valve valve : context.getPipeline().getValves()) {
if (valve instanceof SessionHandlerValve) {
this.handlerValve = (SessionHandlerValve) valve;
this.handlerValve.setSessionManager(this);
initializedValve = true;
break;
}
} }
if (!initializedValve) { /** To set session persist policies */
throw new LifecycleException("Session handling valve is not initialized.."); public void setSessionPersistPolicies(String policyStr) {
} Set<SessionPolicy> policySet = EnumSet.of(SessionPolicy.DEFAULT);
String[] policyArray = policyStr.split(",");
initialize(); for (String policy : policyArray) {
policySet.add(SessionPolicy.fromName(policy));
log.info("The sessions will expire after " + (getSessionTimeout(null)) + " seconds.");
context.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) ? createEmptySession() : null;
if (session != null) {
session.setId(sessionId);
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(getSessionTimeout(session));
session.tellNew();
}
setValues(sessionId, session, false, new SessionMetadata());
if (session != null) {
try {
save(session, true);
} catch (Exception ex) {
log.error("Error occurred 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 { this.sessionPolicy = policySet;
metadata = new SessionMetadata(); }
Session newSession = createEmptySession();
this.serializer.deserializeSessionData(data, newSession, metadata);
newSession.setId(sessionId); public boolean getSaveOnChange() {
newSession.access(); return this.sessionPolicy.contains(SessionPolicy.SAVE_ON_CHANGE);
newSession.setNew(false); }
newSession.setValid(true);
newSession.resetDirtyTracking();
newSession.setMaxInactiveInterval(getSessionTimeout(newSession));
session = newSession; public boolean getAlwaysSaveAfterRequest() {
isPersisted = true; return this.sessionPolicy.contains(SessionPolicy.ALWAYS_SAVE_AFTER_REQUEST);
} catch (Exception ex) { }
log.error("Error occurred while de-serializing the session object..", ex);
/** {@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;
Context context = getContextIns();
for (Valve valve : context.getPipeline().getValves()) {
if (valve instanceof SessionHandlerValve) {
this.handlerValve = (SessionHandlerValve) valve;
this.handlerValve.setSessionManager(this);
initializedValve = true;
break;
}
} }
}
setValues(sessionId, session, isPersisted, metadata); if (!initializedValve) {
throw new LifecycleException("Session handling valve is not initialized..");
}
initialize();
LOGGER.info("The sessions will expire after " + (getSessionTimeout(null)) + " seconds.");
context.setDistributable(true);
} }
return session;
}
/** /** {@inheritDoc} */
* {@inheritDoc} @Override
*/ protected synchronized void stopInternal() throws LifecycleException {
@Override super.setState(LifecycleState.STOPPING);
public void remove(org.apache.catalina.Session session) { super.stopInternal();
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();
Context context = getContextIns();
ClassLoader loader =
(context != null && context.getLoader() != null) ? context.getLoader().getClassLoader()
: null;
this.serializer.setClassLoader(loader);
} catch (Exception ex) {
log.error("Error occurred while initializing the session manager..", ex);
throw ex;
} }
}
/** /** {@inheritDoc} */
* To save session object to data-cache @Override
*/ public Session createSession(String sessionId) {
public void save(org.apache.catalina.Session session, boolean forceSave) { if (sessionId != null) {
try { sessionId =
Boolean isPersisted; (this.dataCache.setnx(sessionId, SessionConstants.NULL_SESSION) == 0L) ? null : sessionId;
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(newSession);
this.dataCache.expire(newSession.getId(), timeout);
log.trace("Session [" + newSession.getId() + "] expire in [" + timeout + "] seconds.");
} catch (IOException ex) {
log.error("Error occurred while saving the session object in data cache..", ex);
}
}
/**
* To process post request process
*/
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 { } else {
remove(session); do {
sessionId = generateSessionId();
} while (this.dataCache.setnx(sessionId, SessionConstants.NULL_SESSION) == 0L);
} }
log.trace(
"Session object " + (session.isValid() ? "saved: " : "removed: ") + session.getId()); Session session = (sessionId != null) ? createEmptySession() : null;
} if (session != null) {
} catch (Exception ex) { session.setId(sessionId);
log.error("Error occurred while processing post request process..", ex); session.setNew(true);
} finally { session.setValid(true);
this.sessionContext.remove(); session.setCreationTime(System.currentTimeMillis());
log.trace( session.setMaxInactiveInterval(getSessionTimeout(session));
"Session removed from ThreadLocal:" + ((session != null) ? session.getIdInternal() : "")); session.tellNew();
}
setValues(sessionId, session, false, new SessionMetadata());
if (session != null) {
try {
save(session, true);
} catch (Exception ex) {
LOGGER.error("Error occurred while creating session..", ex);
setValues(null, null);
session = null;
}
}
return session;
} }
}
private int getSessionTimeout(Session session) { /** {@inheritDoc} */
int timeout = getContextIns().getSessionTimeout() * 60; @Override
int sessionTimeout = (session == null) ? 0 : session.getMaxInactiveInterval(); public Session createEmptySession() {
return (sessionTimeout < timeout) ? ((timeout < 1800) ? 1800 : timeout) : sessionTimeout; return new Session(this);
}
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);
}
private void setValues(boolean isPersisted, SessionMetadata metadata) { /** {@inheritDoc} */
if (this.sessionContext.get() == null) { @Override
this.sessionContext.set(new SessionContext()); public void add(org.apache.catalina.Session session) {
save(session, false);
} }
this.sessionContext.get().setMetadata(metadata);
this.sessionContext.get().setPersisted(isPersisted);
}
private void setValues(String sessionId, Session session, boolean isPersisted, /** {@inheritDoc} */
SessionMetadata metadata) { @Override
setValues(sessionId, session); public Session findSession(String sessionId) throws IOException {
setValues(isPersisted, metadata); 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);
private Context getContextIns() { boolean isPersisted = false;
try { SessionMetadata metadata = null;
Method method = this.getClass().getSuperclass().getDeclaredMethod("getContext"); if (data == null) {
return (Context) method.invoke(this); sessionId = null;
} catch (Exception ex) { isPersisted = false;
try { } else {
Method method = this.getClass().getSuperclass().getDeclaredMethod("getContainer"); if (Arrays.equals(SessionConstants.NULL_SESSION, data)) {
return (Context) method.invoke(this); throw new IOException("NULL session data");
} catch (Exception ex2) { }
log.error("Error in getContext", ex2); try {
} metadata = new SessionMetadata();
Session newSession = createEmptySession();
this.serializer.deserializeSessionData(data, newSession, metadata);
newSession.setId(sessionId);
newSession.access();
newSession.setNew(false);
newSession.setValid(true);
newSession.resetDirtyTracking();
newSession.setMaxInactiveInterval(getSessionTimeout(newSession));
session = newSession;
isPersisted = true;
} catch (Exception ex) {
LOGGER.error("Error occurred while de-serializing the session object..", ex);
}
}
setValues(sessionId, session, isPersisted, metadata);
}
return session;
} }
throw new RuntimeException("Error occurred while creating container instance");
} /** {@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 RedisCache();
this.serializer = new SerializationUtil();
Context context = getContextIns();
ClassLoader loader = (context != null && context.getLoader() != null) ? context.getLoader().getClassLoader() : null;
this.serializer.setClassLoader(loader);
} catch (Exception ex) {
LOGGER.error("Error occurred while initializing the session manager..", ex);
throw ex;
}
}
/** To save session object to data cache. */
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(newSession);
this.dataCache.expire(newSession.getId(), timeout);
LOGGER.trace("Session [" + newSession.getId() + "] expire in [" + timeout + "] seconds.");
} catch (IOException ex) {
LOGGER.error("Error occurred while saving the session object in data cache..", ex);
}
}
/** To process post request process. */
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);
}
LOGGER.trace(
"Session object " + (session.isValid() ? "saved: " : "removed: ") + session.getId());
}
} catch (Exception ex) {
LOGGER.error("Error occurred while processing post request process..", ex);
} finally {
this.sessionContext.remove();
LOGGER.trace(
"Session removed from ThreadLocal:" + ((session != null) ? session.getIdInternal() : ""));
}
}
/** To get session timeout. */
private int getSessionTimeout(Session session) {
int timeout = getContextIns().getSessionTimeout() * 60;
int sessionTimeout = (session == null) ? 0 : session.getMaxInactiveInterval();
return (sessionTimeout < timeout) ? ((timeout < 1800) ? 1800 : timeout) : sessionTimeout;
}
/** To set values to session context. */
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);
}
/** To set values to session context. */
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);
}
/** To set values to session context. */
private void setValues(String sessionId, Session session, boolean isPersisted,
SessionMetadata metadata) {
setValues(sessionId, session);
setValues(isPersisted, metadata);
}
/** To get catalina context instance. */
private Context getContextIns() {
try {
Method method = this.getClass().getSuperclass().getDeclaredMethod("getContext");
return (Context) method.invoke(this);
} catch (Exception ex) {
try {
Method method = this.getClass().getSuperclass().getDeclaredMethod("getContainer");
return (Context) method.invoke(this);
} catch (Exception ex2) {
// skip
}
}
throw new RuntimeException("Error occurred while creating container instance");
}
} }

View File

@ -1,15 +1,13 @@
/** /**
* Tomcat clustering with Redis data-cache implementation. * 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 @ 12 Jul' 2018
*
* @author Ranjith Manickam
* @since 2.0
*/ */
Supports: Supports:
* Apache Tomcat 7 - Apache Tomcat 7
* Apache Tomcat 8 - Apache Tomcat 8
- Apache Tomcat 9
Pre-requisite: Pre-requisite:
1. jedis.jar 1. jedis.jar
@ -20,13 +18,13 @@ more details.. https://github.com/ran-jit/tomcat-cluster-redis-session-manager/w
Steps to be done, Steps to be done,
1. Move the downloaded jars to tomcat/lib directory 1. Move the downloaded jars to tomcat/lib directory
* tomcat/lib/ - tomcat/lib/
2. Add tomcat system property "catalina.base" 2. Add tomcat system property "catalina.base"
* catalina.base="TOMCAT_LOCATION" - 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 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 - tomcat/conf/redis-data-cache.properties
4. Add the below two lines in tomcat/conf/context.xml 4. Add the below two lines in tomcat/conf/context.xml
<Valve className="tomcat.request.session.redis.SessionHandlerValve" /> <Valve className="tomcat.request.session.redis.SessionHandlerValve" />
@ -38,5 +36,5 @@ Steps to be done,
<session-config> <session-config>
Note: Note:
* All your session attribute values must implement java.io.Serializable. - All your session attribute values must implement java.io.Serializable.
* This supports, both redis stand-alone and multiple node cluster based on the redis-data-cache.properties configuration. - Supports redis default, sentinel and cluster based on the redis-data-cache.properties configuration.

View File

@ -3,14 +3,19 @@
#- redis hosts ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, .... #- 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.hosts=127.0.0.1:6379
#- redis password (for stand-alone mode) #- redis password (used for default and sentinel mode)
#redis.password= #redis.password=
#- set true to enable redis cluster mode #- set true to enable redis cluster mode
redis.cluster.enabled=false redis.cluster.enabled=false
#- redis database (default 0) #- set true to enable redis sentinel mode
redis.sentinel.enabled=false
# redis sentinel master name (default value: mymaster)
redis.sentinel.master=mymaster
#- redis database (default value: 0)
#redis.database=0 #redis.database=0
#- redis connection timeout (default 2000) #- redis connection timeout (default value: 2000)
#redis.timeout=2000 #redis.timeout=2000