Skip to content

Commit 0c4b40a

Browse files
committed
Determine whether or not nested changes occurred using MD5 hash of attributes.
1 parent 8b2a7d4 commit 0c4b40a

File tree

5 files changed

+196
-59
lines changed

5 files changed

+196
-59
lines changed

spec/requests/session_updating_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@
7676
json['value'].should == '6'
7777
end
7878

79+
it 'should detect nested changes and persist them' do
80+
post(SESSION_PATH, body: {param1: {subparam: '5'}})
81+
json['attributes']['param1']['subparam'].should == '5'
82+
post(SESSION_PATH, body: {param1: {subparam: '6'}})
83+
get(SESSION_PATH)
84+
json['attributes']['param1']['subparam'].should == '6'
85+
end
86+
7987
it 'should default to last-write-wins behavior for simultaneous updates' do
8088
post(SESSION_PATH, body: {param1: '5'})
8189

src/main/java/com/orangefunction/tomcat/redissessions/JavaSerializer.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,67 @@
33
import org.apache.catalina.util.CustomObjectInputStream;
44

55
import javax.servlet.http.HttpSession;
6+
7+
import java.util.Enumeration;
8+
import java.util.HashMap;
69
import java.io.*;
10+
import java.security.MessageDigest;
11+
import java.security.NoSuchAlgorithmException;
712

13+
import org.apache.juli.logging.Log;
14+
import org.apache.juli.logging.LogFactory;
815

916
public class JavaSerializer implements Serializer {
1017
private ClassLoader loader;
1118

19+
private final Log log = LogFactory.getLog(JavaSerializer.class);
20+
1221
@Override
1322
public void setClassLoader(ClassLoader loader) {
1423
this.loader = loader;
1524
}
1625

17-
@Override
18-
public byte[] serializeFrom(HttpSession session) throws IOException {
26+
public byte[] attributesHashFrom(RedisSession session) throws IOException {
27+
HashMap<String,Object> attributes = new HashMap<String,Object>();
28+
for (Enumeration<String> enumerator = session.getAttributeNames(); enumerator.hasMoreElements();) {
29+
String key = enumerator.nextElement();
30+
attributes.put(key, session.getAttribute(key));
31+
}
1932

20-
RedisSession redisSession = (RedisSession) session;
2133
ByteArrayOutputStream bos = new ByteArrayOutputStream();
2234
try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) {
23-
redisSession.writeObjectData(oos);
35+
oos.writeUnshared(attributes);
2436
}
2537

26-
return bos.toByteArray();
38+
MessageDigest digester = null;
39+
try {
40+
digester = MessageDigest.getInstance("MD5");
41+
} catch (NoSuchAlgorithmException e) {
42+
log.error("Unable to get MessageDigest instance for MD5");
43+
}
44+
return digester.digest(bos.toByteArray());
2745
}
2846

2947
@Override
30-
public HttpSession deserializeInto(byte[] data, HttpSession session) throws IOException, ClassNotFoundException {
48+
public byte[] serializeFrom(RedisSession session, SessionSerializationMetadata metadata) throws IOException {
49+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
50+
try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) {
51+
oos.writeObject(metadata);
52+
session.writeObjectData(oos);
53+
}
54+
55+
return bos.toByteArray();
56+
}
3157

32-
RedisSession redisSession = (RedisSession) session;
58+
@Override
59+
public void deserializeInto(byte[] data, RedisSession session, SessionSerializationMetadata metadata) throws IOException, ClassNotFoundException {
3360
try(
3461
BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data));
3562
ObjectInputStream ois = new CustomObjectInputStream(bis, loader);
3663
) {
37-
redisSession.readObjectData(ois);
38-
return session;
64+
SessionSerializationMetadata serializedMetadata = (SessionSerializationMetadata)ois.readObject();
65+
metadata.copyFieldsFrom(serializedMetadata);
66+
session.readObjectData(ois);
3967
}
4068
}
4169
}

src/main/java/com/orangefunction/tomcat/redissessions/RedisSessionManager.java

Lines changed: 106 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ static SessionPersistPolicy fromName(String name) {
6767

6868
protected RedisSessionHandlerValve handlerValve;
6969
protected ThreadLocal<RedisSession> currentSession = new ThreadLocal<>();
70+
protected ThreadLocal<SessionSerializationMetadata> currentSessionSerializationMetadata = new ThreadLocal<>();
7071
protected ThreadLocal<String> currentSessionId = new ThreadLocal<>();
7172
protected ThreadLocal<Boolean> currentSessionIsPersisted = new ThreadLocal<>();
7273
protected Serializer serializer;
@@ -380,6 +381,7 @@ This ensures that the save(session) at the end of the request
380381
currentSession.set(session);
381382
currentSessionId.set(sessionId);
382383
currentSessionIsPersisted.set(false);
384+
currentSessionSerializationMetadata.set(new SessionSerializationMetadata());
383385

384386
if (null != session) {
385387
try {
@@ -417,27 +419,32 @@ public void add(Session session) {
417419

418420
@Override
419421
public Session findSession(String id) throws IOException {
420-
RedisSession session;
422+
RedisSession session = null;
421423

422-
if (id == null) {
423-
session = null;
424+
if (null == id) {
424425
currentSessionIsPersisted.set(false);
426+
currentSession.set(null);
427+
currentSessionSerializationMetadata.set(null);
428+
currentSessionId.set(null);
425429
} else if (id.equals(currentSessionId.get())) {
426430
session = currentSession.get();
427431
} else {
428-
session = loadSessionFromRedis(id);
429-
430-
if (session != null) {
432+
byte[] data = loadSessionDataFromRedis(id);
433+
if (data != null) {
434+
DeserializedSessionContainer container = sessionFromSerializedData(id, data);
435+
session = container.session;
436+
currentSession.set(session);
437+
currentSessionSerializationMetadata.set(container.metadata);
431438
currentSessionIsPersisted.set(true);
439+
currentSessionId.set(id);
432440
} else {
433441
currentSessionIsPersisted.set(false);
434-
id = null;
442+
currentSession.set(null);
443+
currentSessionSerializationMetadata.set(null);
444+
currentSessionId.set(null);
435445
}
436446
}
437447

438-
currentSession.set(session);
439-
currentSessionId.set(id);
440-
441448
return session;
442449
}
443450

@@ -485,9 +492,7 @@ public String[] keys() throws IOException {
485492
}
486493
}
487494

488-
public RedisSession loadSessionFromRedis(String id) throws IOException {
489-
RedisSession session;
490-
495+
public byte[] loadSessionDataFromRedis(String id) throws IOException {
491496
Jedis jedis = null;
492497
Boolean error = true;
493498

@@ -500,41 +505,54 @@ public RedisSession loadSessionFromRedis(String id) throws IOException {
500505

501506
if (data == null) {
502507
log.trace("Session " + id + " not found in Redis");
503-
session = null;
504-
} else {
505-
log.trace("Deserializing session " + id + " from Redis");
506-
session = (RedisSession)createEmptySession();
507-
serializer.deserializeInto(data, session);
508-
session.setId(id);
509-
session.setNew(false);
510-
session.setMaxInactiveInterval(getMaxInactiveInterval() * 1000);
511-
session.access();
512-
session.setValid(true);
513-
session.resetDirtyTracking();
514-
515-
if (log.isTraceEnabled()) {
516-
log.trace("Session Contents [" + id + "]:");
517-
Enumeration en = session.getAttributeNames();
518-
while(en.hasMoreElements()) {
519-
log.trace(" " + en.nextElement());
520-
}
521-
}
522508
}
523509

524-
return session;
525-
} catch (IOException e) {
526-
log.fatal(e.getMessage());
527-
throw e;
528-
} catch (ClassNotFoundException ex) {
529-
log.fatal("Unable to deserialize into session", ex);
530-
throw new IOException("Unable to deserialize into session", ex);
510+
return data;
531511
} finally {
532512
if (jedis != null) {
533513
returnConnection(jedis, error);
534514
}
535515
}
536516
}
537517

518+
public DeserializedSessionContainer sessionFromSerializedData(String id, byte[] data) throws IOException {
519+
log.trace("Deserializing session " + id + " from Redis");
520+
521+
if (Arrays.equals(NULL_SESSION, data)) {
522+
log.error("Encountered serialized session " + id + " with data equal to NULL_SESSION. This is a bug.");
523+
throw new IOException("Serialized session data was equal to NULL_SESSION");
524+
}
525+
526+
RedisSession session = null;
527+
SessionSerializationMetadata metadata = new SessionSerializationMetadata();
528+
529+
try {
530+
session = (RedisSession)createEmptySession();
531+
532+
serializer.deserializeInto(data, session, metadata);
533+
534+
session.setId(id);
535+
session.setNew(false);
536+
session.setMaxInactiveInterval(getMaxInactiveInterval() * 1000);
537+
session.access();
538+
session.setValid(true);
539+
session.resetDirtyTracking();
540+
541+
if (log.isTraceEnabled()) {
542+
log.trace("Session Contents [" + id + "]:");
543+
Enumeration en = session.getAttributeNames();
544+
while(en.hasMoreElements()) {
545+
log.trace(" " + en.nextElement());
546+
}
547+
}
548+
} catch (ClassNotFoundException ex) {
549+
log.fatal("Unable to deserialize into session", ex);
550+
throw new IOException("Unable to deserialize into session", ex);
551+
}
552+
553+
return new DeserializedSessionContainer(session, metadata);
554+
}
555+
538556
public void save(Session session) throws IOException {
539557
save(session, false);
540558
}
@@ -571,17 +589,49 @@ protected boolean saveInternal(Jedis jedis, Session session, boolean forceSave)
571589
}
572590
}
573591

574-
Boolean sessionIsDirty = redisSession.isDirty();
575-
576-
redisSession.resetDirtyTracking();
577592
byte[] binaryId = redisSession.getId().getBytes();
578593

579-
Boolean isCurrentSessionPersisted = this.currentSessionIsPersisted.get();
580-
if (forceSave || sessionIsDirty || (isCurrentSessionPersisted == null || !isCurrentSessionPersisted)) {
581-
jedis.set(binaryId, serializer.serializeFrom(redisSession));
582-
}
594+
Boolean isCurrentSessionPersisted;
595+
SessionSerializationMetadata sessionSerializationMetadata = currentSessionSerializationMetadata.get();
596+
byte[] originalSessionAttributesHash = sessionSerializationMetadata.getSessionAttributesHash();
597+
byte[] sessionAttributesHash = null;
598+
if (
599+
forceSave
600+
|| redisSession.isDirty()
601+
|| null == (isCurrentSessionPersisted = this.currentSessionIsPersisted.get())
602+
|| !isCurrentSessionPersisted
603+
|| !Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.attributesHashFrom(redisSession)))
604+
) {
605+
606+
log.trace("Save was determined to be necessary");
607+
608+
/*if (forceSave) {
609+
log.info("save was necessary: forceSave=true");
610+
} else if (redisSession.isDirty()) {
611+
log.info("save was necessary: isDirty()=true");
612+
} else if (null == (isCurrentSessionPersisted = this.currentSessionIsPersisted.get())) {
613+
log.info("save was necessary: isCurrentSessionPersisted=null");
614+
} else if (!isCurrentSessionPersisted) {
615+
log.info("save was necessary: isCurrentSessionPersisted=false");
616+
} else if (!Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.attributesHashFrom(redisSession)))) {
617+
log.info("save was necessary: sessionsAttributeHashChanged: orig=" + new String(Base64.encodeBase64(originalSessionAttributesHash)) + " new=" + new String(Base64.encodeBase64(sessionAttributesHash)));
618+
}*/
619+
620+
if (null == sessionAttributesHash) {
621+
sessionAttributesHash = serializer.attributesHashFrom(redisSession);
622+
}
583623

584-
currentSessionIsPersisted.set(true);
624+
SessionSerializationMetadata updatedSerializationMetadata = new SessionSerializationMetadata();
625+
updatedSerializationMetadata.setSessionAttributesHash(sessionAttributesHash);
626+
627+
jedis.set(binaryId, serializer.serializeFrom(redisSession, updatedSerializationMetadata));
628+
629+
redisSession.resetDirtyTracking();
630+
currentSessionSerializationMetadata.set(updatedSerializationMetadata);
631+
currentSessionIsPersisted.set(true);
632+
} else {
633+
log.trace("Save was determined to be unnecessary");
634+
}
585635

586636
log.trace("Setting expire timeout on session [" + redisSession.getId() + "] to " + getMaxInactiveInterval());
587637
jedis.expire(binaryId, getMaxInactiveInterval());
@@ -834,3 +884,12 @@ public void setJmxNamePrefix(String jmxNamePrefix) {
834884
this.connectionPoolConfig.setJmxNamePrefix(jmxNamePrefix);
835885
}
836886
}
887+
888+
class DeserializedSessionContainer {
889+
public final RedisSession session;
890+
public final SessionSerializationMetadata metadata;
891+
public DeserializedSessionContainer(RedisSession session, SessionSerializationMetadata metadata) {
892+
this.session = session;
893+
this.metadata = metadata;
894+
}
895+
}

src/main/java/com/orangefunction/tomcat/redissessions/Serializer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
public interface Serializer {
77
void setClassLoader(ClassLoader loader);
88

9-
byte[] serializeFrom(HttpSession session) throws IOException;
10-
11-
HttpSession deserializeInto(byte[] data, HttpSession session) throws IOException, ClassNotFoundException;
9+
byte[] attributesHashFrom(RedisSession session) throws IOException;
10+
byte[] serializeFrom(RedisSession session, SessionSerializationMetadata metadata) throws IOException;
11+
void deserializeInto(byte[] data, RedisSession session, SessionSerializationMetadata metadata) throws IOException, ClassNotFoundException;
1212
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.orangefunction.tomcat.redissessions;
2+
3+
import java.io.*;
4+
5+
6+
public class SessionSerializationMetadata implements Serializable {
7+
8+
private byte[] sessionAttributesHash;
9+
10+
public SessionSerializationMetadata() {
11+
this.sessionAttributesHash = new byte[0];
12+
}
13+
14+
public byte[] getSessionAttributesHash() {
15+
return sessionAttributesHash;
16+
}
17+
18+
public void setSessionAttributesHash(byte[] sessionAttributesHash) {
19+
this.sessionAttributesHash = sessionAttributesHash;
20+
}
21+
22+
public void copyFieldsFrom(SessionSerializationMetadata metadata) {
23+
this.setSessionAttributesHash(metadata.getSessionAttributesHash());
24+
}
25+
26+
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
27+
out.writeInt(sessionAttributesHash.length);
28+
out.write(this.sessionAttributesHash);
29+
}
30+
31+
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
32+
int hashLength = in.readInt();
33+
byte[] sessionAttributesHash = new byte[hashLength];
34+
in.read(sessionAttributesHash, 0, hashLength);
35+
this.sessionAttributesHash = sessionAttributesHash;
36+
}
37+
38+
private void readObjectNoData() throws ObjectStreamException {
39+
this.sessionAttributesHash = new byte[0];
40+
}
41+
42+
}

0 commit comments

Comments
 (0)