diff --git a/src/main/java/org/appng/tomcat/session/mongo/MongoSessionManager.java b/src/main/java/org/appng/tomcat/session/mongo/MongoSessionManager.java
index 9897b93..6114c24 100644
--- a/src/main/java/org/appng/tomcat/session/mongo/MongoSessionManager.java
+++ b/src/main/java/org/appng/tomcat/session/mongo/MongoSessionManager.java
@@ -23,7 +23,7 @@
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
-import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
@@ -36,6 +36,7 @@
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
+import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
@@ -45,12 +46,18 @@
import com.mongodb.ReadPreference;
import com.mongodb.ServerAddress;
import com.mongodb.WriteConcern;
-import com.mongodb.WriteResult;
/**
- * A {@link SessionManager} implementation that uses a {@link MongoClient}
+ * A {@link SessionManager} implementation that uses a {@link MongoClient}.
+ *
IMPORTANT
+ *
+ * This implementation currently does not handle the case of a site-reload!
+ * Locally cached sessions must be removed in that case to avoid classloader issues!
+ * One possible solution is to set {@code sticky=false} which forces the session to be removed from the local cache
+ * after each request.
+ *
*/
-public final class MongoSessionManager extends SessionManager {
+public class MongoSessionManager extends SessionManager {
private final Log log = Utils.getLog(MongoSessionManager.class);
@@ -59,15 +66,9 @@ public final class MongoSessionManager extends SessionManager {
/** Property used to store the Session's ID */
private static final String PROP_ID = "session_id";
- /** Property used to store the Session's context name */
- protected static final String PROP_CONTEXT = "app";
-
/** Property used to store the Session's last modified date. */
protected static final String PROP_LAST_MODIFIED = "lastModified";
- /** Property used to store the Session's creation date. */
- protected static final String PROP_CREATION_TIME = "creationTime";
-
/** Mongo Collection for the Sessions */
protected DBCollection collection;
@@ -188,6 +189,7 @@ protected void startInternal() throws LifecycleException {
}
log.info(String.format("Using Database [%s]", this.dbName));
+ @SuppressWarnings("deprecation")
DB db = this.mongoClient.getDB(this.dbName);
this.collection = db.getCollection(this.collectionName);
log.info(String.format("Preparing indexes"));
@@ -195,13 +197,11 @@ protected void startInternal() throws LifecycleException {
BasicDBObject lastModifiedIndex = new BasicDBObject(PROP_LAST_MODIFIED, 1);
try {
this.collection.dropIndex(lastModifiedIndex);
- this.collection.dropIndex(new BasicDBObject(PROP_CONTEXT, 1));
} catch (Exception e) {
/* these indexes may not exist, so ignore */
}
this.collection.createIndex(lastModifiedIndex);
- this.collection.createIndex(new BasicDBObject(PROP_CONTEXT, 1));
log.info(String.format("[%s]: Store ready.", this.getName()));
} catch (MongoException me) {
log.error("Unable to Connect to MongoDB", me);
@@ -220,43 +220,38 @@ protected void stopInternal() throws LifecycleException {
protected SessionData findSessionInternal(String id) throws IOException {
DBObject mongoSession = this.collection.findOne(sessionQuery(id));
if (null != mongoSession) {
- byte[] sessionData = (byte[]) mongoSession.get(PROP_SESSIONDATA);
- try (ByteArrayInputStream bais = new ByteArrayInputStream(sessionData);
- ObjectInputStream ois = new ObjectInputStream(bais)) {
- return (SessionData) ois.readObject();
- } catch (ReflectiveOperationException roe) {
- log.error(String.format("Error loading session: %s", id), roe);
- throw new IOException(roe);
- }
+ return loadSession(id, mongoSession);
} else {
log.warn(String.format("Session not found: %s, returning null!", id));
}
return null;
}
+ private SessionData loadSession(String id, DBObject mongoSession) throws IOException {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream((byte[]) mongoSession.get(PROP_SESSIONDATA));
+ ObjectInputStream ois = new ObjectInputStream(bais)) {
+ return (SessionData) ois.readObject();
+ } catch (ReflectiveOperationException roe) {
+ log.error(String.format("Error loading session: %s", id), roe);
+ throw new IOException(roe);
+ }
+ }
+
private BasicDBObject sessionQuery(String id) {
- return new BasicDBObject(PROP_ID, id).append(PROP_CONTEXT, this.getName());
+ return new BasicDBObject(PROP_ID, id);
}
@Override
protected void updateSession(String id, SessionData sessionData) throws IOException {
- long start = System.nanoTime();
-
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
-
oos.writeObject(sessionData);
-
BasicDBObject sessionQuery = sessionQuery(sessionData.getId());
BasicDBObject mongoSession = (BasicDBObject) sessionQuery.copy();
mongoSession.put(PROP_SESSIONDATA, bos.toByteArray());
mongoSession.put(PROP_LAST_MODIFIED, Calendar.getInstance().getTime());
- WriteResult updated = this.collection.update(sessionQuery, mongoSession, true, false);
- log.debug(String.format(Locale.ENGLISH,
- "Saved session %s with query %s in %.2fms (lastModified %s) acknowledged: %s", sessionData.getId(),
- sessionQuery, getDuration(start), mongoSession.getDate(PROP_LAST_MODIFIED),
- updated.wasAcknowledged()));
+ this.collection.update(sessionQuery, mongoSession, true, false);
} catch (MongoException | IOException e) {
log.warn(String.format("Error saving session: %s", sessionData.getId()));
throw e;
@@ -269,16 +264,38 @@ public void removeInternal(Session session) {
BasicDBObject sessionQuery = sessionQuery(id);
try {
this.collection.remove(sessionQuery);
- log.debug(String.format("removed session %s (query: %s)", id, sessionQuery));
+ log.debug(String.format("%s has been removed (query: %s)", id, sessionQuery));
} catch (MongoException e) {
log.error("Unable to remove sessions for [" + id + ":" + this.getName() + "] from MongoDB", e);
throw e;
}
}
-
- //@Override
- protected int expireInternal() {
- return 0;
+
+ @Override
+ public void processExpires() {
+ long timeNow = System.currentTimeMillis();
+ DBCursor allSessions = this.collection.find(new BasicDBObject());
+ int size = allSessions.size();
+ log.debug(String.format("Checking expiry for %s sessions.", size));
+ AtomicInteger count = new AtomicInteger(0);
+ while (allSessions.hasNext()) {
+ DBObject mongoSession = allSessions.next();
+ String id = (String) mongoSession.get(PROP_ID);
+ try {
+ if (expireInternal(id, loadSession(id, mongoSession))) {
+ count.incrementAndGet();
+ }
+ } catch (Throwable t) {
+ log.error(String.format("Error expiring session %s", id), t);
+ }
+ }
+ long timeEnd = System.currentTimeMillis();
+ long duration = timeEnd - timeNow;
+ processingTime += duration;
+ if (log.isInfoEnabled()) {
+ log.info(String.format("Expired %s of %s sessions in %sms", count, size, duration));
+ }
+ super.processExpires();
}
public void setCollectionName(String collectionName) {
@@ -297,4 +314,9 @@ public void setDbName(String dbName) {
protected DBCollection getPersistentSessions() {
return collection;
}
+
+ // for testing
+ protected void clearAll() {
+ sessions.clear();
+ }
}
diff --git a/src/test/java/org/appng/tomcat/session/mongo/MongoSessionManagerIT.java b/src/test/java/org/appng/tomcat/session/mongo/MongoSessionManagerIT.java
index 298eafa..067c1a7 100644
--- a/src/test/java/org/appng/tomcat/session/mongo/MongoSessionManagerIT.java
+++ b/src/test/java/org/appng/tomcat/session/mongo/MongoSessionManagerIT.java
@@ -15,11 +15,12 @@
*/
package org.appng.tomcat.session.mongo;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -39,14 +40,18 @@
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
import org.junit.Test;
+import org.junit.runners.MethodSorters;
import org.testcontainers.containers.MongoDBContainer;
+import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
/**
* Integration test for {@link MongoSessionManager}
*/
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class MongoSessionManagerIT {
static MongoSessionManager manager;
@@ -54,54 +59,133 @@ public class MongoSessionManagerIT {
static MongoDBContainer mongo;
@Test
- public void test() throws Exception {
-
+ public void testSessionExpired() throws Exception {
+ manager.setSessionSaveIntervalSeconds(2);
Session session = manager.createEmptySession();
+ session.setCreationTime(System.currentTimeMillis());
+ session.setMaxInactiveInterval(6);
session.setId("4711");
session.setNew(true);
session.setValid(true);
- session.setCreationTime(System.currentTimeMillis());
+ session.setAttribute("foo", "bar");
+ Assert.assertTrue(manager.commit(session));
+ Thread.sleep(3000);
+ Assert.assertTrue(manager.commit(session));
+ Thread.sleep(7000);
+
+ SessionData serialized = session.serialize("appng");
+ Session created = Session.load(manager, serialized);
+ Assert.assertNull(created);
+ }
+
+ @Test
+ public void test() throws Exception {
+ Session session = createSession();
int checkSum1 = session.checksum();
+ Map map = modifySession(session);
+ int checkSum2 = assertSessionChanged(session, checkSum1, false);
+ SessionData original = session.serialize();
+ int checksum3 = original.checksum();
+ Assert.assertEquals(checkSum2, checksum3);
+ // --- basic tests
+
+ modifySession(session, map);
+
+ long accessedBefore = session.getThisAccessedTimeInternal();
+ Session loaded = manager.findSession(session.getId());
+ Assert.assertEquals(session, loaded);
+ long accessedAfter = session.getThisAccessedTimeInternal();
+ Assert.assertNotEquals(accessedBefore, accessedAfter);
+
+ Assert.assertEquals(1, manager.getActiveSessions());
+ manager.removeLocal(session);
+ Assert.assertEquals(0, manager.getActiveSessions());
+
+ Assert.assertFalse(Site.calledClassloader);
+ loaded = manager.findSession(session.getId());
+ Assert.assertTrue(Site.calledClassloader);
+ Assert.assertEquals(1, manager.getActiveSessions());
+ Assert.assertNotEquals(session, loaded);
+
+ Assert.assertFalse(loaded.isDirty());
+ Assert.assertEquals("test", session.getAttribute("foo"));
+ session.setAttribute("he", "ho");
+ Assert.assertNotNull(manager.getSession(session.getId()));
+ Assert.assertTrue(manager.commit(session));
+ Assert.assertNotNull(manager.getSession(session.getId()));
+
+ manager.remove(session);
+ }
+
+ public void modifySession(Session session, Map map) throws IOException {
+ int oldChecksum = session.checksum();
+ map.put("foo", "test");
+ SessionData modified = session.serialize();
+ int checksum = modified.checksum();
+ Assert.assertNotEquals(oldChecksum, checksum);
+ Assert.assertTrue(manager.commit(session));
+ }
+ private Session createSession() {
+ Session session = manager.createEmptySession();
+ session.setId("4711");
+ session.setNew(true);
+ session.setValid(true);
+ session.setCreationTime(System.currentTimeMillis());
Assert.assertTrue(session.isNew());
Assert.assertFalse(session.isDirty());
+ return session;
+ }
+
+ private Map modifySession(Session session) {
session.setAttribute("foo", "test");
- ConcurrentMap map = new ConcurrentHashMap<>();
+ Map map = new HashMap<>();
session.setAttribute("amap", map);
session.setAttribute("metaData", new MetaData());
+ return map;
+ }
- int checkSum2 = session.checksum();
- Assert.assertNotEquals(checkSum1, checkSum2);
-
+ private int assertSessionChanged(Session session, int oldCheckSum, boolean isCommitted) throws IOException {
+ int checksum = session.checksum();
+ Assert.assertNotEquals(oldCheckSum, checksum);
Assert.assertTrue(session.isDirty());
Assert.assertTrue(manager.commit(session));
Assert.assertFalse(session.isNew());
Assert.assertFalse(session.isDirty());
- Assert.assertFalse(manager.commit(session));
+ Assert.assertEquals(isCommitted, manager.commit(session));
+ return checksum;
+ }
+ @Test
+ public void testNonSticky() throws Exception {
+ manager.getPersistentSessions().remove(new BasicDBObject());
+ manager.clearAll();
+ manager.setSticky(false);
+ Session session = createSession();
+ int checkSum1 = session.checksum();
+ Map map = modifySession(session);
+ int checkSum2 = assertSessionChanged(session, checkSum1, true);
SessionData original = session.serialize();
int checksum3 = original.checksum();
-
Assert.assertEquals(checkSum2, checksum3);
+ // ---------- same like normal test
+ manager.removeLocal(session);
- map.put("foo", "test");
- SessionData modified = session.serialize();
- int checksum4 = modified.checksum();
- Assert.assertNotEquals(checksum3, checksum4);
- Assert.assertTrue(manager.commit(session));
+ modifySession(session, map);
long accessedBefore = session.getThisAccessedTimeInternal();
Session loaded = manager.findSession(session.getId());
- Assert.assertEquals(session, loaded);
- long accessedAfter = session.getThisAccessedTimeInternal();
+
+ Assert.assertNotEquals(session, loaded);
+ long accessedAfter = loaded.getThisAccessedTimeInternal();
Assert.assertNotEquals(accessedBefore, accessedAfter);
Assert.assertEquals(1, manager.getActiveSessions());
manager.removeLocal(session);
Assert.assertEquals(0, manager.getActiveSessions());
- Assert.assertFalse(Site.calledClassloader);
+ Assert.assertTrue(Site.calledClassloader);
loaded = manager.findSession(session.getId());
Assert.assertTrue(Site.calledClassloader);
Assert.assertEquals(1, manager.getActiveSessions());
@@ -110,7 +194,11 @@ public void test() throws Exception {
Assert.assertFalse(loaded.isDirty());
Assert.assertEquals("test", session.getAttribute("foo"));
session.setAttribute("he", "ho");
- manager.commit(session);
+ Assert.assertNotNull(manager.getSession(session.getId()));
+ Assert.assertTrue(manager.commit(session));
+ Assert.assertNotNull(manager.getSession(session.getId()));
+ manager.removeLocal(session);
+ Assert.assertNull(manager.getSession(session.getId()));
manager.remove(session);
}
@@ -132,7 +220,8 @@ public void sessionCreated(HttpSessionEvent se) {
});
Session session = null;
- int numSessions = 50;
+ final int numSessions = 500;
+ final int halfSessions = numSessions/2;
for (int i = 0; i < numSessions; i++) {
Session s = manager.createSession(null);
s.setMaxInactiveInterval((i % 2 == 0 ? 1 : 3600));
@@ -140,27 +229,27 @@ public void sessionCreated(HttpSessionEvent se) {
s.setAttribute("metaData", new MetaData());
if (0 == i) {
session = s;
+ Assert.assertTrue(session.isNew());
}
+ manager.commit(s);
}
- Assert.assertTrue(session.isNew());
DBCollection persistentSessions = manager.getPersistentSessions();
- Assert.assertTrue(session.isNew());
- Assert.assertEquals(0L, persistentSessions.count());
+ Assert.assertEquals(numSessions, persistentSessions.count());
Assert.assertEquals(numSessions, manager.getActiveSessions());
manager.commit(session);
- Assert.assertEquals(1L, persistentSessions.count());
+ Assert.assertEquals(numSessions, persistentSessions.count());
Assert.assertEquals(numSessions, manager.getActiveSessions());
Assert.assertFalse(session.isNew());
TimeUnit.SECONDS.sleep(2);
manager.processExpires();
- Assert.assertEquals(25L, manager.getExpiredSessions());
- Assert.assertEquals(0L, persistentSessions.count());
+ Assert.assertEquals(halfSessions, manager.getExpiredSessions());
+ Assert.assertEquals(halfSessions, persistentSessions.count());
Assert.assertNull(manager.findSession(session.getId()));
- long activeSessions = numSessions / 2;
+ int activeSessions = numSessions / 2;
Assert.assertEquals(activeSessions, manager.getActiveSessions());
Assert.assertEquals(numSessions, created.get());
Assert.assertEquals(activeSessions, destroyed.get());
@@ -170,12 +259,17 @@ public void sessionCreated(HttpSessionEvent se) {
}
Assert.assertEquals(activeSessions, persistentSessions.count());
- }
- @AfterClass
- public static void shutdown() {
- mongo.stop();
- mongo.close();
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);) {
+ objectOutputStream.writeObject("appNG");
+ objectOutputStream.writeObject("clear it!");
+// manager.clearSessionsOnEvent = Arrays.asList("java.lang.String");
+// manager.getTopic().publish(out.toByteArray());
+ }
+ Thread.sleep(100);
+ //Assert.assertEquals(0, manager.getActiveSessions());
+
}
@BeforeClass