Skip to content

Commit ae91148

Browse files
committed
Merge pull request #1 from jcoleman/master
merge newest master
2 parents 66863fc + 40fbec7 commit ae91148

File tree

5 files changed

+93
-43
lines changed

5 files changed

+93
-43
lines changed

README.markdown

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ Tomcat Versions
1818
This project supports both Tomcat 6 and Tomcat 7. Starting at project version 1.1, precompiled JAR downloads are available for either version of Tomcat while project versions before 1.1 are only available for Tomcat 6.
1919

2020
The official release branches in Git are as follows:
21-
* `tomcat-6`: Continuing work for Tomcat 6 releases. Compatible with Java 6.
22-
* `master`: Continuing work for Tomcat 7 releases. Compatible with Java 6 or 7.
23-
* `tomcat-7`: Finalized; has now been merged into `master`. Compatible with Java 6 or 7.
24-
* `java-7`: All of the work from master for Tomcat 7 but taking advantage of new features in Java 7. Compatible with Java 7 only.
21+
* `master`: Continuing work for Tomcat 7 releases. Compatible with Java 7.
22+
* `tomcat-6`: Deprecated; may accept submitted patches, but no new work is being done on this branch. Compatible with Tomcat 6 and Java 6.
23+
24+
Finalized branches include:
25+
* `tomcat-7`: Has been merged into `master`. Compatible with Java 6 or 7.
26+
* `java-7`: Has been merged into `master`. All of the work from master for Tomcat 7 but taking advantage of new features in Java 7. Compatible with Java 7 only.
2527

2628
Architecture
2729
------------
@@ -96,7 +98,10 @@ This condition will be detected by the session manager and a java.lang.IllegalSt
9698

9799
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.
98100

99-
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.
101+
Possible solutions:
102+
103+
- Enable the "save on change" feature by setting `saveOnChange` to `true` in your manager declaration in Tomcat's context.xml. Using this feature will degrade performance slightly as any change to the session will save the session synchronously to Redis, and technically this will still exhibit slight race condition behavior, but it eliminates as much possiblity of errors occurring as possible.
104+
- 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.
100105

101106
Acknowledgements
102107
----------------

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ repositories {
77

88
dependencies {
99
compile group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '7.0.27'
10-
compile group: 'redis.clients', name: 'jedis', version: '2.0.0'
10+
compile group: 'redis.clients', name: 'jedis', version: '2.5.2'
1111
// compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
1212
// testCompile group: 'junit', name: 'junit', version: '4.+'
1313
}

src/main/java/com/radiadesign/catalina/session/JavaSerializer.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ public byte[] serializeFrom(HttpSession session) throws IOException {
1919

2020
RedisSession redisSession = (RedisSession) session;
2121
ByteArrayOutputStream bos = new ByteArrayOutputStream();
22-
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));
23-
oos.writeLong(redisSession.getCreationTime());
24-
redisSession.writeObjectData(oos);
25-
26-
oos.close();
22+
try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) {
23+
oos.writeLong(redisSession.getCreationTime());
24+
redisSession.writeObjectData(oos);
25+
}
2726

2827
return bos.toByteArray();
2928
}

src/main/java/com/radiadesign/catalina/session/RedisSession.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@
44
import org.apache.catalina.Manager;
55
import org.apache.catalina.session.StandardSession;
66
import java.util.HashMap;
7+
import java.io.IOException;
8+
9+
import org.apache.juli.logging.Log;
10+
import org.apache.juli.logging.LogFactory;
711

812

913
public class RedisSession extends StandardSession {
14+
15+
private final Log log = LogFactory.getLog(RedisSession.class);
16+
1017
protected static Boolean manualDirtyTrackingSupportEnabled = false;
1118

1219
public static void setManualDirtyTrackingSupportEnabled(Boolean enabled) {
@@ -37,7 +44,7 @@ public HashMap<String, Object> getChangedAttributes() {
3744
}
3845

3946
public void resetDirtyTracking() {
40-
changedAttributes = new HashMap<String, Object>();
47+
changedAttributes = new HashMap<>();
4148
dirty = false;
4249
}
4350

@@ -54,16 +61,34 @@ public void setAttribute(String key, Object value) {
5461
|| oldValue == null && value != null
5562
|| !value.getClass().isInstance(oldValue)
5663
|| !value.equals(oldValue) ) ) {
57-
changedAttributes.put(key, value);
64+
if (this.manager instanceof RedisSessionManager
65+
&& ((RedisSessionManager)this.manager).getSaveOnChange()) {
66+
try {
67+
((RedisSessionManager)this.manager).save(this);
68+
} catch (IOException ex) {
69+
log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage());
70+
}
71+
} else {
72+
changedAttributes.put(key, value);
73+
}
5874
}
5975

6076
super.setAttribute(key, value);
6177
}
6278

6379
@Override
6480
public void removeAttribute(String name) {
65-
dirty = true;
6681
super.removeAttribute(name);
82+
if (this.manager instanceof RedisSessionManager
83+
&& ((RedisSessionManager)this.manager).getSaveOnChange()) {
84+
try {
85+
((RedisSessionManager)this.manager).save(this);
86+
} catch (IOException ex) {
87+
log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage());
88+
}
89+
} else {
90+
dirty = true;
91+
}
6792
}
6893

6994
@Override

src/main/java/com/radiadesign/catalina/session/RedisSessionManager.java

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,17 @@ public class RedisSessionManager extends ManagerBase implements Lifecycle {
3939
protected JedisPool connectionPool;
4040

4141
protected RedisSessionHandlerValve handlerValve;
42-
protected ThreadLocal<RedisSession> currentSession = new ThreadLocal<RedisSession>();
43-
protected ThreadLocal<String> currentSessionId = new ThreadLocal<String>();
44-
protected ThreadLocal<Boolean> currentSessionIsPersisted = new ThreadLocal<Boolean>();
42+
protected ThreadLocal<RedisSession> currentSession = new ThreadLocal<>();
43+
protected ThreadLocal<String> currentSessionId = new ThreadLocal<>();
44+
protected ThreadLocal<Boolean> currentSessionIsPersisted = new ThreadLocal<>();
4545
protected Serializer serializer;
4646

4747
protected static String name = "RedisSessionManager";
4848

4949
protected String serializationStrategyClass = "com.radiadesign.catalina.session.JavaSerializer";
5050

51+
protected boolean saveOnChange = false;
52+
5153
/**
5254
* The lifecycle event support for this component.
5355
*/
@@ -97,6 +99,14 @@ public void setSerializationStrategyClass(String strategy) {
9799
this.serializationStrategyClass = strategy;
98100
}
99101

102+
public boolean getSaveOnChange() {
103+
return saveOnChange;
104+
}
105+
106+
public void setSaveOnChange(boolean saveOnChange) {
107+
this.saveOnChange = saveOnChange;
108+
}
109+
100110
@Override
101111
public int getRejectedSessions() {
102112
// Essentially do nothing.
@@ -201,13 +211,7 @@ protected synchronized void startInternal() throws LifecycleException {
201211

202212
try {
203213
initializeSerializer();
204-
} catch (ClassNotFoundException e) {
205-
log.fatal("Unable to load serializer", e);
206-
throw new LifecycleException(e);
207-
} catch (InstantiationException e) {
208-
log.fatal("Unable to load serializer", e);
209-
throw new LifecycleException(e);
210-
} catch (IllegalAccessException e) {
214+
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
211215
log.fatal("Unable to load serializer", e);
212216
throw new LifecycleException(e);
213217
}
@@ -246,33 +250,32 @@ protected synchronized void stopInternal() throws LifecycleException {
246250
}
247251

248252
@Override
249-
public Session createSession(String sessionId) {
250-
RedisSession session = (RedisSession)createEmptySession();
251-
252-
// Initialize the properties of the new session and return it
253-
session.setNew(true);
254-
session.setValid(true);
255-
session.setCreationTime(System.currentTimeMillis());
256-
session.setMaxInactiveInterval(getMaxInactiveInterval());
257-
253+
public Session createSession(String requestedSessionId) {
254+
RedisSession session = null;
255+
String sessionId = null;
258256
String jvmRoute = getJvmRoute();
259257

260258
Boolean error = true;
261259
Jedis jedis = null;
262-
263260
try {
264261
jedis = acquireConnection();
265262

266263
// Ensure generation of a unique session identifier.
267-
do {
268-
if (null == sessionId) {
269-
sessionId = generateSessionId();
270-
}
271-
264+
if (null == requestedSessionId) {
272265
if (jvmRoute != null) {
273-
sessionId += '.' + jvmRoute;
266+
sessionId = requestedSessionId + '.' + jvmRoute;
267+
}
268+
if (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L) {
269+
sessionId = null;
274270
}
275-
} while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 1L); // 1 = key set; 0 = key already existed
271+
} else {
272+
do {
273+
sessionId = generateSessionId();
274+
if (jvmRoute != null) {
275+
sessionId += '.' + jvmRoute;
276+
}
277+
} while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L); // 1 = key set; 0 = key already existed
278+
}
276279

277280
/* Even though the key is set in Redis, we are not going to flag
278281
the current thread as having had the session persisted since
@@ -282,12 +285,27 @@ This ensures that the save(session) at the end of the request
282285

283286
error = false;
284287

285-
session.setId(sessionId);
286-
session.tellNew();
288+
if (null != sessionId) {
289+
session = (RedisSession)createEmptySession();
290+
session.setNew(true);
291+
session.setValid(true);
292+
session.setCreationTime(System.currentTimeMillis());
293+
session.setMaxInactiveInterval(getMaxInactiveInterval());
294+
session.setId(sessionId);
295+
session.tellNew();
296+
}
287297

288298
currentSession.set(session);
289299
currentSessionId.set(sessionId);
290300
currentSessionIsPersisted.set(false);
301+
302+
if (null != session && this.getSaveOnChange()) {
303+
try {
304+
save(session);
305+
} catch (IOException ex) {
306+
log.error("Error saving newly created session (triggered by saveOnChange=true): " + ex.getMessage());
307+
}
308+
}
291309
} finally {
292310
if (jedis != null) {
293311
returnConnection(jedis, error);
@@ -326,6 +344,9 @@ public Session findSession(String id) throws IOException {
326344

327345
if (session != null) {
328346
currentSessionIsPersisted.set(true);
347+
} else {
348+
currentSessionIsPersisted.set(false);
349+
id = null;
329350
}
330351
}
331352

0 commit comments

Comments
 (0)