/* * 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.integration.TestServlet.PARAM_REMOVE; import static de.javakaffee.web.msm.integration.TestServlet.PATH_INVALIDATE; import static de.javakaffee.web.msm.integration.TestUtils.*; import static org.testng.Assert.*; import java.io.IOException; import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nonnull; import net.spy.memcached.ConnectionFactory; import net.spy.memcached.DefaultConnectionFactory; import net.spy.memcached.MemcachedClient; import net.spy.memcached.MemcachedClientIF; import org.apache.catalina.Container; import org.apache.catalina.LifecycleException; import org.apache.catalina.Session; import org.apache.http.HttpException; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.thimbleware.jmemcached.MemCacheDaemon; import de.javakaffee.web.msm.MemcachedNodesManager.StorageClientCallback; import de.javakaffee.web.msm.MemcachedSessionService.SessionManager; import de.javakaffee.web.msm.integration.TestServlet; import de.javakaffee.web.msm.integration.TestUtils; import de.javakaffee.web.msm.integration.TestUtils.Predicates; import de.javakaffee.web.msm.integration.TestUtils.Response; import de.javakaffee.web.msm.integration.TestUtils.SessionAffinityMode; import de.javakaffee.web.msm.integration.TomcatBuilder; import de.javakaffee.web.msm.storage.MemcachedStorageClient.ByteArrayTranscoder; /** * Integration test testing basic session manager functionality. * * @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a> * @version $Id$ */ public abstract class MemcachedSessionManagerIntegrationTest { private static final Log LOG = LogFactory.getLog( MemcachedSessionManagerIntegrationTest.class ); private static final String GROUP_WITHOUT_NODE_ID = "withoutNodeId"; private MemCacheDaemon<?> _daemon; private MemcachedClientIF _memcached; private TomcatBuilder<?> _tomcat1; private int _portTomcat1; private final String _memcachedNodeId = "n1"; private String _memcachedNodes; private DefaultHttpClient _httpClient; private int _memcachedPort; private final StorageClientCallback _storageClientCallback = new StorageClientCallback() { @Override public byte[] get(final String key) { return _memcached.get(key, ByteArrayTranscoder.INSTANCE); } }; @BeforeMethod public void setUp(final Method testMethod) throws Throwable { _portTomcat1 = 18888; _memcachedPort = 21211; final InetSocketAddress address = new InetSocketAddress( "localhost", _memcachedPort ); _daemon = createDaemon( address ); _daemon.start(); final String[] testGroups = testMethod.getAnnotation(Test.class).groups(); final String nodePrefix = testGroups.length == 0 || !GROUP_WITHOUT_NODE_ID.equals(testGroups[0]) ? _memcachedNodeId + ":" : ""; _memcachedNodes = nodePrefix + "localhost:" + _memcachedPort; try { System.setProperty( "org.apache.catalina.startup.EXIT_ON_INIT_FAILURE", "true" ); _tomcat1 = tcBuilder().buildAndStart(); } catch ( final Throwable e ) { LOG.error( "could not start tomcat.", e ); throw e; } _memcached = createMemcachedClient( _memcachedNodes, address ); _httpClient = new DefaultHttpClient(); } private TomcatBuilder<?> tcBuilder() { return getTestUtils().tomcatBuilder().port(_portTomcat1).memcachedNodes(_memcachedNodes).sticky(true).jvmRoute("app1"); } private MemcachedClient createMemcachedClient( final String memcachedNodes, final InetSocketAddress address ) throws IOException, InterruptedException { final MemcachedNodesManager nodesManager = MemcachedNodesManager.createFor(memcachedNodes, null, null, _storageClientCallback); final ConnectionFactory cf = nodesManager.isEncodeNodeIdInSessionId() ? new SuffixLocatorConnectionFactory( nodesManager, nodesManager.getSessionIdFormat(), Statistics.create(), 1000, 1000 ) : new DefaultConnectionFactory(); final MemcachedClient result = new MemcachedClient( cf, Arrays.asList( address ) ); // Wait a little bit, so that the memcached client can connect and is ready when test starts Thread.sleep( 100 ); return result; } @AfterMethod public void tearDown() throws Exception { _memcached.shutdown(); _tomcat1.stop(); _httpClient.getConnectionManager().shutdown(); _daemon.stop(); } /** * Test for issue 174: sessions lost on Tomcat 7 reload * http://code.google.com/p/memcached-session-manager/issues/detail?id=174 */ @Test( enabled = true ) public void testContextReload() throws IOException, InterruptedException, HttpException { final String sessionId1 = post( _httpClient, _portTomcat1, null, "foo", "bar" ).getSessionId(); assertNotNull( sessionId1, "No session created." ); _tomcat1.getContext().reload(); final Response response = get( _httpClient, _portTomcat1, sessionId1 ); final String actualValue = response.get( "foo" ); assertEquals( "bar", actualValue ); } /** * Test for issue 106: Session not updated in memcached when only a session attribute was removed * http://code.google.com/p/memcached-session-manager/issues/detail?id=106 */ @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testSessionUpdatedInMemcachedWhenSessionAttributeIsRemovedIssue106( final SessionAffinityMode sessionAffinity ) throws IOException, InterruptedException, HttpException { setStickyness(sessionAffinity); final String key = "foo"; final String value = "bar"; final String sessionId1 = post( _httpClient, _portTomcat1, null, key, value ).getSessionId(); assertNotNull( sessionId1, "No session created." ); Response response = get( _httpClient, _portTomcat1, sessionId1 ); assertEquals( response.getSessionId(), sessionId1 ); assertEquals( response.get( key ), value ); final Map<String, String> params = asMap( PARAM_REMOVE, key ); response = get( _httpClient, _portTomcat1, "/", sessionId1, params ); assertEquals( response.getSessionId(), sessionId1 ); assertNull( response.get( key ) ); // also the next request must not include this session attribute response = get( _httpClient, _portTomcat1, sessionId1 ); assertEquals( response.getSessionId(), sessionId1 ); assertNull( response.get( key ) ); } @Test( enabled = true ) public void testConfiguredMemcachedNodeId() throws IOException, InterruptedException, HttpException { final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); /* * test that we have the configured memcachedNodeId in the sessionId, * the session id looks like "<sid>-<memcachedId>[.<jvmRoute>]" */ final String nodeId = sessionId1.substring( sessionId1.indexOf( '-' ) + 1, sessionId1.indexOf( '.' ) ); assertEquals( _memcachedNodeId, nodeId, "Invalid memcached node id" ); } /** * Related to issue/feature 105 (single memcached node without node id): this shall be possible * and the generated session id must not contain a node id. */ @Test( enabled = true, groups = GROUP_WITHOUT_NODE_ID ) public void testSessionIdIsNotChangedIfSingleNodeWithNoMemcachedNodeIdConfigured() throws IOException, InterruptedException, HttpException { final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); assertTrue( sessionId1.indexOf( '-' ) == -1 ); } /** * Related to issue/feature 105 (single memcached node without node id): the session must be * found on a second request. */ @Test( enabled = true, groups = GROUP_WITHOUT_NODE_ID, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testSessionFoundIfSingleNodeWithNoMemcachedNodeIdConfigured( final SessionAffinityMode sessionAffinity ) throws IOException, InterruptedException, HttpException { setStickyness(sessionAffinity); final String key = "foo"; final String value = "bar"; final String sessionId1 = post( _httpClient, _portTomcat1, null, key, value ).getSessionId(); assertNotNull( sessionId1, "No session created." ); final Response response = get( _httpClient, _portTomcat1, sessionId1 ); final String sessionId2 = response.getSessionId(); assertEquals( sessionId2, sessionId1 ); /* check session attributes could be read */ final String actualValue = response.get( key ); assertEquals( value, actualValue ); } @Test( enabled = true ) public void testSessionIdJvmRouteCompatibility() throws IOException, InterruptedException, HttpException { final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); assertTrue( sessionId1.matches( "[^-.]+-[^.]+(\\.[\\w]+)?" ), "Invalid session format, must be <sid>-<memcachedId>[.<jvmRoute>]." ); } /** * Tests, that session ids with an invalid format (not containing the * memcached id) do not cause issues. Instead, we want to retrieve a new * session id. * * @throws IOException * @throws InterruptedException * @throws HttpException */ @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testInvalidSessionId( final SessionAffinityMode sessionAffinity ) throws IOException, InterruptedException, HttpException { setStickyness(sessionAffinity); final String sessionId1 = makeRequest( _httpClient, _portTomcat1, "12345" ); assertNotNull( sessionId1, "No session created." ); assertTrue( sessionId1.indexOf( '-' ) > -1, "Invalid session id format" ); } private void setStickyness(final SessionAffinityMode sessionAffinity) { if(!sessionAffinity.isSticky()) { _tomcat1.getEngine().setJvmRoute(null); } final SessionManager manager = _tomcat1.getManager(); manager.setSticky( sessionAffinity.isSticky() ); try { waitForReconnect(manager.getMemcachedSessionService().getStorageClient(), 1, 500); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } } @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testSessionAvailableInMemcached( final SessionAffinityMode sessionAffinity ) throws IOException, InterruptedException, HttpException { setStickyness(sessionAffinity); final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); Thread.sleep( 50 ); assertNotNull( _memcached.get( sessionId1 ), "Session not available in memcached." ); } @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testSessionAvailableInMemcachedWithCookiesDisabled( final SessionAffinityMode sessionAffinity ) throws Exception { _tomcat1.stop(); _tomcat1 = tcBuilder().sticky(sessionAffinity.isSticky()).cookies(false).jvmRoute("app1").buildAndStart(); final Response response = get(_httpClient, _portTomcat1, null); final String sessionId = response.get( TestServlet.ID ); assertNotNull( sessionId, "No session created." ); Thread.sleep( 50 ); assertNotNull( _memcached.get( sessionId ), "Session not available in memcached." ); } @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testExpiredSessionRemovedFromMemcached( @Nonnull final SessionAffinityMode sessionAffinity ) throws IOException, InterruptedException, HttpException { setStickyness(sessionAffinity); final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); waitForSessionExpiration( sessionAffinity.isSticky() ); assertNull( _memcached.get( sessionId1 ), "Expired session still existing in memcached" ); } @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testInvalidatedSessionRemovedFromMemcached( @Nonnull final SessionAffinityMode sessionAffinity ) throws IOException, InterruptedException, HttpException { setStickyness(sessionAffinity); final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); final Response response = get( _httpClient, _portTomcat1, PATH_INVALIDATE, sessionId1 ); assertNull( response.getResponseSessionId() ); assertEquals(_daemon.getCache().getGetMisses(), 1); // 1 is ok assertNull( _memcached.get( sessionId1 ), "Invalidated session still existing in memcached" ); if(!sessionAffinity.isSticky()) { assertNull( _memcached.get(new SessionIdFormat().createValidityInfoKeyName( sessionId1 )), "ValidityInfo for invalidated session still exists in memcached." ); } } @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testInvalidSessionNotFound( @Nonnull final SessionAffinityMode sessionAffinity ) throws IOException, InterruptedException, HttpException { setStickyness(sessionAffinity); final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); /* * wait some time, as processExpires runs every second and the * maxInactiveTime is set to 1 sec... */ Thread.sleep( 2100 ); final String sessionId2 = makeRequest( _httpClient, _portTomcat1, sessionId1 ); assertNotSame( sessionId1, sessionId2, "Expired session returned." ); } /** * Tests, that for a session that was not sent to memcached (because it's attributes * were not modified), the expiration is updated so that they don't expire in memcached * before they expire in tomcat. * * @throws Exception if something goes wrong with the http communication with tomcat */ @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testExpirationOfSessionsInMemcachedIfBackupWasSkippedSimple( final SessionAffinityMode stickyness ) throws Exception { final SessionManager manager = _tomcat1.getManager(); setStickyness(stickyness); // set to 1 sec above (in setup), default is 10 seconds final int delay = manager.getContext().getBackgroundProcessorDelay(); manager.setMaxInactiveInterval( delay * 4 ); final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); assertNotNull( _memcached.get( sessionId1 ), "Session not available in memcached." ); /* after 2 seconds make another request without changing the session, so that * it's not sent to memcached */ Thread.sleep( TimeUnit.SECONDS.toMillis( delay * 2 ) ); assertEquals( makeRequest( _httpClient, _portTomcat1, sessionId1 ), sessionId1, "SessionId should be the same" ); /* after another 3 seconds check that the session is still alive in memcached, * this would have been expired without an updated expiration */ Thread.sleep( TimeUnit.SECONDS.toMillis( delay * 3 ) ); assertNotNull( _memcached.get( sessionId1 ), "Session should still exist in memcached." ); /* after another >1 second (4 seconds since the last request) * the session must be expired in memcached */ Thread.sleep( TimeUnit.SECONDS.toMillis( delay ) + 500 ); // +1000 just to be sure that we're >4 secs assertNotSame( makeRequest( _httpClient, _portTomcat1, sessionId1 ), sessionId1, "The sessionId should have changed due to expired sessin" ); } /** * Tests update of session expiration in memcached (like {@link #testExpirationOfSessionsInMemcachedIfBackupWasSkippedSimple()}) * but for the scenario where many readonly requests occur: in this case, we cannot just use * <em>maxInactiveInterval - secondsSinceLastBackup</em> (in {@link MemcachedSessionService#updateExpirationInMemcached}) * to determine if an expiration update is required, but we must use the last expiration time sent to memcached. * * @throws Exception if something goes wrong with the http communication with tomcat */ @Test( enabled = true, dataProviderClass = TestUtils.class, dataProvider = STICKYNESS_PROVIDER ) public void testExpirationOfSessionsInMemcachedIfBackupWasSkippedManyReadonlyRequests( final SessionAffinityMode stickyness ) throws Exception { final SessionManager manager = _tomcat1.getManager(); setStickyness(stickyness); // set to 1 sec above (in setup), default is 10 seconds final int delay = manager.getContext().getBackgroundProcessorDelay(); manager.setMaxInactiveInterval( delay * 4 ); final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); assertWaitingWithProxy(Predicates.<MemcachedClientIF> notNull(), 200l, _memcached).get( sessionId1 ); /* after 3 seconds make another request without changing the session, so that * it's not sent to memcached */ Thread.sleep( TimeUnit.SECONDS.toMillis( delay * 3 ) ); assertEquals( makeRequest( _httpClient, _portTomcat1, sessionId1 ), sessionId1, "SessionId should be the same" ); assertNotNull( _memcached.get( sessionId1 ), "Session should still exist in memcached." ); /* after another 3 seconds make another request without changing the session */ Thread.sleep( TimeUnit.SECONDS.toMillis( delay * 3 ) ); assertEquals( makeRequest( _httpClient, _portTomcat1, sessionId1 ), sessionId1, "SessionId should be the same" ); assertNotNull( _memcached.get( sessionId1 ), "Session should still exist in memcached." ); /* after another nearly 4 seconds (maxInactiveInterval) check that the session is still alive in memcached, * this would have been expired without an updated expiration */ Thread.sleep( TimeUnit.SECONDS.toMillis( manager.getMaxInactiveInterval() ) - 500 ); assertNotNull( _memcached.get( sessionId1 ), "Session should still exist in memcached." ); /* after another second in sticky mode (more than 4 seconds since the last request), or an two times the * maxInactiveInterval in non-sticky mode (we must keep sessions in memcached with double expirationtime) * the session must be expired in memcached */ Thread.sleep( TimeUnit.SECONDS.toMillis( delay ) + 500 ); assertNotSame( makeRequest( _httpClient, _portTomcat1, sessionId1 ), sessionId1, "The sessionId should have changed due to expired sessin" ); } /** * Test for issue #49: * Sessions not associated with a memcached node don't get associated as soon as a memcached is available * @throws InterruptedException * @throws IOException * @throws TimeoutException * @throws ExecutionException */ @Test( enabled = true ) public void testNotAssociatedSessionGetsAssociatedIssue49() throws InterruptedException, IOException, ExecutionException, TimeoutException { _daemon.stop(); final SessionManager manager = _tomcat1.getManager(); manager.setMaxInactiveInterval( 5 ); manager.setSticky( true ); final SessionIdFormat sessionIdFormat = new SessionIdFormat(); final Session session = manager.createSession( null ); assertNull( sessionIdFormat.extractMemcachedId( session.getId() ) ); _daemon.start(); // Wait so that the daemon will be available and the client can reconnect (async get didn't do the trick) waitForReconnect(manager.getMemcachedSessionService().getStorageClient(), 1, 4000); final String newSessionId = manager.getMemcachedSessionService().changeSessionIdOnMemcachedFailover( session.getId() ); assertNotNull( newSessionId ); assertEquals( newSessionId, session.getId() ); assertEquals( sessionIdFormat.extractMemcachedId( newSessionId ), _memcachedNodeId ); } /** * Test for issue #60 (Add possibility to disable msm at runtime): disable msm */ @Test( enabled = true ) public void testDisableMsmAtRuntime() throws InterruptedException, IOException, ExecutionException, TimeoutException, LifecycleException, HttpException { final SessionManager manager = _tomcat1.getManager(); manager.setSticky( true ); // disable msm, shutdown our server and our client manager.setEnabled( false ); _memcached.shutdown(); _daemon.stop(); checkSessionFunctionalityWithMsmDisabled(); } /** * Test for issue #60 (Add possibility to disable msm at runtime): start msm disabled and afterwards enable */ @Test( enabled = true ) public void testStartMsmDisabled() throws Exception { // shutdown our server and our client _memcached.shutdown(); _daemon.stop(); // start a new tomcat with msm initially disabled _tomcat1.stop(); Thread.sleep( 500 ); final String memcachedNodes = _memcachedNodeId + ":localhost:" + _memcachedPort; _tomcat1 = getTestUtils().tomcatBuilder().port(_portTomcat1).memcachedNodes(memcachedNodes).sticky(true).enabled(false).jvmRoute("app1").buildAndStart(); LOG.info( "Waiting, check logs to see if the client causes any 'Connection refused' logging..." ); Thread.sleep( 1000 ); // some basic tests for session functionality checkSessionFunctionalityWithMsmDisabled(); // start memcached, client and reenable msm _daemon.start(); _memcached = createMemcachedClient( memcachedNodes, new InetSocketAddress( "localhost", _memcachedPort ) ); _tomcat1.getManager().setEnabled( true ); // Wait a little bit, so that msm's memcached client can connect and is ready when test starts Thread.sleep( 100 ); // memcached based stuff should work again final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); assertNotNull( new SessionIdFormat().extractMemcachedId( sessionId1 ), "memcached node id missing with msm switched to enabled" ); Thread.sleep( 50 ); assertNotNull( _memcached.get( sessionId1 ), "Session not available in memcached." ); waitForSessionExpiration( true ); assertNull( _memcached.get( sessionId1 ), "Expired session still existing in memcached" ); } abstract TestUtils<?> getTestUtils(); private void checkSessionFunctionalityWithMsmDisabled() throws IOException, HttpException, InterruptedException { assertTrue( _tomcat1.getManager().getMemcachedSessionService().isSticky() ); final String sessionId1 = makeRequest( _httpClient, _portTomcat1, null ); assertNotNull( sessionId1, "No session created." ); assertNull( new SessionIdFormat().extractMemcachedId( sessionId1 ), "Got a memcached node id, even with msm disabled." ); waitForSessionExpiration( true ); final String sessionId2 = makeRequest( _httpClient, _portTomcat1, sessionId1 ); assertNotSame( sessionId2, sessionId1, "SessionId not changed." ); } private void waitForSessionExpiration(final boolean sticky) throws InterruptedException { final SessionManager manager = _tomcat1.getManager(); assertEquals( manager.getMemcachedSessionService().isSticky(), sticky ); final Container container = manager.getContext(); final long timeout = TimeUnit.SECONDS.toMillis( sticky ? container.getBackgroundProcessorDelay() + manager.getMaxInactiveInterval() : 2 * manager.getMaxInactiveInterval() ) + 1000; Thread.sleep( timeout ); } }