diff --git a/pom.xml b/pom.xml index 7090f49..7cdb108 100644 --- a/pom.xml +++ b/pom.xml @@ -1,68 +1,82 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - tomcat-cluster-redis-session-manager - tomcat-cluster-redis-session-manager - 2.0.5 - jar + tomcat-cluster-redis-session-manager + tomcat-cluster-redis-session-manager + 2.0.5 + jar - tomcat-cluster-redis-session-manager - http://maven.apache.org + tomcat-cluster-redis-session-manager + http://maven.apache.org - - UTF-8 - UTF-8 - 1.7 - + + UTF-8 + UTF-8 + 1.7 - - - redis.clients - jedis - 2.9.0 - - - org.apache.commons - commons-pool2 - 2.4.2 - - - commons-logging - commons-logging - 1.2 - + 2.9.0 + 2.4.2 + 1.2 - - - - + 3.6.1 + 1.7 + ${source-java.version} - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.6.1 - - 1.7 - 1.7 - - - - + + apache-tomcat-8.5.32 + apache-tomcat-8.5.32 + apache-tomcat-8.5.32 + + + + + + redis.clients + jedis + ${jedis.version} + + + org.apache.commons + commons-pool2 + ${commons-pool2.version} + + + commons-logging + commons-logging + ${commons-logging.version} + + + + + apache-tomcat + catalina + ${tomcat-catalina.version} + + + apache-tomcat + servlet-api + ${tomcat-servlet-api.version} + + + apache-tomcat + tomcat-api + ${tomcat-api.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler.version} + + ${source-java.version} + ${target-java.version} + + + + diff --git a/src/main/java/tomcat/request/session/SerializationUtil.java b/src/main/java/tomcat/request/session/SerializationUtil.java index 48c2557..2d7fd4c 100644 --- a/src/main/java/tomcat/request/session/SerializationUtil.java +++ b/src/main/java/tomcat/request/session/SerializationUtil.java @@ -1,5 +1,7 @@ package tomcat.request.session; +import org.apache.catalina.util.CustomObjectInputStream; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -11,81 +13,66 @@ import java.security.MessageDigest; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; -import org.apache.catalina.util.CustomObjectInputStream; -/** - * Tomcat clustering with Redis data-cache implementation. - * - * Session serialization utility. - * - * @author Ranjith Manickam - * @since 2.0 - */ +/** author: Ranjith Manickam @ 12 Jul' 2018 */ public class SerializationUtil { - private ClassLoader loader; + private ClassLoader loader; - /** - * To set class 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 attributes = new HashMap<>(); - - for (Enumeration enumerator = session.getAttributeNames(); - enumerator.hasMoreElements(); ) { - String key = enumerator.nextElement(); - attributes.put(key, session.getAttribute(key)); + /** To set class loader. */ + public void setClassLoader(ClassLoader loader) { + this.loader = loader; } - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) { - oos.writeUnshared(attributes); - oos.flush(); - serialized = bos.toByteArray(); + /** To get session attributes hash code. */ + public byte[] getSessionAttributesHashCode(Session session) throws IOException { + byte[] serialized; + Map attributes = new HashMap<>(); + + for (Enumeration enumerator = session.getAttributeNames(); + enumerator.hasMoreElements(); ) { + String key = enumerator.nextElement(); + attributes.put(key, session.getAttribute(key)); + } + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) { + 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; - try { - digester = MessageDigest.getInstance("MD5"); - } catch (Exception ex) { - throw new RuntimeException("Unable to get MessageDigest instance for MD5", ex); + /** To serialize session object. */ + public byte[] serializeSessionData(Session session, SessionMetadata metadata) throws IOException { + byte[] serialized; + 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; } - return digester.digest(serialized); - } - /** - * To serialize session object - */ - public byte[] serializeSessionData(Session session, SessionMetadata metadata) throws IOException { - byte[] serialized; - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) { - oos.writeObject(metadata); - session.writeObjectData(oos); - oos.flush(); - serialized = bos.toByteArray(); + /** 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); + } } - 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); - } - } -} \ No newline at end of file +} diff --git a/src/main/java/tomcat/request/session/Session.java b/src/main/java/tomcat/request/session/Session.java index 55f71b2..cd6f7bb 100644 --- a/src/main/java/tomcat/request/session/Session.java +++ b/src/main/java/tomcat/request/session/Session.java @@ -13,147 +13,126 @@ import org.apache.catalina.session.StandardSession; import tomcat.request.session.redis.SessionManager; -/** - * Tomcat clustering with Redis data-cache implementation. - * - * This class is uses to store and retrieve the HTTP request session objects. - * - * @author Ranjith Manickam - * @since 2.0 - */ +/** author: Ranjith Manickam @ 12 Jul' 2018 */ 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 changedAttributes; - protected Map changedAttributes; + protected static Boolean manualDirtyTrackingSupportEnabled = false; + protected static String manualDirtyTrackingAttributeKey = "__changed__"; - protected static Boolean manualDirtyTrackingSupportEnabled = false; - - protected static String manualDirtyTrackingAttributeKey = "__changed__"; - - public Session(Manager manager) { - super(manager); - resetDirtyTracking(); - } - - public void resetDirtyTracking() { - this.changedAttributes = new HashMap<>(); - this.dirty = false; - } - - public static void setManualDirtyTrackingSupportEnabled(boolean enabled) { - manualDirtyTrackingSupportEnabled = enabled; - } - - public static void setManualDirtyTrackingAttributeKey(String key) { - manualDirtyTrackingAttributeKey = key; - } - - public Boolean isDirty() { - return this.dirty || !this.changedAttributes.isEmpty(); - } - - public Map getChangedAttributes() { - return this.changedAttributes; - } - - /** - * {@inheritDoc} - */ - @Override - public void setId(String id) { - this.id = id; - } - - /** - * {@inheritDoc} - */ - @Override - public void setAttribute(String key, Object value) { - if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) { - this.dirty = true; - return; + public Session(Manager manager) { + super(manager); + resetDirtyTracking(); } - 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); - } + /** To reset dirty tracking. */ + public void resetDirtyTracking() { + this.changedAttributes = new HashMap<>(); + this.dirty = false; } - } - /** - * {@inheritDoc} - */ - @Override - public Object getAttribute(String name) { - return super.getAttribute(name); - } - - /** - * {@inheritDoc} - */ - @Override - public Enumeration getAttributeNames() { - return super.getAttributeNames(); - } - - /** - * {@inheritDoc} - */ - @Override - public void removeAttribute(String name) { - super.removeAttribute(name); - if (this.manager instanceof SessionManager && ((SessionManager) this.manager) - .getSaveOnChange()) { - ((SessionManager) this.manager).save(this, true); - } else { - this.dirty = true; + /** To set manual dirty tracking support enabled. */ + public static void setManualDirtyTrackingSupportEnabled(boolean enabled) { + manualDirtyTrackingSupportEnabled = enabled; } - } - /** - * {@inheritDoc} - */ - @Override - public void setPrincipal(Principal principal) { - super.setPrincipal(principal); - this.dirty = true; - } + /** To set manual dirty tracking attribute key. */ + public static void setManualDirtyTrackingAttributeKey(String key) { + manualDirtyTrackingAttributeKey = key; + } - /** - * {@inheritDoc} - */ - @Override - public void writeObjectData(ObjectOutputStream out) throws IOException { - super.writeObjectData(out); - out.writeLong(this.getCreationTime()); - } + /** Returns dirty check. */ + public Boolean isDirty() { + return this.dirty || !this.changedAttributes.isEmpty(); + } - /** - * {@inheritDoc} - */ - @Override - public void readObjectData(ObjectInputStream in) throws IOException, ClassNotFoundException { - super.readObjectData(in); - this.setCreationTime(in.readLong()); - } + /** To get changed attributes. */ + public Map getChangedAttributes() { + return this.changedAttributes; + } - /** - * {@inheritDoc} - */ - @Override - public void invalidate() { - super.invalidate(); - } -} \ No newline at end of file + /** {@inheritDoc} */ + @Override + public void setId(String id) { + this.id = id; + } + + /** {@inheritDoc} */ + @Override + public void setAttribute(String key, Object value) { + if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) { + this.dirty = true; + return; + } + + Object oldValue = getAttribute(key); + super.setAttribute(key, value); + + if ((value != null || oldValue != null) + && (value == null && oldValue != null || oldValue == null && value != null + || !value.getClass().isInstance(oldValue) || !value.equals(oldValue))) { + if (this.manager instanceof SessionManager && ((SessionManager) this.manager) + .getSaveOnChange()) { + ((SessionManager) this.manager).save(this, true); + } else { + this.changedAttributes.put(key, value); + } + } + } + + /** {@inheritDoc} */ + @Override + public Object getAttribute(String name) { + return super.getAttribute(name); + } + + /** {@inheritDoc} */ + @Override + public Enumeration getAttributeNames() { + return super.getAttributeNames(); + } + + /** {@inheritDoc} */ + @Override + public void removeAttribute(String name) { + super.removeAttribute(name); + if (this.manager instanceof SessionManager && ((SessionManager) this.manager) + .getSaveOnChange()) { + ((SessionManager) this.manager).save(this, true); + } else { + this.dirty = true; + } + } + + /** {@inheritDoc} */ + @Override + public void setPrincipal(Principal principal) { + super.setPrincipal(principal); + this.dirty = true; + } + + /** {@inheritDoc} */ + @Override + public void writeObjectData(ObjectOutputStream out) throws IOException { + super.writeObjectData(out); + out.writeLong(this.getCreationTime()); + } + + /** {@inheritDoc} */ + @Override + public void readObjectData(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readObjectData(in); + this.setCreationTime(in.readLong()); + } + + /** {@inheritDoc} */ + @Override + public void invalidate() { + super.invalidate(); + } + +} diff --git a/src/main/java/tomcat/request/session/SessionConstants.java b/src/main/java/tomcat/request/session/SessionConstants.java index 0cc3550..44f2b2d 100644 --- a/src/main/java/tomcat/request/session/SessionConstants.java +++ b/src/main/java/tomcat/request/session/SessionConstants.java @@ -1,18 +1,22 @@ package tomcat.request.session; -/** - * Tomcat clustering with Redis data-cache implementation. - * - * Session constants. - * - * @author Ranjith Manickam - * @since 2.0 - */ +/** author: Ranjith Manickam @ 12 Jul' 2018 */ 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"; -} \ No newline at end of file +} diff --git a/src/main/java/tomcat/request/session/SessionContext.java b/src/main/java/tomcat/request/session/SessionContext.java index 08c4ac8..263615b 100644 --- a/src/main/java/tomcat/request/session/SessionContext.java +++ b/src/main/java/tomcat/request/session/SessionContext.java @@ -1,85 +1,57 @@ package tomcat.request.session; -/** - * Tomcat clustering with Redis data-cache implementation. - * - * Session context uses to manage current session data. - * - * @author Ranjith Manickam - * @since 2.0 - */ +/** author: Ranjith Manickam @ 12 Jul' 2018 */ 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 get session id - */ - public String getId() { - return id; - } + /** To set session. */ + public void setSession(Session session) { + this.session = session; + } - /** - * To set session id - */ - public void setId(String id) { - this.id = id; - } + /** To check session is persisted. */ + public boolean isPersisted() { + return persisted; + } - /** - * To get session - */ - public Session getSession() { - return session; - } + /** To set session persisted. */ + public void setPersisted(boolean persisted) { + this.persisted = persisted; + } - /** - * To set session - */ - public void setSession(Session session) { - this.session = session; - } + /** To get session meta-data. */ + public SessionMetadata getMetadata() { + return metadata; + } - /** - * To check session is persisted - */ - public boolean isPersisted() { - return persisted; - } + /** To set session meta-data. */ + public void setMetadata(SessionMetadata metadata) { + this.metadata = metadata; + } - /** - * To set session persisted - */ - public void setPersisted(boolean persisted) { - this.persisted = persisted; - } + /** {@inheritDoc} */ + @Override + public String toString() { + return "SessionContext [id=" + id + "]"; + } - /** - * 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 + "]"; - } - -} \ No newline at end of file +} diff --git a/src/main/java/tomcat/request/session/SessionMetadata.java b/src/main/java/tomcat/request/session/SessionMetadata.java index 2b654aa..989cf08 100644 --- a/src/main/java/tomcat/request/session/SessionMetadata.java +++ b/src/main/java/tomcat/request/session/SessionMetadata.java @@ -5,60 +5,44 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; -/** - * Tomcat clustering with Redis data-cache implementation. - * - * This class is uses to store and retrieve the HTTP request session object meta-data. - * - * @author Ranjith Manickam - * @since 2.0 - */ +/** author: Ranjith Manickam @ 12 Jul' 2018 */ 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() { - this.attributesHash = new byte[0]; - } + public SessionMetadata() { + this.attributesHash = new byte[0]; + } - /** - * To get session meta-data hash - */ - public byte[] getAttributesHash() { - return this.attributesHash; - } + /** To get session meta-data hash. */ + public byte[] getAttributesHash() { + return this.attributesHash; + } - /** - * To set session meta-data hash - */ - public void setAttributesHash(byte[] attributesHash) { - this.attributesHash = attributesHash; - } + /** To set session meta-data hash. */ + public void setAttributesHash(byte[] attributesHash) { + this.attributesHash = attributesHash; + } - /** - * To copy session meta-data - */ - public void copyFieldsFrom(SessionMetadata metadata) { - this.setAttributesHash(metadata.getAttributesHash()); - } + /** To copy session meta-data. */ + public void copyFieldsFrom(SessionMetadata metadata) { + this.setAttributesHash(metadata.getAttributesHash()); + } - /** - * To write session meta-data to output stream - */ - private void writeObject(ObjectOutputStream out) throws IOException { - out.writeInt(this.attributesHash.length); - out.write(this.attributesHash); - } + /** To write session meta-data to output stream. */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(this.attributesHash.length); + out.write(this.attributesHash); + } - /** - * To read session meta-data from input stream - */ - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - int hashLength = in.readInt(); - byte[] attributesHash = new byte[hashLength]; - in.read(attributesHash, 0, hashLength); - this.attributesHash = attributesHash; - } -} \ No newline at end of file + /** To read session meta-data from input stream. */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + int hashLength = in.readInt(); + byte[] attributesHash = new byte[hashLength]; + in.read(attributesHash, 0, hashLength); + this.attributesHash = attributesHash; + } + +} diff --git a/src/main/java/tomcat/request/session/data/cache/DataCache.java b/src/main/java/tomcat/request/session/data/cache/DataCache.java index 2988184..be3b045 100644 --- a/src/main/java/tomcat/request/session/data/cache/DataCache.java +++ b/src/main/java/tomcat/request/session/data/cache/DataCache.java @@ -1,40 +1,49 @@ package tomcat.request.session.data.cache; -/** - * Tomcat clustering with Redis data-cache implementation. - * - * API for Data cache. - * - * @author Ranjith Manickam - * @since 2.0 - */ +/** author: Ranjith Manickam @ 12 Jul' 2018 */ public interface DataCache { - /** - * To set value in data-cache - */ - byte[] set(String key, byte[] value); + /** + * Set value in data-cache. + * + * @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 - * - * Returns If key exists = 0 else 1 - */ - Long setnx(String key, byte[] value); + /** + * Set value if key not exists in data-cache. + * + * @param key - key with which the specified value is to be associated. + * @param value - value to be associated with the specified key. + * @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 - */ - Long expire(String key, int seconds); + /** + * Set expiry in data-cache. + * + * @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 - */ - byte[] get(String key); + /** + * Get value from data-cache. + * + * @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 - */ - Long delete(String key); + /** + * Delete value from data-cache. + * + * @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); -} \ No newline at end of file +} diff --git a/src/main/java/tomcat/request/session/data/cache/impl/RedisCacheUtil.java b/src/main/java/tomcat/request/session/data/cache/impl/RedisCacheUtil.java deleted file mode 100644 index 1afe6b2..0000000 --- a/src/main/java/tomcat/request/session/data/cache/impl/RedisCacheUtil.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/src/main/java/tomcat/request/session/data/cache/impl/RedisClusterCacheUtil.java b/src/main/java/tomcat/request/session/data/cache/impl/RedisClusterCacheUtil.java deleted file mode 100644 index 328c3f3..0000000 --- a/src/main/java/tomcat/request/session/data/cache/impl/RedisClusterCacheUtil.java +++ /dev/null @@ -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 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(); - } - } -} diff --git a/src/main/java/tomcat/request/session/data/cache/impl/RedisConstants.java b/src/main/java/tomcat/request/session/data/cache/impl/RedisConstants.java deleted file mode 100644 index 3745a8a..0000000 --- a/src/main/java/tomcat/request/session/data/cache/impl/RedisConstants.java +++ /dev/null @@ -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..."; -} \ No newline at end of file diff --git a/src/main/java/tomcat/request/session/data/cache/impl/RedisDataCache.java b/src/main/java/tomcat/request/session/data/cache/impl/RedisDataCache.java deleted file mode 100644 index db1e25a..0000000 --- a/src/main/java/tomcat/request/session/data/cache/impl/RedisDataCache.java +++ /dev/null @@ -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 nodes = getJedisNodes(hosts, clusterEnabled); - - String password = properties.getProperty(RedisConstants.PASSWORD); - password = (password != null && !password.isEmpty()) ? password : null; - - int database = Integer.parseInt( - properties.getProperty(RedisConstants.DATABASE, String.valueOf(Protocol.DEFAULT_DATABASE))); - - int timeout = Integer.parseInt( - properties.getProperty(RedisConstants.TIMEOUT, String.valueOf(Protocol.DEFAULT_TIMEOUT))); - timeout = (timeout < Protocol.DEFAULT_TIMEOUT) ? Protocol.DEFAULT_TIMEOUT : timeout; - - if (clusterEnabled) { - dataCache = new RedisClusterCacheUtil((Set) nodes, password, timeout, - getPoolConfig(properties)); - } else { - dataCache = new RedisCacheUtil(((List) nodes).get(0), - Integer.parseInt(((List) nodes).get(1)), password, database, timeout, - getPoolConfig(properties)); - } - } - - /** - * To get jedis pool configuration - */ - 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 getJedisNodes(String hosts, boolean clusterEnabled) { - hosts = hosts.replaceAll("\\s", ""); - String[] hostPorts = hosts.split(","); - - List node = null; - Set nodes = null; - - for (String hostPort : hostPorts) { - String[] hostPortArr = hostPort.split(":"); - - if (clusterEnabled) { - nodes = (nodes == null) ? new HashSet() : nodes; - nodes.add(new HostAndPort(hostPortArr[0], Integer.valueOf(hostPortArr[1]))); - } else { - int port = Integer.valueOf(hostPortArr[1]); - if (!hostPortArr[0].isEmpty() && port > 0) { - node = (node == null) ? new ArrayList() : node; - node.add(hostPortArr[0]); - node.add(String.valueOf(port)); - break; - } - } - } - return clusterEnabled ? nodes : node; - } - - /** - * To load data-cache properties - */ - 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; - } -} \ No newline at end of file diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/AbstractRedisUtil.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/AbstractRedisUtil.java new file mode 100644 index 0000000..03d7f35 --- /dev/null +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/AbstractRedisUtil.java @@ -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 pool; + private final long failiureWaitTime; + + AbstractRedisUtil(Pool 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(); + } + } + +} diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisCache.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisCache.java new file mode 100644 index 0000000..e83dd3b --- /dev/null +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisCache.java @@ -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) nodes, password, timeout, poolConfig); + break; + case SENTINEL: + String masterName = String.valueOf(properties.getProperty(RedisConstants.SENTINEL_MASTER, RedisConstants.DEFAULT_SENTINEL_MASTER)); + dataCache = new RedisSentinelUtil((Set) nodes, masterName, password, database, timeout, poolConfig); + break; + default: + dataCache = new RedisUtil(((List) nodes).get(0), Integer.parseInt(((List) 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 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 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; + } + +} diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisClusterUtil.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisClusterUtil.java new file mode 100644 index 0000000..95ad109 --- /dev/null +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisClusterUtil.java @@ -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 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; + } + +} diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisConstants.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisConstants.java new file mode 100644 index 0000000..3009898 --- /dev/null +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisConstants.java @@ -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 + } + +} diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisSentinelUtil.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisSentinelUtil.java new file mode 100644 index 0000000..d617222 --- /dev/null +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisSentinelUtil.java @@ -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 nodes, + String masterName, + String password, + int database, + int timeout, + JedisPoolConfig poolConfig) { + super(new JedisSentinelPool(masterName, nodes, poolConfig, timeout, password, database), FAILIURE_WAIT_TIME); + } + +} diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisUtil.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisUtil.java new file mode 100644 index 0000000..81aab84 --- /dev/null +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisUtil.java @@ -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); + } + +} diff --git a/src/main/java/tomcat/request/session/redis/SessionHandlerValve.java b/src/main/java/tomcat/request/session/redis/SessionHandlerValve.java index dc4df2a..002206a 100644 --- a/src/main/java/tomcat/request/session/redis/SessionHandlerValve.java +++ b/src/main/java/tomcat/request/session/redis/SessionHandlerValve.java @@ -8,35 +8,24 @@ import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase; -/** - * 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 - */ +/** author: Ranjith Manickam @ 12 Jul' 2018 */ public class SessionHandlerValve extends ValveBase { - private SessionManager manager; + private SessionManager manager; - /** - * To set session 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); + /** To set session manager */ + public void setSessionManager(SessionManager manager) { + this.manager = manager; } - } -} \ No newline at end of file + + /** {@inheritDoc} */ + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + try { + getNext().invoke(request, response); + } finally { + manager.afterRequest(request); + } + } + +} diff --git a/src/main/java/tomcat/request/session/redis/SessionManager.java b/src/main/java/tomcat/request/session/redis/SessionManager.java index 60ca4d3..de71715 100644 --- a/src/main/java/tomcat/request/session/redis/SessionManager.java +++ b/src/main/java/tomcat/request/session/redis/SessionManager.java @@ -1,11 +1,5 @@ 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.Lifecycle; import org.apache.catalina.LifecycleException; @@ -16,396 +10,343 @@ import org.apache.catalina.connector.Request; import org.apache.catalina.session.ManagerBase; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import tomcat.request.session.SerializationUtil; import tomcat.request.session.Session; import tomcat.request.session.SessionConstants; +import tomcat.request.session.SessionConstants.SessionPolicy; import tomcat.request.session.SessionContext; import tomcat.request.session.SessionMetadata; import tomcat.request.session.data.cache.DataCache; -import tomcat.request.session.data.cache.impl.RedisDataCache; +import tomcat.request.session.data.cache.impl.redis.RedisCache; -/** - * Tomcat clustering with Redis data-cache implementation. - * - * Manager that implements per-request session persistence. It is intended to be used with - * non-sticky load-balancers. - * - * @author Ranjith Manickam - * @since 2.0 - */ +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; + +/** author: Ranjith Manickam @ 12 Jul' 2018 */ 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 = new ThreadLocal<>(); + protected Set sessionPolicy = EnumSet.of(SessionPolicy.DEFAULT); - protected ThreadLocal sessionContext = new ThreadLocal<>(); + private static final Log LOGGER = LogFactory.getLog(SessionManager.class); - protected SessionHandlerValve handlerValve; - - protected Set sessionPolicy = EnumSet.of(SessionPolicy.DEFAULT); - - private Log log = LogFactory.getLog(SessionManager.class); - - enum SessionPolicy { - DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST; - - static SessionPolicy fromName(String name) { - for (SessionPolicy policy : SessionPolicy.values()) { - if (policy.name().equalsIgnoreCase(name)) { - return policy; + /** 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()); } - } - 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 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; - } + return policyStr; } - if (!initializedValve) { - throw new LifecycleException("Session handling valve is not initialized.."); - } + /** To set session persist policies */ + public void setSessionPersistPolicies(String policyStr) { + Set policySet = EnumSet.of(SessionPolicy.DEFAULT); + String[] policyArray = policyStr.split(","); - initialize(); - - 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"); + for (String policy : policyArray) { + policySet.add(SessionPolicy.fromName(policy)); } - try { - metadata = new SessionMetadata(); - Session newSession = createEmptySession(); - this.serializer.deserializeSessionData(data, newSession, metadata); + this.sessionPolicy = policySet; + } - newSession.setId(sessionId); - newSession.access(); - newSession.setNew(false); - newSession.setValid(true); - newSession.resetDirtyTracking(); - newSession.setMaxInactiveInterval(getSessionTimeout(newSession)); + public boolean getSaveOnChange() { + return this.sessionPolicy.contains(SessionPolicy.SAVE_ON_CHANGE); + } - session = newSession; - isPersisted = true; - } catch (Exception ex) { - log.error("Error occurred while de-serializing the session object..", ex); + 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; + } } - } - 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} - */ - @Override - public void remove(org.apache.catalina.Session session) { - remove(session, false); - } - - /** - * {@inheritDoc} - */ - @Override - public void remove(org.apache.catalina.Session session, boolean update) { - this.dataCache.expire(session.getId(), 10); - } - - /** - * {@inheritDoc} - */ - @Override - public void load() throws ClassNotFoundException, IOException { - // Auto-generated method stub - } - - /** - * {@inheritDoc} - */ - @Override - public void unload() throws IOException { - // Auto-generated method stub - } - - /** - * To initialize the session manager - */ - private void initialize() { - try { - this.dataCache = new RedisDataCache(); - - this.serializer = new SerializationUtil(); - 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} */ + @Override + protected synchronized void stopInternal() throws LifecycleException { + super.setState(LifecycleState.STOPPING); + super.stopInternal(); } - } - /** - * 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); - 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()); + /** {@inheritDoc} */ + @Override + public Session createSession(String sessionId) { + if (sessionId != null) { + sessionId = + (this.dataCache.setnx(sessionId, SessionConstants.NULL_SESSION) == 0L) ? null : sessionId; } 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()); - } - } catch (Exception ex) { - log.error("Error occurred while processing post request process..", ex); - } finally { - this.sessionContext.remove(); - log.trace( - "Session removed from ThreadLocal:" + ((session != null) ? session.getIdInternal() : "")); + + 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) { + LOGGER.error("Error occurred while creating session..", ex); + setValues(null, null); + session = null; + } + } + return session; } - } - 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; - } - - private void setValues(String sessionId, Session session) { - if (this.sessionContext.get() == null) { - this.sessionContext.set(new SessionContext()); + /** {@inheritDoc} */ + @Override + public Session createEmptySession() { + return new Session(this); } - this.sessionContext.get().setId(sessionId); - this.sessionContext.get().setSession(session); - } - private void setValues(boolean isPersisted, SessionMetadata metadata) { - if (this.sessionContext.get() == null) { - this.sessionContext.set(new SessionContext()); + /** {@inheritDoc} */ + @Override + 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, - SessionMetadata metadata) { - setValues(sessionId, session); - setValues(isPersisted, metadata); - } + /** {@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); - 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) { - log.error("Error in getContext", ex2); - } + boolean isPersisted = false; + SessionMetadata metadata = null; + if (data == null) { + sessionId = null; + isPersisted = false; + } else { + if (Arrays.equals(SessionConstants.NULL_SESSION, data)) { + throw new IOException("NULL session data"); + } + try { + metadata = new SessionMetadata(); + Session newSession = 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"); + } + } diff --git a/src/main/resources/readMe.txt b/src/main/resources/readMe.txt index b810d2d..75394b1 100644 --- a/src/main/resources/readMe.txt +++ b/src/main/resources/readMe.txt @@ -1,15 +1,13 @@ /** * Tomcat clustering with Redis data-cache implementation. * - * Tomcat clustering with Redis is the plugable one. It uses to store session objects to Redis data cache. - * - * @author Ranjith Manickam - * @since 2.0 + * author: Ranjith Manickam @ 12 Jul' 2018 */ Supports: - * Apache Tomcat 7 - * Apache Tomcat 8 + - Apache Tomcat 7 + - Apache Tomcat 8 + - Apache Tomcat 9 Pre-requisite: 1. jedis.jar @@ -20,13 +18,13 @@ more details.. https://github.com/ran-jit/tomcat-cluster-redis-session-manager/w Steps to be done, 1. Move the downloaded jars to tomcat/lib directory - * tomcat/lib/ + - tomcat/lib/ 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 - * tomcat/conf/redis-data-cache.properties + - tomcat/conf/redis-data-cache.properties 4. Add the below two lines in tomcat/conf/context.xml @@ -38,5 +36,5 @@ Steps to be done, Note: - * 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. + - All your session attribute values must implement java.io.Serializable. + - Supports redis default, sentinel and cluster based on the redis-data-cache.properties configuration. diff --git a/src/main/resources/redis-data-cache.properties b/src/main/resources/redis-data-cache.properties index fa923b0..b1de040 100644 --- a/src/main/resources/redis-data-cache.properties +++ b/src/main/resources/redis-data-cache.properties @@ -3,14 +3,19 @@ #- redis hosts ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, .... redis.hosts=127.0.0.1:6379 -#- redis password (for stand-alone mode) +#- redis password (used for default and sentinel mode) #redis.password= #- set true to enable redis cluster mode 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 connection timeout (default 2000) +#- redis connection timeout (default value: 2000) #redis.timeout=2000 \ No newline at end of file