Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-10.0.x

Signed-off-by: Jan Bartel <janb@webtide.com>
This commit is contained in:
Jan Bartel 2019-08-26 15:55:08 +10:00
commit 9534940578
7 changed files with 458 additions and 4 deletions

View File

@ -12,6 +12,11 @@
<New class="org.eclipse.jetty.server.session.NullSessionCacheFactory">
<Set name="saveOnCreate" property="jetty.session.saveOnCreate"/>
<Set name="removeUnloadableSessions" property="jetty.session.removeUnloadableSessions"/>
<Set name="writeThroughMode">
<Call class="org.eclipse.jetty.server.session.NullSessionCache$WriteThroughMode" name="valueOf">
<Arg><Property name="jetty.session.writeThroughMode" default="ON_EXIT"/></Arg>
</Call>
</Set>
</New>
</Arg>
</Call>

View File

@ -18,3 +18,4 @@ etc/sessions/session-cache-null.xml
[ini-template]
#jetty.session.saveOnCreate=false
#jetty.session.removeUnloadableSessions=false
#jetty.session.writeThroughMode=ON_EXIT

View File

@ -126,7 +126,7 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem
LOG.debug("Store: id={}, dirty={}, lsave={}, period={}, elapsed={}", id, data.isDirty(), data.getLastSaved(), savePeriodMs, (System.currentTimeMillis() - lastSave));
//save session if attribute changed or never been saved or time between saves exceeds threshold
if (data.isDirty() || (lastSave <= 0) || ((System.currentTimeMillis() - lastSave) > savePeriodMs))
if (data.isDirty() || (lastSave <= 0) || ((System.currentTimeMillis() - lastSave) >= savePeriodMs))
{
//set the last saved time to now
data.setLastSaved(System.currentTimeMillis());

View File

@ -18,7 +18,13 @@
package org.eclipse.jetty.server.session;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
/**
* NullSessionCache
@ -30,6 +36,154 @@ import javax.servlet.http.HttpServletRequest;
*/
public class NullSessionCache extends AbstractSessionCache
{
/**
* If the writethrough mode is ALWAYS or NEW, then use an
* attribute listener to ascertain when the attribute has changed.
*
*/
public class WriteThroughAttributeListener implements HttpSessionAttributeListener
{
Set<Session> _sessionsBeingWritten = ConcurrentHashMap.newKeySet();
@Override
public void attributeAdded(HttpSessionBindingEvent event)
{
doAttributeChanged(event);
}
@Override
public void attributeRemoved(HttpSessionBindingEvent event)
{
doAttributeChanged(event);
}
@Override
public void attributeReplaced(HttpSessionBindingEvent event)
{
doAttributeChanged(event);
}
private void doAttributeChanged(HttpSessionBindingEvent event)
{
if (_writeThroughMode == WriteThroughMode.ON_EXIT)
return;
Session session = (Session)event.getSession();
SessionDataStore store = getSessionDataStore();
if (store == null)
return;
if (_writeThroughMode == WriteThroughMode.ALWAYS
|| (_writeThroughMode == WriteThroughMode.NEW && session.isNew()))
{
//ensure that a call to willPassivate doesn't result in a passivation
//listener removing an attribute, which would cause this listener to
//be called again
if (_sessionsBeingWritten.add(session))
{
try
{
//should hold the lock on the session, but as sessions are never shared
//with the NullSessionCache, there can be no other thread modifying the
//same session at the same time (although of course there can be another
//request modifying its copy of the session data, so it is impossible
//to guarantee the order of writes).
if (store.isPassivating())
session.willPassivate();
store.store(session.getId(), session.getSessionData());
if (store.isPassivating())
session.didActivate();
}
catch (Exception e)
{
LOG.warn("Write through of {} failed", e);
}
finally
{
_sessionsBeingWritten.remove(session);
}
}
}
}
}
/**
* Defines the circumstances a session will be written to the backing store.
*/
public enum WriteThroughMode
{
/**
* ALWAYS means write through every attribute change.
*/
ALWAYS,
/**
* NEW means to write through every attribute change only
* while the session is freshly created, ie its id has not yet been returned to the client
*/
NEW,
/**
* ON_EXIT means write the session only when the request exits
* (which is the default behaviour of AbstractSessionCache)
*/
ON_EXIT
};
private WriteThroughMode _writeThroughMode = WriteThroughMode.ON_EXIT;
protected WriteThroughAttributeListener _listener = null;
/**
* @return the writeThroughMode
*/
public WriteThroughMode getWriteThroughMode()
{
return _writeThroughMode;
}
/**
* @param writeThroughMode the writeThroughMode to set
*/
public void setWriteThroughMode(WriteThroughMode writeThroughMode)
{
if (getSessionHandler() == null)
throw new IllegalStateException ("No SessionHandler");
//assume setting null is the same as ON_EXIT
if (writeThroughMode == null)
{
if (_listener != null)
getSessionHandler().removeEventListener(_listener);
_listener = null;
_writeThroughMode = WriteThroughMode.ON_EXIT;
return;
}
switch (writeThroughMode)
{
case ON_EXIT:
{
if (_listener != null)
getSessionHandler().removeEventListener(_listener);
_listener = null;
break;
}
case NEW:
case ALWAYS:
{
if (_listener == null)
{
_listener = new WriteThroughAttributeListener();
getSessionHandler().addEventListener(_listener);
}
break;
}
}
_writeThroughMode = writeThroughMode;
}
/**
* @param handler The SessionHandler related to this SessionCache
@ -39,6 +193,7 @@ public class NullSessionCache extends AbstractSessionCache
super(handler);
super.setEvictionPolicy(EVICT_ON_SESSION_EXIT);
}
/**
* @see org.eclipse.jetty.server.session.SessionCache#shutdown()

View File

@ -27,6 +27,23 @@ public class NullSessionCacheFactory implements SessionCacheFactory
{
boolean _saveOnCreate;
boolean _removeUnloadableSessions;
NullSessionCache.WriteThroughMode _writeThroughMode;
/**
* @return the writeThroughMode
*/
public NullSessionCache.WriteThroughMode getWriteThroughMode()
{
return _writeThroughMode;
}
/**
* @param writeThroughMode the writeThroughMode to set
*/
public void setWriteThroughMode(NullSessionCache.WriteThroughMode writeThroughMode)
{
_writeThroughMode = writeThroughMode;
}
/**
* @return the saveOnCreate
@ -69,6 +86,7 @@ public class NullSessionCacheFactory implements SessionCacheFactory
NullSessionCache cache = new NullSessionCache(handler);
cache.setSaveOnCreate(isSaveOnCreate());
cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions());
cache.setWriteThroughMode(_writeThroughMode);
return cache;
}
}

View File

@ -79,8 +79,8 @@ public class TestSessionDataStore extends AbstractSessionDataStore
@Override
public void doStore(String id, SessionData data, long lastSaveTime) throws Exception
{
_numSaves.addAndGet(1);
_map.put(id, data);
_numSaves.addAndGet(1);
}
@Override

View File

@ -18,13 +18,21 @@
package org.eclipse.jetty.server.session;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionEvent;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@ -32,9 +40,275 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public class NullSessionCacheTest
{
@Test
public void testEvictOnExit() throws Exception
public static class SerializableTestObject implements Serializable, HttpSessionActivationListener
{
int count;
static int passivates = 0;
static int activates = 0;
public SerializableTestObject(int i)
{
count = i;
}
@Override
public void sessionWillPassivate(HttpSessionEvent se)
{
//should never be called, as we are replaced with the
//non-serializable object and thus passivate will be called on that
++passivates;
}
@Override
public void sessionDidActivate(HttpSessionEvent se)
{
++activates;
//remove myself, replace with something serializable
se.getSession().setAttribute("pv", new TestObject(count));
}
}
public static class TestObject implements HttpSessionActivationListener
{
int i;
static int passivates = 0;
static int activates = 0;
public TestObject(int j)
{
i = j;
}
@Override
public void sessionWillPassivate(HttpSessionEvent se)
{
++passivates;
//remove myself, replace with something serializable
se.getSession().setAttribute("pv", new SerializableTestObject(i));
}
@Override
public void sessionDidActivate(HttpSessionEvent se)
{
//this should never be called because we replace ourselves during passivation,
//so it is the SerializableTestObject that is activated instead
++activates;
}
}
@Test
public void testWritesWithPassivation() throws Exception
{
//Test that a session that is in the process of being saved cannot cause
//another save via a passivation listener
Server server = new Server();
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/test");
context.setServer(server);
NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory();
cacheFactory.setWriteThroughMode(NullSessionCache.WriteThroughMode.ALWAYS);
NullSessionCache cache = (NullSessionCache)cacheFactory.getSessionCache(context.getSessionHandler());
TestSessionDataStore store = new TestSessionDataStore(true); //pretend to passivate
cache.setSessionDataStore(store);
context.getSessionHandler().setSessionCache(cache);
context.start();
//make a session
long now = System.currentTimeMillis();
SessionData data = store.newSessionData("1234", now - 20, now - 10, now - 20, TimeUnit.MINUTES.toMillis(10));
data.setExpiry(now + TimeUnit.DAYS.toMillis(1));
Session session = cache.newSession(null, data); //mimic a request making a session
cache.add("1234", session);
//at this point the session should not be saved to the store
assertEquals(0, store._numSaves.get());
//set an attribute that is not serializable, should cause a save
TestObject obj = new TestObject(1);
session.setAttribute("pv", obj);
assertTrue(cache._listener._sessionsBeingWritten.isEmpty());
assertTrue(store.exists("1234"));
assertEquals(1, store._numSaves.get());
assertEquals(1, TestObject.passivates);
assertEquals(0, TestObject.activates);
assertEquals(1, SerializableTestObject.activates);
assertEquals(0, SerializableTestObject.passivates);
}
@Test
public void testChangeWriteThroughMode() throws Exception
{
Server server = new Server();
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/test");
context.setServer(server);
NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory();
NullSessionCache cache = (NullSessionCache)cacheFactory.getSessionCache(context.getSessionHandler());
TestSessionDataStore store = new TestSessionDataStore();
cache.setSessionDataStore(store);
context.getSessionHandler().setSessionCache(cache);
assertEquals(NullSessionCache.WriteThroughMode.ON_EXIT, cache.getWriteThroughMode());
assertNull(cache._listener);
//change mode to NEW
cache.setWriteThroughMode(NullSessionCache.WriteThroughMode.NEW);
assertEquals(NullSessionCache.WriteThroughMode.NEW, cache.getWriteThroughMode());
assertNotNull(cache._listener);
assertEquals(1, context.getSessionHandler()._sessionAttributeListeners.size());
assertTrue(context.getSessionHandler()._sessionAttributeListeners.contains(cache._listener));
//change mode to ALWAYS from NEW, listener should remain
NullSessionCache.WriteThroughAttributeListener old = cache._listener;
cache.setWriteThroughMode(NullSessionCache.WriteThroughMode.ALWAYS);
assertEquals(NullSessionCache.WriteThroughMode.ALWAYS, cache.getWriteThroughMode());
assertNotNull(cache._listener);
assertSame(old,cache._listener);
assertEquals(1, context.getSessionHandler()._sessionAttributeListeners.size());
//check null is same as ON_EXIT
cache.setWriteThroughMode(null);
assertEquals(NullSessionCache.WriteThroughMode.ON_EXIT, cache.getWriteThroughMode());
assertNull(cache._listener);
assertEquals(0, context.getSessionHandler()._sessionAttributeListeners.size());
//change to ON_EXIT
cache.setWriteThroughMode(NullSessionCache.WriteThroughMode.ON_EXIT);
assertEquals(NullSessionCache.WriteThroughMode.ON_EXIT, cache.getWriteThroughMode());
assertNull(cache._listener);
assertEquals(0, context.getSessionHandler()._sessionAttributeListeners.size());
}
@Test
public void testWriteThroughAlways() throws Exception
{
Server server = new Server();
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/test");
context.setServer(server);
NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory();
cacheFactory.setWriteThroughMode(NullSessionCache.WriteThroughMode.ALWAYS);
NullSessionCache cache = (NullSessionCache)cacheFactory.getSessionCache(context.getSessionHandler());
TestSessionDataStore store = new TestSessionDataStore();
cache.setSessionDataStore(store);
context.getSessionHandler().setSessionCache(cache);
context.start();
//make a session
long now = System.currentTimeMillis();
SessionData data = store.newSessionData("1234", now - 20, now - 10, now - 20, TimeUnit.MINUTES.toMillis(10));
data.setExpiry(now + TimeUnit.DAYS.toMillis(1));
Session session = cache.newSession(null, data); //mimic a request making a session
cache.add("1234", session);
//at this point the session should not be saved to the store
assertEquals(0, store._numSaves.get());
//check each call to set attribute results in a store
session.setAttribute("colour", "blue");
assertTrue(store.exists("1234"));
assertEquals(1, store._numSaves.get());
//mimic releasing the session after the request is finished
cache.release("1234", session);
assertTrue(store.exists("1234"));
assertFalse(cache.contains("1234"));
assertEquals(2, store._numSaves.get());
//simulate a new request using the previously created session
//the session should not now be new
session = cache.get("1234"); //get the session again
session.access(now); //simulate a request
session.setAttribute("spin", "left");
assertTrue(store.exists("1234"));
assertEquals(3, store._numSaves.get());
cache.release("1234", session); //finish with the session
assertFalse(session.isResident());
}
@Test
public void testWriteThroughNew () throws Exception
{
Server server = new Server();
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/test");
context.setServer(server);
NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory();
cacheFactory.setWriteThroughMode(NullSessionCache.WriteThroughMode.NEW);
NullSessionCache cache = (NullSessionCache)cacheFactory.getSessionCache(context.getSessionHandler());
TestSessionDataStore store = new TestSessionDataStore();
cache.setSessionDataStore(store);
context.getSessionHandler().setSessionCache(cache);
context.start();
//make a session
long now = System.currentTimeMillis();
SessionData data = store.newSessionData("1234", now - 20, now - 10, now - 20, TimeUnit.MINUTES.toMillis(10));
data.setExpiry(now + TimeUnit.DAYS.toMillis(1));
Session session = cache.newSession(null, data); //mimic a request making a session
cache.add("1234", session);
//at this point the session should not be saved to the store
assertEquals(0, store._numSaves.get());
assertTrue(session.isNew());
//check each call to set attribute results in a store while the session is new
session.setAttribute("colour", "blue");
assertTrue(store.exists("1234"));
assertEquals(1, store._numSaves.get());
session.setAttribute("charge", "positive");
assertEquals(2, store._numSaves.get());
//mimic releasing the session after the request is finished
cache.release("1234", session);
assertTrue(store.exists("1234"));
assertFalse(cache.contains("1234"));
assertEquals(3, store._numSaves.get()); //even if the session isn't dirty, we will save the access time
//simulate a new request using the previously created session
//the session should not now be new, so setAttribute should
//not result in a save
session = cache.get("1234"); //get the session again
session.access(now); //simulate a request
assertFalse(session.isNew());
assertEquals(3, store._numSaves.get());
session.setAttribute("spin", "left");
assertTrue(store.exists("1234"));
assertEquals(3, store._numSaves.get());
session.setAttribute("flavor", "charm");
assertEquals(3, store._numSaves.get());
cache.release("1234", session); //finish with the session
assertEquals(4, store._numSaves.get());//release session should write it out
assertFalse(session.isResident());
}
@Test
public void testNotCached() throws Exception
{
//Test the NullSessionCache never contains the session
Server server = new Server();
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
@ -67,6 +341,7 @@ public class NullSessionCacheTest
session = cache.get("1234"); //get the session again
session.access(now); //simulate a request
cache.release("1234", session); //finish with the session
assertFalse(cache.contains("1234"));
assertFalse(session.isResident());
}