Browse files

Override other necessary parts of the ManagerBase implementation (suc…

…h as add(session) and createSession()) since the default implementation relies on local session storage. In this we also ensure that session identifiers are unique in Redis.

Update manager to use the updated dirty tracking of RedisSession.
Capture race condition where session identifier has been created but not yet persisted and throw an intelligent error.
Only load session from Redis if we haven't already tried once for that identifier in the current request in the current thread.
  • Loading branch information...
1 parent b94a4a7 commit 57c859422845cbec918b70ee6b68dbaf60828439 @jcoleman committed Aug 26, 2011
Showing with 110 additions and 31 deletions.
  1. +2 −0 README.markdown
  2. +108 −31 src/main/java/com/radiadesign/catalina/session/RedisSessionManager.java
View
2 README.markdown
@@ -43,6 +43,8 @@ Possible Issues
There is the possibility of a race condition that would cause seeming invisibility of the session immediately after your web application logs in a user: if the response has finished streaming and the client requests a new page before the valve has been able to complete saving the session into Redis, then the new request will not see the session.
+This condition will be detected by the session manager and a java.lang.IllegalStateException with the message `Race condition encountered: attempted to load session[SESSION_ID] which has been created but not yet serialized.` will be thrown.
+
Normally this should be incredibly unlikely (insert joke about programmers and "this should never happen" statements here) since the connection to save the session into Redis is almost guaranteed to be faster than the latency between a client receiving the response, processing it, and starting a new request.
If you encounter errors, then you can force save the session early (before sending a response to the client) then you can retrieve the current session, and call `currentSession.manager.save(currentSession)` to synchronously eliminate the race condition. Note: this will only work directly if your application has the actual session object directly exposed. Many frameworks (and often even Tomcat) will expose the session in their own wrapper HttpSession implementing class. You may be able to dig through these layers to expose the actual underlying RedisSession instance--if so, then using that instance will allow you to implement the workaround.
View
139 src/main/java/com/radiadesign/catalina/session/RedisSessionManager.java
@@ -16,6 +16,7 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -24,6 +25,9 @@
public class RedisSessionManager extends ManagerBase implements Lifecycle {
+
+ protected byte[] NULL_SESSION = "null".getBytes();
+
private static Logger log = Logger.getLogger("RedisSessionManager");
protected String host = "localhost";
protected int port = 6379;
@@ -32,8 +36,12 @@
protected RedisSessionHandlerValve handlerValve;
protected ThreadLocal<RedisSession> currentSession = new ThreadLocal<RedisSession>();
+ protected ThreadLocal<String> currentSessionId = new ThreadLocal<String>();
+ protected ThreadLocal<Boolean> currentSessionIsPersisted = new ThreadLocal<Boolean>();
protected Serializer serializer;
+ protected static String name = "RedisSessionManager";
+
protected String serializationStrategyClass = "com.radiadesign.catalina.session.JavaSerializer";
/**
@@ -87,7 +95,7 @@ protected Jedis acquireConnection() {
return jedis;
}
-
+
protected void returnConnection(Jedis jedis, Boolean error) {
if (error) {
connectionPool.returnBrokenResource(jedis);
@@ -117,7 +125,6 @@ public void addLifecycleListener(LifecycleListener listener) {
lifecycle.addLifecycleListener(listener);
}
-
/**
* Get the lifecycle listeners associated with this lifecycle. If this
* Lifecycle has no listeners registered, a zero-length array is returned.
@@ -136,11 +143,6 @@ public void removeLifecycleListener(LifecycleListener listener) {
lifecycle.removeLifecycleListener(listener);
}
- @Override
- public Session createEmptySession() {
- return new RedisSession(this);
- }
-
public void start() throws LifecycleException {
for (Valve valve : getContainer().getPipeline().getValves()) {
if (valve instanceof RedisSessionHandlerValve) {
@@ -169,7 +171,7 @@ public void start() throws LifecycleException {
initializeDatabaseConnection();
setDistributable(true);
-
+
lifecycle.fireLifecycleEvent(START_EVENT, null);
}
@@ -183,8 +185,89 @@ public void stop() throws LifecycleException {
lifecycle.fireLifecycleEvent(STOP_EVENT, null);
}
+ @Override
+ public Session createSession() {
+ RedisSession session = (RedisSession)createEmptySession();
+
+ // Initialize the properties of the new session and return it
+ session.setNew(true);
+ session.setValid(true);
+ session.setCreationTime(System.currentTimeMillis());
+ session.setMaxInactiveInterval(getMaxInactiveInterval());
+
+ String sessionId;
+ String jvmRoute = getJvmRoute();
+
+ Boolean error = true;
+ Jedis jedis = null;
+
+ try {
+ jedis = acquireConnection();
+
+ // Ensure generation of a unique session identifier.
+ do {
+ sessionId = generateSessionId();
+
+ if (jvmRoute != null) {
+ sessionId += '.' + jvmRoute;
+ }
+ } while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 1L); // 1 = key set; 0 = key already existed
+
+ /* Even though the key is set in Redis, we are not going to flag
+ the current thread as having had the session persisted since
+ the session isn't actually serialized to Redis yet.
+ This ensures that the save(session) at the end of the request
+ will serialize the session into Redis with 'set' instead of 'setnx'. */
+
+ error = false;
+
+ session.setId(sessionId);
+ session.tellNew();
+ } finally {
+ if (jedis != null) {
+ returnConnection(jedis, error);
+ }
+ }
+
+ return session;
+ }
+
+ @Override
+ public Session createEmptySession() {
+ return new RedisSession(this);
+ }
+
+ @Override
+ public void add(Session session) {
+ try {
+ save(session);
+ } catch (IOException ex) {
+ log.warning("Unable to add to session manager store: " + ex.getMessage());
+ throw new RuntimeException("Unable to add to session manager store.", ex);
+ }
+ }
+
+ @Override
public Session findSession(String id) throws IOException {
- return loadSession(id);
+ RedisSession session;
+
+ if (id == null) {
+ session = null;
+ currentSessionIsPersisted.set(false);
+ } else if (id.equals(currentSessionId.get())) {
+ session = currentSession.get();
+ } else {
+ session = loadSessionFromRedis(id);
+
+ if (session != null) {
+ currentSessionIsPersisted.set(true);
+ }
+ }
+
+ currentSession.set(session);
+ currentSessionId.set(id);
+
+ return session;
}
public void clear() {
@@ -231,17 +314,8 @@ public int getSize() throws IOException {
}
}
- public Session loadSession(String id) throws IOException {
+ public RedisSession loadSessionFromRedis(String id) throws IOException {
RedisSession session;
-
- session = currentSession.get();
- if (session != null) {
- if (id.equals(session.getId())) {
- return session;
- } else {
- currentSession.remove();
- }
- }
Jedis jedis = null;
Boolean error = true;
@@ -256,34 +330,33 @@ public Session loadSession(String id) throws IOException {
if (data == null) {
log.fine("Session " + id + " not found in Redis");
session = null;
+ } else if (Arrays.equals(NULL_SESSION, data)) {
+ throw new IllegalStateException("Race condition encountered: attempted to load session[" + id + "] which has been created but not yet serialized.");
} else {
- log.fine("Deserializing session from Redis");
+ log.fine("Deserializing session " + id + " from Redis");
session = (RedisSession)createEmptySession();
serializer.deserializeInto(data, session);
session.setId(id);
session.setNew(false);
session.setMaxInactiveInterval(getMaxInactiveInterval() * 1000);
session.access();
session.setValid(true);
- session.resetChangedAttributes();
+ session.resetDirtyTracking();
if (log.isLoggable(Level.FINE)) {
- log.fine("Session Contents [" + session.getId() + "]:");
+ log.fine("Session Contents [" + id + "]:");
for (Object name : Collections.list(session.getAttributeNames())) {
log.fine(" " + name);
}
}
-
- log.fine("Loaded session id " + id);
}
- currentSession.set(session);
return session;
} catch (IOException e) {
log.severe(e.getMessage());
throw e;
} catch (ClassNotFoundException ex) {
- log.log(Level.SEVERE, "Unable to deserialize into session ", ex);
+ log.log(Level.SEVERE, "Unable to deserialize into session", ex);
throw new IOException("Unable to deserialize into session", ex);
} finally {
if (jedis != null) {
@@ -310,20 +383,20 @@ public void save(Session session) throws IOException {
Boolean sessionIsDirty = redisSession.isDirty();
- redisSession.resetChangedAttributes();
+ redisSession.resetDirtyTracking();
byte[] binaryId = redisSession.getId().getBytes();
byte[] data = serializer.serializeFrom(redisSession);
jedis = acquireConnection();
- if (sessionIsDirty) {
+ if (sessionIsDirty || currentSessionIsPersisted.get() != true) {
jedis.set(binaryId, data);
} else {
- // TODO: Only do this if this session object was not loaded from the database.
- // Or if forced (potentially on first save).
jedis.setnx(binaryId, data);
}
+ currentSessionIsPersisted.set(true);
+
log.fine("Setting expire timeout on session [" + redisSession.getId() + "] to " + getMaxInactiveInterval());
jedis.expire(binaryId, getMaxInactiveInterval());
@@ -342,8 +415,9 @@ public void save(Session session) throws IOException {
public void remove(Session session) {
Jedis jedis = null;
Boolean error = true;
-
+
log.fine("Removing session ID : " + session.getId());
+
try {
jedis = acquireConnection();
jedis.del(session.getId());
@@ -359,10 +433,13 @@ public void afterRequest() {
RedisSession redisSession = currentSession.get();
if (redisSession != null) {
currentSession.remove();
+ currentSessionId.remove();
+ currentSessionIsPersisted.remove();
log.fine("Session removed from ThreadLocal :" + redisSession.getIdInternal());
}
}
+ @Override
public void processExpires() {
// We are going to use Redis's ability to expire keys for session expiration.

0 comments on commit 57c8594

Please sign in to comment.