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