package org.sakaiproject.tool.impl; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; import org.apache.commons.lang.mutable.MutableLong; import org.jmock.Expectations; import org.sakaiproject.id.api.IdManager; import org.sakaiproject.thread_local.api.ThreadLocalManager; import org.sakaiproject.tool.api.ContextSession; import org.sakaiproject.tool.api.NonPortableSession; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.SessionAttributeListener; import org.sakaiproject.tool.api.SessionBindingEvent; import org.sakaiproject.tool.api.SessionBindingListener; import org.sakaiproject.tool.api.ToolManager; import org.sakaiproject.tool.api.ToolSession; /** * Verifies behavior of {@link MySession}, which * is the standard implementation of {@link Session}. * * <p>Tests are not comprehensive. Where an implementation is simple * field access, the corresponding test has typically been skipped.</p> * * <p>Read on for more design notes.</p> * * <p>Theoretically, this guards against regressions in this module. * In reality, though, this test will eventually fail as-is b/c it * actually does test <code>MySession</code> as a unit as opposed to, * for example, relying on factory methods like * {@link SessionComponent#startSession()}. Thus, were * <code>MySession</code> refactored to a top-level class, which we * expect, this test will need to be modified accordingly. So we were * forced to choose between "pure" unit tests, which break * (paradoxically) if the relationship between <code>MySession</code> * and <code>SessionComponent</code> changes, and black box-ish tests * which intentionally obscure the division of responsibilities between * <code>MySession</code> and <code>SessionComponent</code>.</p> * * <p>We decided to pursue "pure" unit tests in combination with a small * quantity of black box-ish tests in {@link SessionComponentRegressionTest} * to guard against complete failure. For example, see * {@link SessionComponentRegressionTest#testGetSessionReturnsNullIfSessionExpired()}, * which implicitly tests callbacks to <code>SessionComponent</code> * from {@link Session#invalidate()}. We felt this was the correct * decision because testing the behavior of factory methods should be a * separate concern from testing the behavior of the created object * itself. Such a design improves the overall quality of the code base * while still satisfying our original goal of supporting anticipated * modifications to both {@link SessionComponent} and its internal * implementations of the Sessions domain.</p> * * * @author dmccallum@unicon.net * */ public class MySessionTest extends BaseSessionComponentTest { public void testCreatedInExpectedState() throws Exception { final String sessionId = "SESSION_ID"; doTestCreatedInExpectedState(sessionId, new Callable<MySession>() { public MySession call() throws Exception { return createSession(sessionId); } }); } public void testCreatedInExpectedStateWithClientSpecifiedId() throws Exception { final String sessionId = "SESSION_ID"; doTestCreatedInExpectedState(sessionId, new Callable<MySession>() { public MySession call() throws Exception { return createSessionWithClientSpecifiedId(sessionId); } }); } protected void doTestCreatedInExpectedState(String sessionId, Callable<MySession> factoryCallback) throws Exception { MySession session = factoryCallback.call(); assertEquals(sessionId, session.getId()); assertTrue(session.getCreationTime() > 0); assertEquals(session.getCreationTime(), session.getLastAccessedTime()); assertEquals(sessionComponent.m_defaultInactiveInterval, session .getMaxInactiveInterval()); assertNull(session.getUserId()); assertNull(session.getUserEid()); assertFalse(session.getAttributeNames().hasMoreElements()); } /** * {@link MySession#invalidate()} should behave like * {@link MySession#clear()} but with extra logic for unsetting itself as * the "current" session. * * @see #doTestSessionClear(org.sakaiproject.tool.impl.MySessionTest.MyTestableSession, * Runnable) */ public void testInvalidateClearsSessionAndUnsetsItselfAsCurrent() { final MyTestableSession session = createSession(); doTestSessionClear(session, new Runnable() { public void run() { expectGetAndUnsetCurrentSession(session); session.invalidate(); } }); } /** * Exactly the same as {@link #testInvalidateClearsSessionAndUnsetsItselfAsCurrent()} * except that the session's currentness should not be affected. * * @see #doTestSessionClear(org.sakaiproject.tool.impl.MySessionTest.MyTestableSession, Runnable) */ public void testClearUnbindsAttributes() { final MyTestableSession session = createSession(); doTestSessionClear(session, new Runnable() { public void run() { session.clear(); } }); } protected void doTestSessionClear(final MyTestableSession session, Runnable codeExerciseCallback) { ContextSession contextSession = getContextSession(session, "CONTEXT_SESSION_ID"); ToolSession toolSession = getToolSession(session, "PLACEMENT_ID"); final String sessionAttribKey = "SESSION_ATTRIB_KEY"; final ListeningAttribValue sessionAttribValue = setNewListeningAttribValue(session, sessionAttribKey); setNewListeningAttribValue(contextSession, "CONTEXT_SESSION_ATTRIB_KEY"); setNewListeningAttribValue(toolSession, "TOOL_SESSION_ATTRIB_KEY"); codeExerciseCallback.run(); assertHasNoAttributes(session); assertHasNoAttributes(toolSession); assertHasNoAttributes(contextSession); assertEquals(new HashMap<String,Object>() {{ put(sessionAttribKey, sessionAttribValue); }}, session.unbindInvokedWith); // Oddly, we can still use the session as a factory even after its been // invalidated. Were this to change, we'd need to devise a more clever // mechanism for detecting cascaded context/tool session invalidation. assertNotSame(contextSession, session.getContextSession(contextSession.getContextId())); assertNotSame(toolSession, session.getToolSession(toolSession.getPlacementId())); } /** * Hard to believe this is the correct behavior, but this is what * the "legacy" implementation does. The {@link Session}'s own attributes * are selectively filtered, retaining only those named by the specified * <code>Collection</code>, but all child {@link ToolSession} and * {@link ContextSession} attributes are wiped away by virtue of a call to * {@link Session#clear()}. */ public void testClearExceptFiltersSessionAttribsButClearsAllToolAndContextSessionAttribs() { MyTestableSession session = createSession(); ContextSession contextSession = getContextSession(session, "CONTEXT_SESSION_ID"); ToolSession toolSession = getToolSession(session, "PLACEMENT_ID"); final String sessionAttribKey1 = "SESSION_ATTRIB_KEY_1"; final String sessionAttribKey2 = "SESSION_ATTRIB_KEY_2"; final ListeningAttribValue sessionAttribValue1 = setNewListeningAttribValue(session, sessionAttribKey1); final ListeningAttribValue sessionAttribValue2 = setNewListeningAttribValue(session, sessionAttribKey2); setNewListeningAttribValue(contextSession, "CONTEXT_SESSION_ATTRIB_KEY"); setNewListeningAttribValue(toolSession, "TOOL_SESSION_ATTRIB_KEY"); session.clearExcept(new HashSet<String>() {{ add(sessionAttribKey1); }}); assertEquals(sessionAttribValue1, session.getAttribute(sessionAttribKey1)); assertNull(session.getAttribute(sessionAttribKey2)); assertEquals(new HashMap<String,Object>() {{ put(sessionAttribKey2, sessionAttribValue2); }}, session.unbindInvokedWith); assertHasNoAttributes(toolSession); assertHasNoAttributes(contextSession); } public void testRemoveAttributeReleasesAttributeAndFiresUnbind() { MyTestableSession session = createSession(); final String sessionAttribKey = "SESSION_ATTRIB_KEY"; final ListeningAttribValue sessionAttribValue = setNewListeningAttribValue(session, sessionAttribKey); session.removeAttribute(sessionAttribKey); assertNull(session.getAttribute(sessionAttribKey)); assertHasNoAttributes(session); assertEquals(new HashMap<String,Object>() {{ put(sessionAttribKey, sessionAttribValue); }}, session.unbindInvokedWith); } public void testSetAndRemoveAttributeDoNotUpdateLastAccessedTime() throws InterruptedException { MyTestableSession session = createSession(); long origLastAccessed = session.getLastAccessedTime(); final String sessionAttribKey = "SESSION_ATTRIB_KEY"; final ListeningAttribValue sessionAttribValue = setNewListeningAttribValue(session, sessionAttribKey); Thread.sleep(2); // we might execute too quickly to affect lastAccessedTime session.removeAttribute(sessionAttribKey); assertEquals(origLastAccessed, session.getLastAccessedTime()); } public void testSettingNullAttributeValueReleasesAttributeAndFiresUnbind() { MyTestableSession session = createSession(); final String sessionAttribKey = "SESSION_ATTRIB_KEY"; final ListeningAttribValue sessionAttribValue = setNewListeningAttribValue(session, sessionAttribKey); session.setAttribute(sessionAttribKey, null); assertNull(session.getAttribute(sessionAttribKey)); assertHasNoAttributes(session); assertEquals(new HashMap<String,Object>() {{ put(sessionAttribKey, sessionAttribValue); }}, session.unbindInvokedWith); } public void testSetAttributeCachesAttributeAndFiresBind() { MyTestableSession session = createSession(); final String sessionAttribKey = "SESSION_ATTRIB_KEY"; final ListeningAttribValue sessionAttribValue = setNewListeningAttribValue(session, sessionAttribKey); assertEquals(sessionAttribValue, session.getAttribute(sessionAttribKey)); assertEquals(sessionAttribKey, session.getAttributeNames().nextElement()); assertEquals(new HashMap<String,Object>() {{ put(sessionAttribKey, sessionAttribValue); }}, session.bindInvokedWith); } public void testSetAttributeOverwritesExistingAttributeAndFiresBindAndUnbind() { MyTestableSession session = createSession(); final String sessionAttribKey = "SESSION_ATTRIB_KEY"; final ListeningAttribValue sessionAttribValue1 = setNewListeningAttribValue(session, sessionAttribKey); final ListeningAttribValue sessionAttribValue2 = setNewListeningAttribValue(session, sessionAttribKey); assertEquals(sessionAttribValue2, session.getAttribute(sessionAttribKey)); assertEquals(sessionAttribKey, session.getAttributeNames().nextElement()); assertEquals(new HashMap<String,Object>() {{ put(sessionAttribKey, sessionAttribValue1); }}, session.unbindInvokedWith); assertEquals(new HashMap<String,Object>() {{ put(sessionAttribKey, sessionAttribValue2); }}, session.bindInvokedWith); } public void testLazilyCreatesToolSessionInExpectedState() { MySession session = createSession(); session.setUserEid("USER_EID"); session.setUserId("USER_ID"); String toolSessionId = "TOOL_SESSION_ID"; String placementId = "TOOL_PLACEMENT_ID"; ToolSession toolSession = getToolSession(session, placementId, toolSessionId); assertEquals(toolSessionId, toolSession.getId()); assertEquals(placementId, toolSession.getPlacementId()); assertTrue(toolSession.getCreationTime() > 0); assertTrue(toolSession.getLastAccessedTime() >= toolSession.getCreationTime()); assertEquals(session.getUserEid(), toolSession.getUserEid()); assertEquals(session.getUserId(), toolSession.getUserId()); assertHasNoAttributes(toolSession); } public void testLazilyCreatesContextSessionInExpectedState() { MySession session = createSession(); session.setUserEid("USER_EID"); session.setUserId("USER_ID"); String contextSessionId = "CONTEXT_SESSION_ID"; String contextId = "CONTEXT_ID"; ContextSession contextSession = getContextSession(session, contextId, contextSessionId); assertEquals(contextSessionId, contextSession.getId()); assertEquals(contextId, contextSession.getContextId()); assertTrue(contextSession.getCreationTime() > 0); assertTrue(contextSession.getLastAccessedTime() >= contextSession.getCreationTime()); assertEquals(session.getUserEid(), contextSession.getUserEid()); assertEquals(session.getUserId(), contextSession.getUserId()); assertHasNoAttributes(contextSession); } // note this is part of the protected API public void testIsInactive() throws InterruptedException { //return ((m_inactiveInterval > 0) && (System.currentTimeMillis() > (m_accessed + (m_inactiveInterval * 1000)))); MySession session = createSession(); int inactivityThreshold = 1; session.setMaxInactiveInterval(1); session.setActive(); Thread.sleep(inactivityThreshold * 2000); assertTrue(session.isInactive()); } public void testNeverInactiveIfMaxInactiveIntervalLteZero() { MySession session = createSession(); session.setMaxInactiveInterval(0); assertFalse(session.isInactive()); session.setMaxInactiveInterval(-1); assertFalse(session.isInactive()); } public void testSetActiveDoesNotUpdateTimeExpirationSuggestion() { // inactivity in seconds int inactivityThreshold = 30; // original expirationTimeSuggestion value gets set to current time + MaxInActive MySession session = createSessionSetMaxInActive(inactivityThreshold); long originalValue = session.expirationTimeSuggestion.longValue(); // setActive should force expirationTimeSuggestion value to be updated // if the difference between currentTime and expiry time is smaller then inactive period/2 session.setActive(); long newValue = session.expirationTimeSuggestion.longValue(); // make sure the expiry value was not updated (because we let no time expire) // by asserting the original and new value are the same assertEquals(originalValue, newValue); } public void testSetActiveUpdatesTimeExpirationSuggestion() { // inactivity in seconds int inactivityThreshold = 30; // original expirationTimeSuggestion value gets set to current time + MaxInActive // Simulate some time has past since the session was last accessed by setting the accessed time long pastTime = now() - ((inactivityThreshold*1000)+5000); MySession session = createSessionSetMaxInActiveAndAccessTime(inactivityThreshold,pastTime); long originalValue = session.expirationTimeSuggestion.longValue(); // setActive should force expirationTimeSuggestion value to be updated // if the difference between currentTime and expiry time is smaller then inactive period/2 session.setActive(); long newValue = session.expirationTimeSuggestion.longValue(); // make sure the expiry value was not updated (because we let no time expire) // by asserting the original and new value are the same assertNotSame(Long.valueOf(originalValue), Long.valueOf(newValue)); } public MySession createSessionSetMaxInActive(int maxInactive) { MySession session = createSession(); session.setMaxInactiveInterval(maxInactive); return session; } public MySession createSessionSetMaxInActiveAndAccessTime(int maxInactive, long accessedTime) { MySession session = createSessionSetMaxInActive(maxInactive); session.m_accessed = accessedTime; return session; } public long now() { return System.currentTimeMillis(); } public void testEqualsMatchesAnySessionImplementorHavingSameId() { // attributes added in for a little noise, to be "extra sure" we only care about IDs. final MySession session1 = createSession(); setNewListeningAttribValue(session1, "SESSION_ATTRIB_KEY_1"); MySession session2 = createSession(session1.getId()); setNewListeningAttribValue(session2, "SESSION_ATTRIB_KEY_2"); assertEquals(session1, session2); // now lets see if it cares about a sibling implementation final Session session3 = mock(Session.class); checking(new Expectations(){{ one(session3).getId(); will(returnValue(session1.getId())); }}); assertEquals(session1, session3); } public void testUnbindNotifiesValueIfIsSessionBindingListener() { MySession session = createSession(); ListeningAttribValue attribValue = new ListeningAttribValue(); session.unBind("SESSION_KEY", attribValue); assertEquals(1, attribValue.sessionValueUnboundInvokedWith.size()); SessionBindingEvent event = attribValue.sessionValueUnboundInvokedWith.get(0); assertEventState(event, "SESSION_KEY", session, attribValue); } public void testUnbindNotifiesValueIfIsHttpSessionBindingListener() { MySession session = createSession(); ListeningAttribValue attribValue = new ListeningAttribValue(); session.unBind("SESSION_KEY", attribValue); assertEquals(1, attribValue.httpSessionValueUnboundInvokedWith.size()); HttpSessionBindingEvent event = attribValue.httpSessionValueUnboundInvokedWith.get(0); assertEventState(event, "SESSION_KEY", session, attribValue); } public void testBindNotifiesValueIfIsSessionBindingListener() { MySession session = createSession(); ListeningAttribValue attribValue = new ListeningAttribValue(); session.bind("SESSION_KEY", attribValue); assertEquals(1, attribValue.sessionValueBoundInvokedWith.size()); SessionBindingEvent event = attribValue.sessionValueBoundInvokedWith.get(0); assertEventState(event, "SESSION_KEY", session, attribValue); } public void testBindNotifiesValueIfIsHttpSessionBindingListener() { MySession session = createSession(); ListeningAttribValue attribValue = new ListeningAttribValue(); session.bind("SESSION_KEY", attribValue); assertEquals(1, attribValue.httpSessionValueBoundInvokedWith.size()); HttpSessionBindingEvent event = attribValue.httpSessionValueBoundInvokedWith.get(0); assertEventState(event, "SESSION_KEY", session, attribValue); } public void testNonPortableAttributesStoreAndRetrieve() { MySession session = createSession(); String value = "VALUE"; System.setProperty("sakai.cluster.terracotta","true"); session.setAttribute("SESSION_KEY", value); System.setProperty("sakai.cluster.terracotta","false"); session.setAttribute("SESSION_KEY_2", value); assertEquals(value,session.getAttribute("SESSION_KEY")); assertEquals(value,session.getAttribute("SESSION_KEY_2")); } public void testNonPortableBindNotifiesValueIfIsSessionBindingListener() { MySession session = createSession(); System.setProperty("sakai.cluster.terracotta","true"); ListeningAttribValue attribValue = new ListeningAttribValue(); session.bind("SESSION_KEY", attribValue); assertEquals(1, attribValue.sessionValueBoundInvokedWith.size()); SessionBindingEvent event = attribValue.sessionValueBoundInvokedWith.get(0); assertEventState(event, "SESSION_KEY", session, attribValue); System.setProperty("sakai.cluster.terracotta","false"); } public void testNonPortableUnbindNotifiesValueIfIsSessionBindingListener() { MySession session = createSession(); System.setProperty("sakai.cluster.terracotta","true"); ListeningAttribValue attribValue = new ListeningAttribValue(); session.unBind("SESSION_KEY", attribValue); assertEquals(1, attribValue.sessionValueUnboundInvokedWith.size()); SessionBindingEvent event = attribValue.sessionValueUnboundInvokedWith.get(0); assertEventState(event, "SESSION_KEY", session, attribValue); System.setProperty("sakai.cluster.terracotta","false"); } public void testNonPortableRemoveAttributeReleasesAttributeAndFiresUnbind() { System.setProperty("sakai.cluster.terracotta","true"); MyTestableSession session = createSession(); expectToolCheck("simple.unit.test"); final String sessionAttribKey = "SESSION_ATTRIB_KEY"; final ListeningAttribValue sessionAttribValue = setNewListeningAttribValue(session, sessionAttribKey); session.removeAttribute(sessionAttribKey); assertNull(session.getAttribute(sessionAttribKey)); assertHasNoAttributes(session); assertEquals(new HashMap<String,Object>() {{ put(sessionAttribKey, sessionAttribValue); }}, session.unbindInvokedWith); System.setProperty("sakai.cluster.terracotta","false"); } public void testNonPortableClearUnbindsAttributes() { final MyTestableSession session = createSession(); System.setProperty("sakai.cluster.terracotta","true"); allowToolCheck("simple.unit.test"); doTestSessionClear(session, new Runnable() { public void run() { session.clear(); } }); System.setProperty("sakai.cluster.terracotta","false"); } protected void assertEventState(SessionBindingEvent event, String name, MySession session, Object value) { assertEquals(name, event.getName()); assertEquals(session, event.getSession()); assertEquals(value, event.getValue()); } protected void assertEventState(HttpSessionBindingEvent event, String name, MySession session, Object value) { assertEquals(name, event.getName()); assertEquals(session, event.getSession()); assertEquals(value, event.getValue()); } protected ContextSession getContextSession(MySession session, String contextId) { allowCreateUuidRequest(); return session.getContextSession(contextId); } protected ContextSession getContextSession(MySession session, String contextId, String contextSessionId) { expectCreateUuidRequest(contextSessionId); // mtd signature implies we must _expect_ the IdManager call return session.getContextSession(contextId); } protected ToolSession getToolSession(MySession session, String placementId) { allowCreateUuidRequest(); return session.getToolSession(placementId); } protected ToolSession getToolSession(MySession session, String placementId, String toolSessionId) { expectCreateUuidRequest(toolSessionId); // mtd signature implies we must _expect_ the IdManager call return session.getToolSession(placementId); } protected MyTestableSession createSession() { String uuid = nextUuid(); return new MyTestableSession(sessionComponent,uuid,threadLocalManager,idManager,sessionListener,new MyNonPortableSession()); } protected MyTestableSession createSession(String sessionId) { return new MyTestableSession(sessionComponent,sessionId,threadLocalManager,idManager,sessionListener,new MyNonPortableSession()); } /** * Like {@link #createSession(String)} but assigns the given ID * directly, rather than allowing the created session to * retrieve an ID from the <code>IdManager</code>. This allows * us to test a different constructor than does * {@link #createSession(String)}. * * @param sessionId * @return */ protected MyTestableSession createSessionWithClientSpecifiedId(String sessionId) { return new MyTestableSession(sessionComponent, sessionId, threadLocalManager, idManager, sessionListener, new MyNonPortableSession()); } private static class MyTestableSession extends MySession { private Map<String,Object> unbindInvokedWith = new HashMap<String,Object>(); private Map<String,Object> bindInvokedWith = new HashMap<String,Object>(); public MyTestableSession(SessionComponent outer, String sessionId, ThreadLocalManager threadLocalManager, IdManager idManager, SessionAttributeListener sessionListener, NonPortableSession nps) { super(outer, sessionId, threadLocalManager, idManager, outer, sessionListener, outer.getInactiveInterval(),nps,new MutableLong(System.currentTimeMillis())); } @Override protected void unBind(String name, Object value) { unbindInvokedWith.put(name, value); super.unBind(name, value); } @Override protected void bind(String name, Object value) { bindInvokedWith.put(name, value); super.bind(name, value); } } private static class ListeningAttribValue implements SessionBindingListener, HttpSessionBindingListener { private List<SessionBindingEvent> sessionValueBoundInvokedWith = Collections.synchronizedList(new ArrayList<SessionBindingEvent>()); private List<SessionBindingEvent> sessionValueUnboundInvokedWith = Collections.synchronizedList(new ArrayList<SessionBindingEvent>()); private List<HttpSessionBindingEvent> httpSessionValueBoundInvokedWith = Collections.synchronizedList(new ArrayList<HttpSessionBindingEvent>()); private List<HttpSessionBindingEvent> httpSessionValueUnboundInvokedWith = Collections.synchronizedList(new ArrayList<HttpSessionBindingEvent>()); public void valueBound(SessionBindingEvent event) { this.sessionValueBoundInvokedWith.add(event); } public void valueUnbound(SessionBindingEvent event) { this.sessionValueUnboundInvokedWith.add(event); } public void valueBound(HttpSessionBindingEvent event) { this.httpSessionValueBoundInvokedWith.add(event); } public void valueUnbound(HttpSessionBindingEvent event) { this.httpSessionValueUnboundInvokedWith.add(event); } } protected ListeningAttribValue setNewListeningAttribValue( ToolSession toolSession, String key) { ListeningAttribValue value = new ListeningAttribValue(); toolSession.setAttribute(key, value); return value; } protected ListeningAttribValue setNewListeningAttribValue( ContextSession contextSession, String key) { ListeningAttribValue value = new ListeningAttribValue(); contextSession.setAttribute(key, value); return value; } protected ListeningAttribValue setNewListeningAttribValue(MySession session, String key) { ListeningAttribValue value = new ListeningAttribValue(); session.setAttribute(key, value); return value; } protected void assertHasNoAttributes(ContextSession contextSession) { assertFalse(contextSession.getAttributeNames().hasMoreElements()); } protected void assertHasNoAttributes(ToolSession toolSession) { assertFalse(toolSession.getAttributeNames().hasMoreElements()); } protected void assertHasNoAttributes(MySession session) { assertFalse(session.getAttributeNames().hasMoreElements()); } /** * Verifies that multiple {@link MySession#invalidate()} calls can proceed * concurrently without error and with the session properly invalidated * after all threads return. This turns out to be quite difficult to test * reliably, even with knowledge of the implementation. In fact, you can't get * the following test to fail, even if you remove all explicit concurrency * precautions in <code>MySession.invalidate()</code>. It will fail, though, * if sleep times are injected after taking a local copies of the attribute * map, though. So, at best this test will catch truly gross errors, but * for the most part is dead-weight. */ public void testConcurrentInvalidation() { final MyTestableSession session = createSession(); Collection<ListeningAttribValue> attribValues = new ArrayList<ListeningAttribValue>(); attribValues.add(setNewListeningAttribValue(session, "SESSION_ATTRIB_KEY_1")); attribValues.add(setNewListeningAttribValue(session, "SESSION_ATTRIB_KEY_2")); attribValues.add(setNewListeningAttribValue(session, "SESSION_ATTRIB_KEY_3")); attribValues.add(setNewListeningAttribValue(session, "SESSION_ATTRIB_KEY_4")); attribValues.add(setNewListeningAttribValue(session, "SESSION_ATTRIB_KEY_5")); final Set<Throwable> failures = Collections.synchronizedSet(new HashSet<Throwable>()); final int workersCnt = 5; final CyclicBarrier invalidateBarrier = new CyclicBarrier(workersCnt); final CyclicBarrier testExitBarrier = new CyclicBarrier(workersCnt + 1); class Worker extends Thread { public void run() { try { invalidateBarrier.await(10, TimeUnit.SECONDS); session.invalidate(); } catch ( Throwable t ) { failures.add(t); } finally { try { testExitBarrier.await(); } catch ( Throwable t ) {} } } } allowGetAndUnsetCurrentSession(session); Worker[] workers = new Worker[workersCnt]; for ( int p = 0; p < workersCnt; p++) { workers[p] = new Worker(); workers[p].start(); } try { testExitBarrier.await(); } catch ( InterruptedException e ) { } catch ( BrokenBarrierException e ) {} assertEquals(Collections.synchronizedSet(new HashSet<Throwable>()), failures); for ( ListeningAttribValue attribValue : attribValues ) { assertEquals(1, attribValue.httpSessionValueUnboundInvokedWith.size()); assertEquals(1, attribValue.sessionValueUnboundInvokedWith.size()); } } /* Questioning the cost/benefit ration of the following: public void testConcurrentInvalidationAndSetAttribute() { fail("implement me"); } public void testConcurrentClear() { fail("implement me"); } public void testConcurrentSetAttributeAndGetAttributeNames() { fail("implement me"); } public void testConcurrentClearExcept() { fail("implement me"); } */ }