Add in idle expiry of sessions silently in the cache; modify the LightLoadTest so that it is clear it is NOT testing expected behaviour of any jetty session impl; add better load test for a single node.
diff --git a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionIdManager.java b/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionIdManager.java
index 257f6ba..7ca6a58 100644
--- a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionIdManager.java
+++ b/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionIdManager.java
@@ -19,6 +19,7 @@
 package org.eclipse.jetty.session.infinispan;
 
 import java.util.Random;
+import java.util.concurrent.TimeUnit;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
@@ -52,18 +53,25 @@
  * </pre>
  * where [id] is the id of the session.
  * 
+ * If the first session to be added is not immortal (ie it has a timeout on it) then
+ * the corresponding session id is entered into infinispan with an idle expiry timeout
+ * equivalent to double the session's timeout (the multiplier is configurable).
+ * 
+ * 
  * Having one entry per in-use session id means that there is no contention on
  * cache entries (as would be the case if a single entry was kept containing a 
  * list of in-use session ids).
  * 
- * TODO synchronization
+ * 
  */
 public class InfinispanSessionIdManager extends AbstractSessionIdManager
 {
     private  final static Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
-    protected final static String ID_KEY = "__o.e.j.s.infinispanIdMgr__";
+    public final static String ID_KEY = "__o.e.j.s.infinispanIdMgr__";
+    public static final int DEFAULT_IDLE_EXPIRY_MULTIPLE = 2;
     protected BasicCache<String,Object> _cache;
     private Server _server;
+    private int _idleExpiryMultiple = DEFAULT_IDLE_EXPIRY_MULTIPLE;
 
     
     
@@ -132,7 +140,8 @@
         
         String clusterId = getClusterId(id);
         
-        //ask the cluster
+        //ask the cluster - this should also tickle the idle expiration timer on the sessionid entry
+        //keeping it valid
         try
         {
             return exists(clusterId);
@@ -155,13 +164,35 @@
     @Override
     public void addSession(HttpSession session)
     {
-       if (session == null)
-           return;
+        if (session == null)
+            return;
+
+        //insert into the cache and set an idle expiry on the entry that
+        //is based off the max idle time configured for the session. If the
+        //session is immortal, then there is no idle expiry on the corresponding
+        //session id
+        if (session.getMaxInactiveInterval() == 0)
+            insert (((AbstractSession)session).getClusterId());
+        else
+            insert (((AbstractSession)session).getClusterId(), session.getMaxInactiveInterval() * getIdleExpiryMultiple());
+    }
     
-       //insert into the cache
-       insert (((AbstractSession)session).getClusterId());
+    
+    public void setIdleExpiryMultiple (int multiplier)
+    {
+        if (multiplier <= 1)
+        {
+            LOG.warn("Idle expiry multiple of {} for session ids set to less than minimum. Using value of {} instead.", multiplier, DEFAULT_IDLE_EXPIRY_MULTIPLE);
+        }
+        _idleExpiryMultiple = multiplier;
     }
 
+    public int getIdleExpiryMultiple ()
+    {
+        return _idleExpiryMultiple;
+    }
+    
+    
     /** 
      * Remove a session id from the list of in-use ids.
      * 
@@ -246,16 +277,38 @@
 
     }
 
+    /**
+     * Get the cache.
+     * @return
+     */
     public BasicCache<String,Object> getCache() 
     {
         return _cache;
     }
 
+    /**
+     * Set the cache.
+     * @param cache
+     */
     public void setCache(BasicCache<String,Object> cache) 
     {
         this._cache = cache;
     }
     
+    
+    
+    /**
+     * Do any operation to the session id in the cache to
+     * ensure its idle expiry time moves forward
+     * @param id
+     */
+    public void touch (String id)
+    {
+        exists(id);
+    }
+    
+    
+    
     /**
      * Ask the cluster if a particular id exists.
      * 
@@ -267,10 +320,7 @@
         if (_cache == null)
             throw new IllegalStateException ("No cache");
         
-        Object key =_cache.get(makeKey(id));
-        if (key == null)
-            return false;
-        return true;
+        return _cache.containsKey(makeKey(id));
     }
     
 
@@ -288,6 +338,19 @@
     }
     
     
+    /**
+     * Put a session id into the cluster with an idle expiry.
+     * 
+     * @param id
+     */
+    protected void insert (String id, long idleTimeOutSec)
+    {
+        if (_cache == null)
+            throw new IllegalStateException ("No cache");
+        
+        _cache.putIfAbsent(makeKey(id),id,-1L, TimeUnit.SECONDS, idleTimeOutSec, TimeUnit.SECONDS);
+    }
+   
     
     /**
      * Remove a session id from the cluster.
diff --git a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionManager.java b/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionManager.java
index 45778f3..6e79a2a 100644
--- a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionManager.java
+++ b/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionManager.java
@@ -208,102 +208,6 @@
     
     
     /**
-     * SerializableSession
-     *
-     * Helper class that is responsible for de/serialization of the non-serializable session object.
-     */
-    public class SerializableSession implements Serializable
-    {
-        
-        /**
-         * 
-         */
-        private static final long serialVersionUID = -7603529353470249059L;
-        private transient Session _session;
-
-        
-        public SerializableSession ()
-        {
-            
-        }
-        
-        public SerializableSession (Session session)
-        {
-            setSession(session);
-        }
-        
-        /**
-         * Existing session
-         * @param session
-         */
-        public void setSession (Session session)
-        {
-            _session = session;
-        }
-        
-        public Session getSession ()
-        {
-            return _session;
-        }
-   
-        
-        private void writeObject(java.io.ObjectOutputStream out) throws IOException
-        {
-            if (_session == null)
-                throw new IOException ("No session to serialize");
-  
-            out.writeUTF(_session.getClusterId()); //session id
-            out.writeUTF(_session.getContextPath()); //context path
-            out.writeUTF(_session.getVHost()); //first vhost
-
-            out.writeLong(_session.getAccessed());//accessTime
-            out.writeLong(_session.getLastAccessedTime()); //lastAccessTime
-            out.writeLong(_session.getCreationTime()); //time created
-            out.writeLong(_session.getCookieSetTime());//time cookie was set
-            out.writeUTF(_session.getLastNode()); //name of last node managing
-      
-            out.writeLong(_session.getExpiry()); 
-            out.writeLong(_session.getMaxInactiveInterval());
-            out.writeObject(_session.getAttributeMap());
-        }
-        
-        
-        private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
-        {
-            String clusterId = in.readUTF();
-            String context = in.readUTF();
-            String vhost = in.readUTF();
-            
-            Long accessed = in.readLong();//accessTime
-            Long lastAccessed = in.readLong(); //lastAccessTime
-            Long created = in.readLong(); //time created
-            Long cookieSet = in.readLong();//time cookie was set
-            String lastNode = in.readUTF(); //last managing node
-            Long expiry = in.readLong(); 
-            Long maxIdle = in.readLong();
-            HashMap<String,Object> attributes = (HashMap<String,Object>)in.readObject();
-            Session session = new Session(clusterId, created, accessed, maxIdle);
-            session.setCookieSetTime(cookieSet);
-            session.setLastAccessedTime(lastAccessed);
-            session.setLastNode(lastNode);
-            session.setContextPath(context);
-            session.setVHost(vhost);
-            session.setExpiry(expiry);
-            session.addAttributes(attributes);
-            setSession(session);
-        }
-        
-        
-        private void readObjectNoData() throws ObjectStreamException
-        {
-            setSession(null);
-        }
-
-        
-    }
-    
-    
-    /**
      * Session
      *
      * Representation of a session in local memory.
@@ -338,12 +242,24 @@
         private String _lastNode;
         
         
+        /**
+         * If dirty, session needs to be (re)sent to cluster
+         */
+        protected boolean _dirty=false;
         
+        
+     
 
         /**
          * Any virtual hosts for the context with which this session is associated
          */
         private String _vhost;
+
+        
+        /**
+         * Count of how many threads are active in this session
+         */
+        private AtomicInteger _activeThreads = new AtomicInteger(0);
         
         
         
@@ -361,6 +277,7 @@
             _lastNode = getSessionIdManager().getWorkerName();
            setVHost(InfinispanSessionManager.getVirtualHost(_context));
            setContextPath(InfinispanSessionManager.getContextPath(_context));
+           _activeThreads.incrementAndGet(); //access will not be called on a freshly created session so increment here
         }
         
         
@@ -403,14 +320,18 @@
             {
 
                 long now = System.currentTimeMillis();
+                //lock so that no other thread can call access or complete until the first one has refreshed the session object if necessary
                 _lock.lock();
-
-                //if the first thread, check that the session in memory is not stale, if we're checking for stale sessions
-                if (getStaleIntervalSec() > 0  && (now - getLastSyncTime()) >= (getStaleIntervalSec() * 1000L))
+                //a request thread is entering
+                if (_activeThreads.incrementAndGet() == 1)
                 {
-                    if (LOG.isDebugEnabled())
-                        LOG.debug("Acess session({}) for context {} on worker {} stale session. Reloading.", getId(), getContextPath(), getSessionIdManager().getWorkerName());
-                    refresh();
+                    //if the first thread, check that the session in memory is not stale, if we're checking for stale sessions
+                    if (getStaleIntervalSec() > 0  && (now - getLastSyncTime()) >= (getStaleIntervalSec() * 1000L))
+                    {
+                        if (LOG.isDebugEnabled())
+                            LOG.debug("Acess session({}) for context {} on worker {} stale session. Reloading.", getId(), getContextPath(), getSessionIdManager().getWorkerName());
+                        refresh();
+                    }
                 }
             }
             catch (Exception e)
@@ -441,32 +362,65 @@
         {
             super.complete();
 
+            //lock so that no other thread that might be calling access can proceed until this complete is done
+            _lock.lock();
+
             try
             {
-                //an invalid session will already have been removed from the
-                //local session map and deleted from the cluster. If its valid save
-                //it to the cluster.
-                //TODO consider doing only periodic saves if only the last access
-                //time to the session changes
-                if (isValid())
+                //if this is the last request thread to be in the session
+                if (_activeThreads.decrementAndGet() == 0)
                 {
-                    willPassivate();
                     try
                     {
-                        _lock.lock();
-                        save(this);
+                        //an invalid session will already have been removed from the
+                        //local session map and deleted from the cluster. If its valid save
+                        //it to the cluster.
+                        //TODO consider doing only periodic saves if only the last access
+                        //time to the session changes
+                        if (isValid())
+                        {
+                            //if session still valid && its dirty or stale or never been synced, write it to the cluster
+                            //otherwise, we just keep the updated last access time in memory
+                            if (_dirty || getLastSyncTime() == 0 || isStale(System.currentTimeMillis()))
+                            {
+                                willPassivate();
+                                save(this);
+                                didActivate();
+                            }
+                        }
                     }
+                    catch (Exception e)
+                    {
+                        LOG.warn("Problem saving session({})",getId(), e);
+                    } 
                     finally
                     {
-                        _lock.unlock();
+                        _dirty = false;
                     }
-                    didActivate();
                 }
             }
-            catch (Exception e)
+            finally
             {
-                LOG.warn("Problem saving session({})",getId(), e);
-            } 
+                _lock.unlock();
+            }
+        }
+        
+        /** Test if the session is stale
+         * @param atTime
+         * @return
+         */
+        protected boolean isStale (long atTime)
+        {
+            return (getStaleIntervalSec() > 0) && (atTime - getLastSyncTime() >= (getStaleIntervalSec()*1000L));
+        }
+        
+        
+        /** Test if the session is dirty
+         * @return
+         */
+        protected boolean isDirty ()
+        {
+            return _dirty;
         }
 
         /** 
@@ -480,6 +434,8 @@
             super.timeout();
         }
         
+      
+        
         /**
          * Reload the session from the cluster. If the node that
          * last managed the session from the cluster is ourself,
@@ -561,6 +517,7 @@
         
         public void swapId (String newId, String newNodeId)
         {
+            //TODO probably synchronize rather than use the access/complete lock?
             _lock.lock();
             setClusterId(newId);
             setNodeId(newNodeId);
@@ -574,7 +531,7 @@
             if (value == null && old == null)
                 return; //if same as remove attribute but attribute was already removed, no change
             
-           //TODO _dirty = true;
+           _dirty = true;
         }
         
         
@@ -716,7 +673,8 @@
             if (candidateSession != null)
             {
                 //double check the state of the session in the cache, as the
-                //session may have migrated to another node
+                //session may have migrated to another node. This leaves a window
+                //where the cached session may have been changed by another node
                 Session cachedSession = load(makeKey(candidateId, _context));
                 if (cachedSession == null)
                 {
@@ -752,8 +710,7 @@
     
     
     /**
-     * Set the interval between runs of the scavenger. As this will be a costly
-     * exercise (need to iterate over all cache entries) it should not be run too
+     * Set the interval between runs of the scavenger. It should not be run too
      * often.
      * 
      * 
@@ -951,8 +908,28 @@
     @Override
     protected void shutdownSessions() throws Exception
     {
-        //TODO if implementing period saves, if we might have un-saved changes, 
-        //then we need to write them back to the clustered cache
+        Set<String> keys = new HashSet<String>(_sessions.keySet());
+        for (String key:keys)
+        {
+            Session session = _sessions.remove(key); //take the session out of the session list
+            //If the session is dirty, then write it to the cluster.
+            //If the session is simply stale do NOT write it to the cluster, as some other node
+            //may have started managing that session - this means that the last accessed/expiry time
+            //will not be updated, meaning it may look like it can expire sooner than it should.
+            try
+            {
+                if (session.isDirty())
+                {
+                    if (LOG.isDebugEnabled())
+                        LOG.debug("Saving dirty session {} before exiting ", session.getId());
+                    save(session);
+                }
+            }
+            catch (Exception e)
+            {
+                LOG.warn(e);
+            }
+        }
     }
 
 
@@ -1057,7 +1034,20 @@
         if (LOG.isDebugEnabled()) LOG.debug("Writing session {} to cluster", session.getId());
     
         SerializableSessionData storableSession = new SerializableSessionData(session);
-        _cache.put(makeKey(session, _context), storableSession);
+
+        //Put an idle timeout on the cache entry if the session is not immortal - 
+        //if no requests arrive at any node before this timeout occurs, or no node 
+        //scavenges the session before this timeout occurs, the session will be removed.
+        //NOTE: that no session listeners can be called for this.
+        InfinispanSessionIdManager sessionIdManager = (InfinispanSessionIdManager)getSessionIdManager();
+        if (storableSession.maxInactive > 0)
+            _cache.put(makeKey(session, _context), storableSession, -1, TimeUnit.SECONDS, storableSession.maxInactive*sessionIdManager.getIdleExpiryMultiple(), TimeUnit.SECONDS);
+        else
+            _cache.put(makeKey(session, _context), storableSession);
+        
+        //tickle the session id manager to keep the sessionid entry for this session up-to-date
+        sessionIdManager.touch(session.getClusterId());
+        
         session.setLastSyncTime(System.currentTimeMillis());
     }
     
diff --git a/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/LightLoadTest.java b/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/ScatterGunLoadTest.java
similarity index 92%
rename from tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/LightLoadTest.java
rename to tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/ScatterGunLoadTest.java
index acff356..90e428c 100644
--- a/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/LightLoadTest.java
+++ b/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/ScatterGunLoadTest.java
@@ -21,9 +21,9 @@
 import org.junit.Test;
 
 /**
- * LightLoadTest
+ * ScatterGunLoadTest
  */
-public class LightLoadTest extends AbstractLightLoadTest
+public class ScatterGunLoadTest extends AbstractScatterGunLoadTest
 {
 
     public AbstractTestServer createServer(int port)
diff --git a/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/InfinispanTestSessionServer.java b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/InfinispanTestSessionServer.java
index 8d3479c..c6b2122 100644
--- a/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/InfinispanTestSessionServer.java
+++ b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/InfinispanTestSessionServer.java
@@ -76,14 +76,34 @@
         return new SessionHandler(sessionManager);
     }
 
+    public boolean exists (String id)
+    {
+        BasicCache cache = ((InfinispanSessionIdManager)_sessionIdManager).getCache();
+        if (cache != null)
+        {
+            return cache.containsKey(id);      
+        }
+        
+        return false;
+    }
+    
+    public Object get (String id)
+    {
+        BasicCache cache = ((InfinispanSessionIdManager)_sessionIdManager).getCache();
+        if (cache != null)
+        {
+            return cache.get(id);      
+        }
+        
+        return null;
+    }
 
     public void dumpCache ()
     {
         BasicCache cache = ((InfinispanSessionIdManager)_sessionIdManager).getCache();
         if (cache != null)
         {
-            System.err.println(cache.getName()+" contains "+cache.size()+" entries");
-            
+            System.err.println(cache.getName()+" contains "+cache.size()+" entries");         
         }
     }
 
diff --git a/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java
new file mode 100644
index 0000000..44a9c17
--- /dev/null
+++ b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/SameNodeLoadTest.java
@@ -0,0 +1,66 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+
+package org.eclipse.jetty.server.session;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+/**
+ * SameNodeLoadTest
+ *
+ *
+ */
+public class SameNodeLoadTest extends AbstractSameNodeLoadTest
+{
+
+    public static InfinispanTestSupport __testSupport;
+
+    
+    
+    @BeforeClass
+    public static void setup () throws Exception
+    {
+        __testSupport = new InfinispanTestSupport();
+        __testSupport.setup();
+    }
+    
+    @AfterClass
+    public static void teardown () throws Exception
+    {
+       __testSupport.teardown();
+    }
+    
+    /** 
+     * @see org.eclipse.jetty.server.session.AbstractSameNodeLoadTest#createServer(int)
+     */
+    @Override
+    public AbstractTestServer createServer(int port)
+    {
+        InfinispanTestSessionServer server = new InfinispanTestSessionServer(port, __testSupport.getCache());
+        return server;
+    }
+
+    @Override
+    public void testLoad() throws Exception
+    {
+        super.testLoad();
+    }
+
+}
diff --git a/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/remote/RemoteSameNodeLoadTest.java b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/remote/RemoteSameNodeLoadTest.java
new file mode 100644
index 0000000..ea98f0f
--- /dev/null
+++ b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/remote/RemoteSameNodeLoadTest.java
@@ -0,0 +1,69 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+
+package org.eclipse.jetty.server.session.remote;
+
+import org.eclipse.jetty.server.session.AbstractSameNodeLoadTest;
+import org.eclipse.jetty.server.session.AbstractTestServer;
+import org.eclipse.jetty.server.session.InfinispanTestSessionServer;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+/**
+ * SameNodeLoadTest
+ *
+ *
+ */
+public class RemoteSameNodeLoadTest extends AbstractSameNodeLoadTest
+{
+
+    public static RemoteInfinispanTestSupport __testSupport;
+
+    
+    
+    @BeforeClass
+    public static void setup () throws Exception
+    {
+        __testSupport = new RemoteInfinispanTestSupport("remote-session-test");
+        __testSupport.setup();
+    }
+    
+    @AfterClass
+    public static void teardown () throws Exception
+    {
+       __testSupport.teardown();
+    }
+    
+    /** 
+     * @see org.eclipse.jetty.server.session.AbstractSameNodeLoadTest#createServer(int)
+     */
+    @Override
+    public AbstractTestServer createServer(int port)
+    {
+        InfinispanTestSessionServer server = new InfinispanTestSessionServer(port, __testSupport.getCache());
+        return server;
+    }
+
+    @Override
+    public void testLoad() throws Exception
+    {
+        super.testLoad();
+    }
+
+}
diff --git a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/LightLoadTest.java b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/ScatterGunLoadTest.java
similarity index 88%
rename from tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/LightLoadTest.java
rename to tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/ScatterGunLoadTest.java
index 9d241e0..34d7d07 100644
--- a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/LightLoadTest.java
+++ b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/ScatterGunLoadTest.java
@@ -18,15 +18,15 @@
 
 package org.eclipse.jetty.nosql.mongodb;
 
-import org.eclipse.jetty.server.session.AbstractLightLoadTest;
+import org.eclipse.jetty.server.session.AbstractScatterGunLoadTest;
 import org.eclipse.jetty.server.session.AbstractTestServer;
 import org.junit.Ignore;
 import org.junit.Test;
 
 /**
- * LightLoadTest
+ * ScatterGunLoadTest
  */
-public class LightLoadTest extends AbstractLightLoadTest
+public class ScatterGunLoadTest extends AbstractScatterGunLoadTest
 {
 
     public AbstractTestServer createServer(int port)
diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSameNodeLoadTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSameNodeLoadTest.java
new file mode 100644
index 0000000..999858f
--- /dev/null
+++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSameNodeLoadTest.java
@@ -0,0 +1,231 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Random;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.junit.Test;
+
+
+/**
+ * AbstractSameNodeLoadTest
+ * 
+ * This test performs multiple concurrent requests for the same session on the same node.
+ * 
+ */
+public abstract class AbstractSameNodeLoadTest
+{
+    protected boolean _stress = Boolean.getBoolean( "STRESS" );
+
+    public abstract AbstractTestServer createServer(int port);
+
+    @Test
+    public void testLoad() throws Exception
+    {
+        if ( _stress )
+        {
+            String contextPath = "";
+            String servletMapping = "/server";
+            AbstractTestServer server1 = createServer( 0 );
+            server1.addContext( contextPath ).addServlet( TestServlet.class, servletMapping );
+
+            try
+            {
+                server1.start();
+                int port1 = server1.getPort();
+
+                HttpClient client = new HttpClient();
+                client.start();
+                try
+                {
+                    String url = "http://localhost:" + port1 + contextPath + servletMapping;
+
+
+                    //create session via first server
+                    ContentResponse response1 = client.GET(url + "?action=init");
+                    assertEquals(HttpServletResponse.SC_OK,response1.getStatus());
+                    String sessionCookie = response1.getHeaders().getStringField( "Set-Cookie" );
+                    assertTrue(sessionCookie != null);
+                    // Mangle the cookie, replacing Path with $Path, etc.
+                    sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");
+
+                    //simulate 10 clients making 100 requests each
+                    ExecutorService executor = Executors.newCachedThreadPool();
+                    int clientsCount = 10;
+                    CyclicBarrier barrier = new CyclicBarrier( clientsCount + 1 );
+                    int requestsCount = 100;
+                    Worker[] workers = new Worker[clientsCount];
+                    for ( int i = 0; i < clientsCount; ++i )
+                    {
+                        workers[i] = new Worker(barrier, client, requestsCount, sessionCookie, url);
+                        executor.execute( workers[i] );
+                    }
+                    // Wait for all workers to be ready
+                    barrier.await();
+                    long start = System.nanoTime();
+
+                    // Wait for all workers to be done
+                    barrier.await();
+                    long end = System.nanoTime();
+                    long elapsed = TimeUnit.NANOSECONDS.toMillis( end - start );
+                    System.out.println( "elapsed ms: " + elapsed );
+
+                    executor.shutdownNow();
+
+                    // Perform one request to get the result
+                    Request request = client.newRequest( url + "?action=result" );
+                    request.header("Cookie", sessionCookie);
+                    ContentResponse response2 = request.send();
+                    assertEquals(HttpServletResponse.SC_OK,response2.getStatus());
+                    String response = response2.getContentAsString();
+                    System.out.println( "get = " + response );
+                    assertEquals(response.trim(), String.valueOf( clientsCount * requestsCount ) );
+                }
+                finally
+                {
+                    client.stop();
+                }
+            }
+            finally
+            {
+                server1.stop();
+            }
+        }
+    }
+
+    public static class Worker implements Runnable
+    {
+        public static int COUNT = 0;
+        
+        private final HttpClient client;
+
+        private final CyclicBarrier barrier;
+
+        private final int requestsCount;
+
+        private final String sessionCookie;
+
+        private final String url;
+        
+        private final String name;
+
+
+        public Worker(CyclicBarrier barrier, HttpClient client, int requestsCount, String sessionCookie, String url)
+        {
+            this.client = client;
+            this.barrier = barrier;
+            this.requestsCount = requestsCount;
+            this.sessionCookie = sessionCookie;
+            this.url = url;
+            this.name = ""+(COUNT++);
+        }
+
+
+        public void run()
+        {
+            try
+            {
+                // Wait for all workers to be ready
+                barrier.await();
+
+                Random random = new Random( System.nanoTime() );
+
+                for ( int i = 0; i < requestsCount; ++i )
+                {
+                    int pauseMsec = random.nextInt(1000);
+
+                    //wait a random number of milliseconds between requests up to 1 second
+                    if (pauseMsec > 0)
+                    {
+                        Thread.currentThread().sleep(pauseMsec);
+                    }
+                    Request request = client.newRequest(url + "?action=increment");
+                    request.header("Cookie", sessionCookie);
+                    ContentResponse response = request.send();
+                    assertEquals(HttpServletResponse.SC_OK,response.getStatus());
+                }
+
+                // Wait for all workers to be done
+                barrier.await();
+            }
+            catch ( Exception x )
+            {
+                throw new RuntimeException( x );
+            }
+        }
+    }
+
+    public static class TestServlet
+        extends HttpServlet
+    {
+        @Override
+        protected void doGet( HttpServletRequest request, HttpServletResponse response )
+            throws ServletException, IOException
+        {
+            String action = request.getParameter( "action" );
+            if ( "init".equals( action ) )
+            {
+                HttpSession session = request.getSession( true );
+                session.setAttribute( "value", 0 );
+            }
+            else if ( "increment".equals( action ) )
+            {
+                HttpSession session = request.getSession( false );
+                assertNotNull(session);
+                synchronized(session)
+                {
+                    int value = (Integer) session.getAttribute( "value" );
+                    session.setAttribute( "value", value + 1 );
+                }
+            }
+            else if ( "result".equals( action ) )
+            {
+                HttpSession session = request.getSession( false );
+                assertNotNull(session);
+                Integer value = null;
+                synchronized (session)
+                {
+                    value = (Integer) session.getAttribute( "value" );
+                }
+                PrintWriter writer = response.getWriter();
+                writer.println( value );
+                writer.flush();
+            }
+        }
+    }
+}
diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractLightLoadTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractScatterGunLoadTest.java
similarity index 98%
rename from tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractLightLoadTest.java
rename to tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractScatterGunLoadTest.java
index d9b798b..ed24b60 100644
--- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractLightLoadTest.java
+++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractScatterGunLoadTest.java
@@ -42,7 +42,7 @@
 
 
 /**
- * AbstractLightLoadTest
+ * AbstractScatterGunLoadTest
  * 
  * This is an unrealistic test. It takes a scatter-gun approach to smearing a
  * single session across 2 different nodes at once. 
@@ -50,7 +50,7 @@
  * In the real world, we must have a load balancer that uses sticky sessions
  * to keep the session pinned to a particular node.
  */
-public abstract class AbstractLightLoadTest
+public abstract class AbstractScatterGunLoadTest
 {
     protected boolean _stress = Boolean.getBoolean( "STRESS" );
 
diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractStopSessionManagerPreserveSessionTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractStopSessionManagerPreserveSessionTest.java
index de4c44d..b67f4d7 100644
--- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractStopSessionManagerPreserveSessionTest.java
+++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractStopSessionManagerPreserveSessionTest.java
@@ -36,6 +36,11 @@
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.junit.Test;
 
+/**
+ * AbstractStopSessionManagerPreserveSessionTest
+ *
+ *
+ */
 public abstract class AbstractStopSessionManagerPreserveSessionTest
 {
     public String _id;