/* * Copyright 2009 Martin Grotzke * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package de.javakaffee.web.msm; import static de.javakaffee.web.msm.SessionValidityInfo.encode; import static de.javakaffee.web.msm.integration.TestUtils.STICKYNESS_PROVIDER; import static de.javakaffee.web.msm.integration.TestUtils.createContext; import static de.javakaffee.web.msm.integration.TestUtils.createSession; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; import static org.testng.Assert.*; import java.util.Arrays; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nonnull; import de.javakaffee.web.msm.storage.MemcachedStorageClient; import net.spy.memcached.MemcachedClient; import net.spy.memcached.internal.OperationFuture; import net.spy.memcached.transcoders.Transcoder; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.connector.Request; import org.apache.catalina.core.StandardContext; import org.mockito.ArgumentCaptor; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import de.javakaffee.web.msm.BackupSessionTask.BackupResult; import de.javakaffee.web.msm.LockingStrategy.LockingMode; import de.javakaffee.web.msm.MemcachedSessionService.SessionManager; import de.javakaffee.web.msm.integration.TestUtils; import de.javakaffee.web.msm.integration.TestUtils.SessionAffinityMode; /** * Test the {@link MemcachedSessionService}. * * @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a> */ public abstract class MemcachedSessionServiceTest { private MemcachedSessionService _service; private MemcachedClient _memcachedMock; private ExecutorService _executor; @SuppressWarnings("unchecked") @BeforeMethod public void setup() throws Exception { final StandardContext context = createContext(); context.setBackgroundProcessorDelay( 1 ); // needed for test of updateExpiration final SessionManager manager = createSessionManager(context); _service = manager.getMemcachedSessionService(); _service.setMemcachedNodes( "n1:127.0.0.1:11211" ); _service.setSessionBackupAsync( false ); _service.setSticky( true ); _memcachedMock = mock( MemcachedClient.class ); final OperationFuture<Boolean> setResultMock = mock( OperationFuture.class ); when( setResultMock.get( ) ).thenReturn( Boolean.TRUE ); when( setResultMock.get( anyInt(), any( TimeUnit.class ) ) ).thenReturn( Boolean.TRUE ); when( _memcachedMock.set( any( String.class ), anyInt(), any(), any( Transcoder.class ) ) ).thenReturn( setResultMock ); final OperationFuture<Boolean> deleteResultMock = mock( OperationFuture.class ); when( deleteResultMock.get() ).thenReturn( Boolean.TRUE ); when( _memcachedMock.delete( anyString() ) ).thenReturn( deleteResultMock ); startInternal( manager, _memcachedMock ); _executor = Executors.newCachedThreadPool(); } @AfterMethod public void afterMethod() { _executor.shutdown(); } protected void startInternal( @Nonnull final SessionManager manager, @Nonnull final MemcachedClient memcachedMock ) throws LifecycleException { throw new UnsupportedOperationException(); } @Nonnull protected abstract SessionManager createSessionManager(Context context); @Test public void testConfigurationFormatMemcachedNodesFeature44() throws LifecycleException { _service.setMemcachedNodes( "n1:127.0.0.1:11211" ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); Assert.assertEquals( _service.getNodeIds(), Arrays.asList( "n1" ) ); _service.setMemcachedNodes( "n1:127.0.0.1:11211 n2:127.0.0.1:11212" ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); Assert.assertEquals( _service.getNodeIds(), Arrays.asList( "n1", "n2" ) ); _service.setMemcachedNodes( "n1:127.0.0.1:11211,n2:127.0.0.1:11212" ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); Assert.assertEquals( _service.getNodeIds(), Arrays.asList( "n1", "n2" ) ); } @Test public void testConfigurationFormatFailoverNodesFeature44() throws LifecycleException { _service.setMemcachedNodes( "n1:127.0.0.1:11211 n2:127.0.0.1:11212" ); _service.setFailoverNodes( "n1" ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); Assert.assertEquals( _service.getFailoverNodeIds(), Arrays.asList( "n1" ) ); _service.setMemcachedNodes( "n1:127.0.0.1:11211 n2:127.0.0.1:11212 n3:127.0.0.1:11213" ); _service.setFailoverNodes( "n1 n2" ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); Assert.assertEquals( _service.getFailoverNodeIds(), Arrays.asList( "n1", "n2" ) ); _service.setMemcachedNodes( "n1:127.0.0.1:11211 n2:127.0.0.1:11212 n3:127.0.0.1:11213" ); _service.setFailoverNodes( "n1,n2" ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); Assert.assertEquals( _service.getFailoverNodeIds(), Arrays.asList( "n1", "n2" ) ); } /** * Test for issue #105: Make memcached node optional for single-node setup * http://code.google.com/p/memcached-session-manager/issues/detail?id=105 */ @Test public void testConfigurationFormatMemcachedNodesFeature105() throws LifecycleException { _service.setMemcachedNodes( "127.0.0.1:11211" ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); assertEquals(_service.getMemcachedNodesManager().getCountNodes(), 1); assertEquals(_service.getMemcachedNodesManager().isEncodeNodeIdInSessionId(), false); assertEquals(_service.getMemcachedNodesManager().isValidForMemcached("123456"), true); _service.shutdown(); _service.setMemcachedNodes( "n1:127.0.0.1:11211" ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); assertEquals(_service.getMemcachedNodesManager().getCountNodes(), 1); assertEquals(_service.getMemcachedNodesManager().isEncodeNodeIdInSessionId(), true); assertEquals(_service.getMemcachedNodesManager().isValidForMemcached("123456"), false); assertEquals(_service.getMemcachedNodesManager().isValidForMemcached("123456-n1"), true); } /** * Test for issue #105: Make memcached node optional for single-node setup * http://code.google.com/p/memcached-session-manager/issues/detail?id=105 */ @Test public void testBackupSessionFailureWithoutMemcachedNodeIdConfigured105() throws Exception { _service.setMemcachedNodes( "127.0.0.1:11211" ); _service.setSessionBackupAsync(false); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); final MemcachedBackupSession session = createSession( _service ); session.access(); session.endAccess(); session.setAttribute( "foo", "bar" ); @SuppressWarnings( "unchecked" ) final OperationFuture<Boolean> futureMock = mock( OperationFuture.class ); when( futureMock.get( ) ).thenThrow(new ExecutionException(new RuntimeException("Simulated exception."))); when( futureMock.get( anyInt(), any( TimeUnit.class ) ) ).thenThrow(new ExecutionException(new RuntimeException("Simulated exception."))); when( _memcachedMock.set( eq( session.getId() ), anyInt(), any(), any( Transcoder.class ) ) ).thenReturn( futureMock ); final BackupResult backupResult = _service.backupSession( session.getIdInternal(), false, null ).get(); assertEquals(backupResult.getStatus(), BackupResultStatus.FAILURE); verify( _memcachedMock, times( 1 ) ).set( eq( session.getId() ), anyInt(), any(), any( Transcoder.class ) ); } /** * Test that sessions are only backuped if they are modified. * @throws ExecutionException * @throws InterruptedException */ @Test public void testOnlySendModifiedSessions() throws InterruptedException, ExecutionException { final MemcachedBackupSession session = createSession( _service ); /* simulate the first request, with session access */ session.access(); session.endAccess(); session.setAttribute( "foo", "bar" ); _service.backupSession( session.getIdInternal(), false, null ).get(); verify( _memcachedMock, times( 1 ) ).set( eq( session.getId() ), anyInt(), any(), any( Transcoder.class ) ); // we need some millis between last backup and next access (due to check in BackupSessionService) Thread.sleep(5L); /* simulate the second request, with session access */ session.access(); session.endAccess(); session.setAttribute( "foo", "bar" ); session.setAttribute( "bar", "baz" ); _service.backupSession( session.getIdInternal(), false, null ).get(); verify( _memcachedMock, times( 2 ) ).set( eq( session.getId() ), anyInt(), any(), any( Transcoder.class ) ); // we need some millis between last backup and next access (due to check in BackupSessionService) Thread.sleep(5L); /* simulate the third request, without session access */ _service.backupSession( session.getIdInternal(), false, null ).get(); verify( _memcachedMock, times( 2 ) ).set( eq( session.getId() ), anyInt(), any(), any( Transcoder.class ) ); } /** * Test that session attribute serialization and hash calculation is only * performed if session attributes were accessed since the last backup. * Otherwise this computing time shall be saved for a better world :-) * @throws ExecutionException * @throws InterruptedException */ @Test public void testOnlyHashAttributesOfAccessedAttributes() throws InterruptedException, ExecutionException { final TranscoderService transcoderServiceMock = mock( TranscoderService.class ); @SuppressWarnings( "unchecked" ) final ConcurrentMap<String, Object> anyMap = any( ConcurrentMap.class ); when( transcoderServiceMock.serializeAttributes( any( MemcachedBackupSession.class ), anyMap ) ).thenReturn( new byte[0] ); _service.setTranscoderService( transcoderServiceMock ); final MemcachedBackupSession session = createSession( _service ); session.access(); session.endAccess(); session.setAttribute( "foo", "bar" ); _service.backupSession( session.getIdInternal(), false, null ).get(); verify( transcoderServiceMock, times( 1 ) ).serializeAttributes( eq( session ), eq( session.getAttributesInternal() ) ); session.access(); session.endAccess(); _service.backupSession( session.getIdInternal(), false, null ).get(); verify( transcoderServiceMock, times( 1 ) ).serializeAttributes( eq( session ), eq( session.getAttributesInternal() ) ); } /** * Test that session attribute serialization and hash calculation is only * performed if the session and its attributes were accessed since the last backup/backup check. * Otherwise this computing time shall be saved for a better world :-) * @throws ExecutionException * @throws InterruptedException */ @Test public void testOnlyHashAttributesOfAccessedSessionsAndAttributes() throws InterruptedException, ExecutionException { final TranscoderService transcoderServiceMock = mock( TranscoderService.class ); @SuppressWarnings( "unchecked" ) final ConcurrentMap<String, Object> anyMap = any( ConcurrentMap.class ); when( transcoderServiceMock.serializeAttributes( any( MemcachedBackupSession.class ), anyMap ) ).thenReturn( new byte[0] ); _service.setTranscoderService( transcoderServiceMock ); final MemcachedBackupSession session = createSession( _service ); session.setAttribute( "foo", "bar" ); _service.backupSession( session.getIdInternal(), false, null ).get(); verify( transcoderServiceMock, times( 1 ) ).serializeAttributes( eq( session ), eq( session.getAttributesInternal() ) ); // we need some millis between last backup and next access (due to check in BackupSessionService) Thread.sleep(5L); session.access(); session.getAttribute( "foo" ); _service.backupSession( session.getIdInternal(), false, null ).get(); verify( transcoderServiceMock, times( 2 ) ).serializeAttributes( eq( session ), eq( session.getAttributesInternal() ) ); // we need some millis between last backup and next access (due to check in BackupSessionService) Thread.sleep(5L); _service.backupSession( session.getIdInternal(), false, null ).get(); verify( transcoderServiceMock, times( 2 ) ).serializeAttributes( eq( session ), eq( session.getAttributesInternal() ) ); } /** * Test for issue #68: External change of sessionId must be handled correctly. * * When the webapp is configured with BASIC auth the sessionId is changed on login since 6.0.21 * (AuthenticatorBase.register invokes manager.changeSessionId(session)). * This change of the sessionId was not recognized by msm so that it might have happened that the * session is removed from memcached under the old id but not sent to memcached (if the case the session * was not accessed during this request at all, which is very unprobable but who knows). */ @Test( dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testChangeSessionId( final SessionAffinityMode stickyness ) throws InterruptedException, ExecutionException, TimeoutException { _service.setStickyInternal( stickyness.isSticky() ); if ( !stickyness.isSticky() ) { _service.setLockingMode( LockingMode.NONE, null, false ); } final MemcachedBackupSession session = createSession( _service ); session.setAttribute( "foo", "bar" ); _service.backupSession( session.getIdInternal(), false, "foo" ).get(); final String oldSessionId = session.getId(); _service.getManager().changeSessionId( session ); // on session backup we specify sessionIdChanged as false as we're not aware of this fact _service.backupSession( session.getIdInternal(), false, "foo" ); // remove session with old id and add it with the new id verify( _memcachedMock, times( 1 ) ).delete( eq( oldSessionId ) ); verify( _memcachedMock, times( 1 ) ).set( eq( session.getId() ), anyInt(), any(), any( Transcoder.class ) ); if ( !stickyness.isSticky() ) { Thread.sleep(200l); // check validity info verify( _memcachedMock, times( 1 ) ).delete( eq( new SessionIdFormat().createValidityInfoKeyName( oldSessionId ) ) ); verify( _memcachedMock, times( 1 ) ).set( eq( new SessionIdFormat().createValidityInfoKeyName( session.getId() ) ), anyInt(), any(), any( Transcoder.class ) ); } } /** * Test that sessions with a timeout of 0 or less are stored in memcached with unlimited * expiration time (0) also (see http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt). * For non-sticky sessions that must hold true for all related items stored in memcached (validation, * backup etc.) * * This is the test for issue #88 "Support session-timeout of 0 or less (no session expiration)" * http://code.google.com/p/memcached-session-manager/issues/detail?id=88 */ @Test( dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testSessionTimeoutUnlimitedWithSessionLoaded( final SessionAffinityMode stickyness ) throws InterruptedException, ExecutionException, LifecycleException { _service.setStickyInternal( stickyness.isSticky() ); if ( !stickyness.isSticky() ) { _service.setLockingMode( LockingMode.NONE, null, false ); _service.setMemcachedNodes( "n1:127.0.0.1:11211 n2:127.0.0.1:11212" ); // for backup support _service.startInternal(new MemcachedStorageClient(_memcachedMock)); // we must put in our mock again } final MemcachedBackupSession session = createSession( _service ); session.setMaxInactiveInterval( -1 ); session.access(); session.endAccess(); session.setAttribute( "foo", "bar" ); final String sessionId = session.getId(); _service.backupSession( sessionId, false, null ).get(); verify( _memcachedMock, times( 1 ) ).set( eq( sessionId ), eq( 0 ), any(), any( Transcoder.class ) ); if ( !stickyness.isSticky() ) { // check validity info final String validityKey = new SessionIdFormat().createValidityInfoKeyName( sessionId ); verify( _memcachedMock, times( 1 ) ).set( eq( validityKey ), eq( 0 ), any(), any( Transcoder.class ) ); // As the backup is done asynchronously, we shutdown the executor so that we know the backup // task is executed/finished. _service.getLockingStrategy().getExecutorService().shutdown(); // On windows we need to wait a little bit so that the tasks _really_ have finished (not needed on linux) Thread.sleep(15); final String backupSessionKey = new SessionIdFormat().createBackupKey( sessionId ); verify( _memcachedMock, times( 1 ) ).set( eq( backupSessionKey ), eq( 0 ), any(), any( Transcoder.class ) ); final String backupValidityKey = new SessionIdFormat().createBackupKey( validityKey ); verify( _memcachedMock, times( 1 ) ).set( eq( backupValidityKey ), eq( 0 ), any(), any( Transcoder.class ) ); } } /** * Test that non-sticky sessions with a timeout of 0 or less that have not been loaded by a request * the validity info is stored in memcached with unlimited * expiration time (0) also (see http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt). * For non-sticky sessions that must hold true for all related items stored in memcached (validation, * backup etc.) * * This is the test for issue #88 "Support session-timeout of 0 or less (no session expiration)" * http://code.google.com/p/memcached-session-manager/issues/detail?id=88 */ @Test public void testSessionTimeoutUnlimitedWithNonStickySessionNotLoaded() throws InterruptedException, ExecutionException, LifecycleException, TimeoutException { _service.setStickyInternal( false ); _service.setLockingMode( LockingMode.NONE, null, false ); _service.setMemcachedNodes( "n1:127.0.0.1:11211 n2:127.0.0.1:11212" ); // for backup support _service.startInternal(new MemcachedStorageClient(_memcachedMock)); // we must put in our mock again final String sessionId = "someSessionNotLoaded-n1"; // stub loading of validity info final String validityKey = new SessionIdFormat().createValidityInfoKeyName( sessionId ); final byte[] validityData = encode( -1, System.currentTimeMillis(), System.currentTimeMillis() ); when( _memcachedMock.get( eq( validityKey ), any ( Transcoder.class) ) ).thenReturn( validityData ); // stub session (backup) ping @SuppressWarnings( "unchecked" ) final OperationFuture<Boolean> futureMock = mock( OperationFuture.class ); when( futureMock.get() ).thenReturn( Boolean.FALSE ); when( futureMock.get( anyInt(), any( TimeUnit.class ) ) ).thenReturn( Boolean.FALSE ); when( _memcachedMock.add( any( String.class ), anyInt(), any(), any( Transcoder.class ) ) ).thenReturn( futureMock ); _service.backupSession( sessionId, false, null ).get(); // update validity info verify( _memcachedMock, times( 1 ) ).set( eq( validityKey ), eq( 0 ), any(), any( Transcoder.class ) ); // As the backup is done asynchronously, we shutdown the executor so that we know the backup // task is executed/finished. _service.getLockingStrategy().getExecutorService().shutdown(); // On windows we need to wait a little bit so that the tasks _really_ have finished (not needed on linux) Thread.sleep(15); // ping session verify( _memcachedMock, times( 1 ) ).add( eq( sessionId ), anyInt(), any(), any( Transcoder.class ) ); // ping session backup final String backupSessionKey = new SessionIdFormat().createBackupKey( sessionId ); verify( _memcachedMock, times( 1 ) ).add( eq( backupSessionKey ), anyInt(), any(), any( Transcoder.class ) ); // update validity backup final String backupValidityKey = new SessionIdFormat().createBackupKey( validityKey ); verify( _memcachedMock, times( 1 ) ).set( eq( backupValidityKey ), eq( 0 ), any(), any( Transcoder.class ) ); } /** * Tests sessionAttributeFilter attribute: when excluded attributes are accessed/put the session should * not be marked as touched. */ @SuppressWarnings( "unchecked" ) @Test public void testOnlyHashAttributesOfAccessedFilteredAttributes() throws InterruptedException, ExecutionException { final TranscoderService transcoderServiceMock = mock( TranscoderService.class ); _service.setTranscoderService( transcoderServiceMock ); final MemcachedBackupSession session = createSession( _service ); _service.setSessionAttributeFilter( "^(foo|bar)$" ); session.setAttribute( "baz", "baz" ); session.access(); session.endAccess(); _service.backupSession( session.getIdInternal(), false, null ).get(); verify( transcoderServiceMock, never() ).serializeAttributes( (MemcachedBackupSession)any(), (ConcurrentMap)any() ); } /** * Tests sessionAttributeFilter attribute: only filtered/allowed attributes must be serialized. */ @SuppressWarnings( { "unchecked", "rawtypes" } ) @Test public void testOnlyFilteredAttributesAreIncludedInSessionBackup() throws InterruptedException, ExecutionException { final TranscoderService transcoderServiceMock = mock( TranscoderService.class ); final ConcurrentMap<String, Object> anyMap = any( ConcurrentMap.class ); when( transcoderServiceMock.serializeAttributes( any( MemcachedBackupSession.class ), anyMap ) ).thenReturn( new byte[0] ); _service.setTranscoderService( transcoderServiceMock ); final MemcachedBackupSession session = createSession( _service ); _service.setSessionAttributeFilter( "^(foo|bar)$" ); session.setAttribute( "foo", "foo" ); session.setAttribute( "bar", "bar" ); session.setAttribute( "baz", "baz" ); _service.backupSession( session.getIdInternal(), false, null ).get(); // capture the supplied argument, alternatively we could have used some Matcher (but there seems to be no MapMatcher). final ArgumentCaptor<ConcurrentMap> model = ArgumentCaptor.forClass( ConcurrentMap.class ); verify( transcoderServiceMock, times( 1 ) ).serializeAttributes( eq( session ), model.capture() ); // the serialized attributes must only contain allowed ones assertTrue( model.getValue().containsKey( "foo" ) ); assertTrue( model.getValue().containsKey( "bar" ) ); assertFalse( model.getValue().containsKey( "baz" ) ); } /** * Tests sessionAttributeFilter attribute: only filtered/allowed attributes must be serialized in updateExpirationInMemcached. */ @SuppressWarnings( { "unchecked", "rawtypes" } ) @Test public void testOnlyFilteredAttributesAreIncludedDuringUpdateExpiration() throws InterruptedException, ExecutionException { final TranscoderService transcoderServiceMock = mock( TranscoderService.class ); final ConcurrentMap<String, Object> anyMap = any( ConcurrentMap.class ); when( transcoderServiceMock.serializeAttributes( any( MemcachedBackupSession.class ), anyMap ) ).thenReturn( new byte[0] ); _service.setTranscoderService( transcoderServiceMock ); final MemcachedBackupSession session = createSession( _service ); _service.setSessionAttributeFilter( "^(foo|bar)$" ); session.setAttribute( "foo", "foo" ); session.setAttribute( "bar", "bar" ); session.setAttribute( "baz", "baz" ); session.access(); session.endAccess(); _service.updateExpirationInMemcached(); // capture the supplied argument, alternatively we could have used some Matcher (but there seems to be no MapMatcher). final ArgumentCaptor<ConcurrentMap> model = ArgumentCaptor.forClass( ConcurrentMap.class ); verify( transcoderServiceMock, times( 1 ) ).serializeAttributes( eq( session ), model.capture() ); // the serialized attributes must only contain allowed ones assertTrue( model.getValue().containsKey( "foo" ) ); assertTrue( model.getValue().containsKey( "bar" ) ); assertFalse( model.getValue().containsKey( "baz" ) ); } @Test public void testSessionsRefCountHandlingIssue111() throws Exception { _service.setSticky(false); _service.setLockingMode(LockingMode.ALL.name()); final TranscoderService transcoderService = new TranscoderService(new JavaSerializationTranscoder()); _service.setTranscoderService( transcoderService ); _service.setStorageClient(new MemcachedStorageClient(_memcachedMock)); _service.startInternal(); @SuppressWarnings("unchecked") final OperationFuture<Boolean> addResultMock = mock(OperationFuture.class); when(addResultMock.get()).thenReturn(true); when(addResultMock.get(anyLong(), any(TimeUnit.class))).thenReturn(true); when(_memcachedMock.add(anyString(), anyInt(), any(), any(Transcoder.class))).thenReturn(addResultMock); final MemcachedBackupSession session = createSession( _service ); // the session is now already added to the internal session map assertNotNull(session.getId()); Future<BackupResult> result = _service.backupSession(session.getId(), false, null); assertFalse(_service.getManager().getSessionsInternal().containsKey(session.getId())); // start another request that loads the session from mc final Request requestMock = mock(Request.class); when(requestMock.getNote(eq(RequestTrackingContextValve.INVOKED))).thenReturn(Boolean.TRUE); _service.getTrackingHostValve().storeRequestThreadLocal(requestMock); when(_memcachedMock.get(eq(session.getId()), any(Transcoder.class))).thenReturn(transcoderService.serialize(session)); final MemcachedBackupSession session2 = _service.findSession(session.getId()); assertTrue(session2.isLocked()); assertEquals(session2.getRefCount(), 1); session2.setAttribute("foo", "bar"); final CyclicBarrier barrier = new CyclicBarrier(2); // the session is now in the internal session map, // now let's run a concurrent request final Future<BackupResult> request2 = _executor.submit(new Callable<BackupResult>() { @Override public BackupResult call() throws Exception { final MemcachedBackupSession session3 = _service.findSession(session.getId()); assertSame(session3, session2); assertEquals(session3.getRefCount(), 2); // let the other thread proceed (or wait) barrier.await(); // and wait again so that the other thread can do some work barrier.await(); final Future<BackupResult> result = _service.backupSession(session.getId(), false, null); _service.getTrackingHostValve().resetRequestThreadLocal(); assertEquals(result.get().getStatus(), BackupResultStatus.SUCCESS); // The session should be released now and no longer stored assertFalse(_service.getManager().getSessionsInternal().containsKey(session.getId())); // just some double checking on expectations... assertEquals(session2.getRefCount(), 0); return result.get(); } }); barrier.await(); result = _service.backupSession(session.getId(), false, null); _service.getTrackingHostValve().resetRequestThreadLocal(); assertEquals(result.get().getStatus(), BackupResultStatus.SKIPPED); // This is the important point! assertTrue(_service.getManager().getSessionsInternal().containsKey(session.getId())); // just some double checking on expectations... assertEquals(session2.getRefCount(), 1); // now let the other thread proceed barrier.await(); // and wait for the result, also to get exceptions/assertion errors. request2.get(); } @Test public void testInvalidNonStickySessionDoesNotCallOnBackupWithoutLoadedSessionIssue137() throws Exception { _service.setStickyInternal( false ); _service.setLockingMode( LockingMode.NONE, null, false ); _service.startInternal(new MemcachedStorageClient(_memcachedMock)); // we must put in our mock again final String sessionId = "nonStickySessionToTimeOut-n1"; // For findSession needed final Request requestMock = mock(Request.class); when(requestMock.getNote(eq(RequestTrackingContextValve.INVOKED))).thenReturn(Boolean.TRUE); _service.getTrackingHostValve().storeRequestThreadLocal(requestMock); final MemcachedBackupSession session = _service.findSession(sessionId); assertNull(session); _service.backupSession( sessionId, false, null ).get(); // check that validity info is not loaded - this would trigger the // WARNING: Found no validity info for session id ... final String validityKey = new SessionIdFormat().createValidityInfoKeyName( sessionId ); verify( _memcachedMock, times( 0 ) ).get( eq( validityKey ) ); } }