/* * 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.Configurations.MAX_RECONNECT_DELAY_KEY; import static de.javakaffee.web.msm.Configurations.getSystemProperty; import static de.javakaffee.web.msm.Statistics.StatsType.DELETE_FROM_MEMCACHED; import static de.javakaffee.web.msm.Statistics.StatsType.LOAD_FROM_MEMCACHED; import static de.javakaffee.web.msm.Statistics.StatsType.SESSION_DESERIALIZATION; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.security.Principal; import java.util.List; import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.Manager; import org.apache.catalina.Session; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.apache.catalina.session.StandardSession; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import de.javakaffee.web.msm.BackupSessionService.SimpleFuture; import de.javakaffee.web.msm.BackupSessionTask.BackupResult; import de.javakaffee.web.msm.LockingStrategy.LockingMode; import de.javakaffee.web.msm.MemcachedNodesManager.StorageClientCallback; import de.javakaffee.web.msm.storage.StorageClient; /** * This is the core of memcached session manager, managing sessions in memcached. * A {@link SessionManager} interface represents the dependency to tomcats session manager * (which normally keeps sessions in memory). This {@link SessionManager} has to be subclassed * for a concrete major tomcat version (e.g. for 7.x.x) and configured in the context.xml * as manager (see <a href="http://code.google.com/p/memcached-session-manager/wiki/SetupAndConfiguration">SetupAndConfiguration</a>) * for more. The {@link SessionManager} then has to pass configuration settings to this * {@link MemcachedSessionService}. Relevant lifecycle methods are {@link #startInternal()} * and {@link #shutdown()}. * * @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a> */ public class MemcachedSessionService { static enum LockStatus { /** * For sticky sessions or readonly requests with non-sticky sessions there's no lock required. */ LOCK_NOT_REQUIRED, LOCKED, COULD_NOT_AQUIRE_LOCK } public static final String PROTOCOL_TEXT = "text"; public static final String PROTOCOL_BINARY = "binary"; protected static final String NODE_FAILURE = "node.failure"; /** * Used to store the id for a new session in a request note. This is needed * for a context configured with cookie="false" as in this case there's no * set-cookie header with the session id. When the request came in with a * requestedSessionId this will be changed in the case of a tomcat/memcached * failover (via request.changeSessionId, called by the contextValve) so in * this case we don't need to note the new/changed session id. */ protected static final String NEW_SESSION_ID = "msm.session.id"; protected final Log _log = LogFactory.getLog( getClass() ); // -------------------- configuration properties -------------------- /** * The memcached nodes space separated and with the id prefix, e.g. * n1:localhost:11211 n2:localhost:11212 * */ private String _memcachedNodes; /** * The ids of memcached failover nodes separated by space, e.g. * <code>n1 n2</code> * */ private String _failoverNodes; /** * The pattern used for excluding requests from a session-backup, e.g. * <code>.*\.(png|gif|jpg|css|js)$</code>. Is matched against * request.getRequestURI. */ private String _requestUriIgnorePattern; /** * The pattern used for including session attributes to a session-backup, * e.g. <code>^(userName|sessionHistory)$</code>. If not set, all session * attributes will be part of the session-backup. */ private String _sessionAttributeFilter = null; /** * The compiled pattern used for including session attributes to a session-backup, * e.g. <code>^(userName|sessionHistory)$</code>. If not set, all session * attributes will be part of the session-backup. */ private Pattern _sessionAttributePattern = null; /** * Specifies if the session shall be stored asynchronously in memcached as * {@link StorageClient#set(String, int, byte[])} supports it. If this is * false, the timeout set via {@link #setSessionBackupTimeout(int)} is * evaluated. If this is <code>true</code>, the {@link #setBackupThreadCount(int)} * is evaluated. * <p> * By default this property is set to <code>true</code> - the session * backup is performed asynchronously. * </p> */ private boolean _sessionBackupAsync = true; /** * The timeout in milliseconds after that a session backup is considered as * beeing failed. * <p> * This property is only evaluated if sessions are stored synchronously (set * via {@link #setSessionBackupAsync(boolean)}). * </p> * <p> * The default value is <code>100</code> millis. * </p> */ private int _sessionBackupTimeout = 100; /** * The class name of the factory for * {@link net.spy.memcached.transcoders.Transcoder}s. Default class name is * {@link JavaSerializationTranscoderFactory}. */ private String _transcoderFactoryClassName = JavaSerializationTranscoderFactory.class.getName(); /** * Specifies, if iterating over collection elements shall be done on a copy * of the collection or on the collection itself. * <p> * This option can be useful if you have multiple requests running in * parallel for the same session (e.g. AJAX) and you are using * non-thread-safe collections (e.g. {@link java.util.ArrayList} or * {@link java.util.HashMap}). In this case, your application might modify a * collection while it's being serialized for backup in memcached. * </p> * <p> * <strong>Note:</strong> This must be supported by the TranscoderFactory * specified via {@link #setTranscoderFactoryClass(String)}. * </p> */ private boolean _copyCollectionsForSerialization = false; private String _customConverterClassNames; private boolean _enableStatistics = true; private int _backupThreadCount = Runtime.getRuntime().availableProcessors(); private String _memcachedProtocol = PROTOCOL_TEXT; private String _username; private String _password; private final AtomicBoolean _enabled = new AtomicBoolean( true ); private String _storageKeyPrefix = StorageKeyFormat.WEBAPP_VERSION; // -------------------- END configuration properties -------------------- protected Statistics _statistics; /* * the storage client (typically talks to a memcached server, but e.g. Redis is also supported) */ private StorageClient _storage; /* * findSession may be often called in one request. If a session is requested * that we don't have locally stored each findSession invocation would * trigger a memcached request - this would open the door for DOS attacks... * * this solution: use a LRUCache with a timeout to store, which session had * been requested in the last <n> millis. * * this cache is also used to track sessions that are not existing in memcached * or that got invalidated, to be able to handle backupSession (in non-sticky mode) correctly. */ private final LRUCache<String, Boolean> _invalidSessionsCache = new LRUCache<String, Boolean>( 2000, 500 ); private MemcachedNodesManager _memcachedNodesManager; //private LRUCache<String, String> _relocatedSessions; protected TranscoderService _transcoderService; private TranscoderFactory _transcoderFactory; private BackupSessionService _backupSessionService; private boolean _sticky = true; private String _lockingMode; private LockingStrategy _lockingStrategy; private long _operationTimeout = 1000; private int _lockExpiration = 5; private CurrentRequest _currentRequest; private RequestTrackingHostValve _trackingHostValve; private RequestTrackingContextValve _trackingContextValve; protected final SessionManager _manager; private final StorageClientCallback _storageClientCallback = createStorageClientCallback(); public MemcachedSessionService( final SessionManager manager ) { _manager = manager; } /** * Returns the tomcat session manager. * @return the session manager */ @Nonnull public SessionManager getManager() { return _manager; } public static interface SessionManager extends Manager { /** * Must return the configured session cookie name. * @return the session cookie name. */ @Nonnull String getSessionCookieName(); /** * Reads the Set-Cookie header(s) from the given response. */ String[] getSetCookieHeaders(Response response); String generateSessionId(); void expireSession( final String sessionId ); MemcachedBackupSession getSessionInternal( String sessionId ); Map<String, Session> getSessionsInternal(); String getJvmRoute(); /** * Get a string from the underlying resource bundle or return * null if the String is not found. * @param key to desired resource String * @return resource String matching <i>key</i> from underlying * bundle or null if not found. * @throws IllegalArgumentException if <i>key</i> is null. */ String getString(String key); /** * Get a string from the underlying resource bundle and format * it with the given set of arguments. * * @param key to desired resource String * @param args args for placeholders in the string * @return resource String matching <i>key</i> from underlying * bundle or null if not found. * @throws IllegalArgumentException if <i>key</i> is null. */ String getString(final String key, final Object... args); boolean isMaxInactiveIntervalSet(); int getMaxInactiveInterval(); void setMaxInactiveInterval(int interval); int getMaxActiveSessions(); void incrementSessionCounter(); void incrementRejectedSessions(); /** * Remove this Session from the active Sessions for this Manager without * removing it from memcached. * * @param session Session to be removed * @param update Should the expiration statistics be updated (since tomcat7) */ void removeInternal( final Session session, final boolean update ); /** * Must return the initialized status. Must return <code>true</code> if this manager * has already been started. * @return the initialized status */ boolean isInitialized(); @Nonnull MemcachedSessionService getMemcachedSessionService(); /** * Return the Context with which this Manager is associated. */ @Nonnull Context getContext(); /** * Return the Context with which this Manager is associated. */ @Nonnull ClassLoader getContainerClassLoader(); /** * Writes the given Principal to the provided output stream. * @param principal the principal * @param oos the output stream * @throws IOException expected to be declared by the implementation. */ void writePrincipal( @Nonnull Principal principal, @Nonnull ObjectOutputStream oos) throws IOException; /** * Reads the Principal from the given OIS. * @param ois the object input stream to read from. Will be closed by the caller. * @return the deserialized principal * @throws ClassNotFoundException expected to be declared by the implementation. * @throws IOException expected to be declared by the implementation. */ @Nonnull Principal readPrincipal( @Nonnull ObjectInputStream ois ) throws ClassNotFoundException, IOException; /** * Determines if the context has a security contraint with form based login. */ boolean contextHasFormBasedSecurityConstraint(); // --------------------- setters for testing /** * Sets the sticky mode, must be provided for tests at least. * @param sticky the stickyness. */ void setSticky( boolean sticky ); void setEnabled( boolean b ); void setOperationTimeout(long operationTimeout); void setLockExpiration(int lockExpiration); /** * Set the manager checks frequency in seconds. * @param processExpiresFrequency the new manager checks frequency */ void setProcessExpiresFrequency( int processExpiresFrequency ); void setMemcachedNodes( @Nonnull String memcachedNodes ); void setFailoverNodes( String failoverNodes ); void setLockingMode( @Nullable final String lockingMode ); void setLockingMode( @Nullable final LockingMode lockingMode, @Nullable final Pattern uriPattern, final boolean storeSecondaryBackup ); void setUsername(String username); void setPassword(String password); /** * Creates a new instance of {@link MemcachedBackupSession} (needed so that it's possible to * create specialized {@link MemcachedBackupSession} instances). */ @Nonnull MemcachedBackupSession newMemcachedBackupSession(); /** * Frequency of the session expiration, and related manager operations. * Manager operations will be done once for the specified amount of * backgrondProcess calls (ie, the lower the amount, the most often the * checks will occur). */ int getProcessExpiresFrequency(); } public void shutdown() { _log.info( "Stopping services." ); _manager.getContext().getParent().getPipeline().removeValve(_trackingHostValve); _manager.getContext().getPipeline().removeValve(_trackingContextValve); _backupSessionService.shutdown(); if ( _lockingStrategy != null ) { _lockingStrategy.shutdown(); } if ( _storage != null ) { _storage.shutdown(); _storage = null; } _transcoderFactory = null; _invalidSessionsCache.clear(); } /** * Initialize this manager. The memcachedClient parameter is there for testing * purposes. If the memcachedClient is provided it's used, otherwise a "real"/new * memcached client is created based on the configuration (like {@link #setMemcachedNodes(String)} etc.). * * @param storage the storage client to use, for normal operations this should be <code>null</code>. */ void startInternal( final StorageClient storage ) throws LifecycleException { if (storage == null) _storage = null; else _storage = storage; startInternal(); } /** * Initialize this manager. */ void startInternal() throws LifecycleException { _log.info( getClass().getSimpleName() + " starts initialization... (configured" + " nodes definition " + _memcachedNodes + ", failover nodes " + _failoverNodes + ")" ); _statistics = Statistics.create( _enableStatistics ); _memcachedNodesManager = createMemcachedNodesManager( _memcachedNodes, _failoverNodes); if(_storage == null) { _storage = createStorageClient( _memcachedNodesManager, _statistics ); } final String sessionCookieName = _manager.getSessionCookieName(); _currentRequest = new CurrentRequest(); _trackingHostValve = createRequestTrackingHostValve(sessionCookieName, _currentRequest); final Context context = _manager.getContext(); context.getParent().getPipeline().addValve(_trackingHostValve); _trackingContextValve = createRequestTrackingContextValve(sessionCookieName); context.getPipeline().addValve( _trackingContextValve ); initNonStickyLockingMode( _memcachedNodesManager ); _transcoderService = createTranscoderService( _statistics ); _backupSessionService = new BackupSessionService( _transcoderService, _sessionBackupAsync, _sessionBackupTimeout, _backupThreadCount, _storage, _memcachedNodesManager, _statistics ); _log.info( "--------\n- " + getClass().getSimpleName() + " finished initialization:" + "\n- sticky: "+ _sticky + "\n- operation timeout: " + _operationTimeout + "\n- node ids: " + _memcachedNodesManager.getPrimaryNodeIds() + "\n- failover node ids: " + _memcachedNodesManager.getFailoverNodeIds() + "\n- storage key prefix: " + _memcachedNodesManager.getStorageKeyFormat().prefix + "\n- locking mode: " + _lockingMode + " (expiration: " + _lockExpiration + "s)" + "\n--------"); } protected RequestTrackingContextValve createRequestTrackingContextValve(final String sessionCookieName) { return new RequestTrackingContextValve(sessionCookieName, this); } protected RequestTrackingHostValve createRequestTrackingHostValve(final String sessionCookieName, final CurrentRequest currentRequest) { return new RequestTrackingHostValve(_requestUriIgnorePattern, sessionCookieName, this, _statistics, _enabled, currentRequest) { @Override protected String[] getSetCookieHeaders(final Response response) { return _manager.getSetCookieHeaders(response); } }; } protected StorageClientCallback createStorageClientCallback() { return new StorageClientCallback() { @Override public byte[] get(final String key) { return _storage.get(_memcachedNodesManager.getStorageKeyFormat().format( key )); } }; } protected MemcachedNodesManager createMemcachedNodesManager(final String memcachedNodes, final String failoverNodes) { final Context context = _manager.getContext(); final String webappVersion = Reflections.invoke(context, "getWebappVersion", null); final StorageKeyFormat storageKeyFormat = StorageKeyFormat.of(_storageKeyPrefix, context.getParent().getName(), context.getName(), webappVersion); return MemcachedNodesManager.createFor( memcachedNodes, failoverNodes, storageKeyFormat, _storageClientCallback); } private TranscoderService createTranscoderService( final Statistics statistics ) { return new TranscoderService( getTranscoderFactory().createTranscoder( _manager ) ); } protected TranscoderFactory getTranscoderFactory() { if ( _transcoderFactory == null ) { try { _transcoderFactory = createTranscoderFactory(); } catch ( final Exception e ) { throw new RuntimeException( "Could not create transcoder factory.", e ); } } return _transcoderFactory; } protected StorageClient createStorageClient(final MemcachedNodesManager memcachedNodesManager, final Statistics statistics ) { if ( ! _enabled.get() ) { return null; } final long maxReconnectDelay = getSystemProperty(MAX_RECONNECT_DELAY_KEY, 30); return new StorageClientFactory().createStorageClient(memcachedNodesManager, _memcachedProtocol, _username, _password, _operationTimeout, maxReconnectDelay, statistics); } private TranscoderFactory createTranscoderFactory() throws InstantiationException, IllegalAccessException, ClassNotFoundException { _log.info( "Creating transcoder factory " + _transcoderFactoryClassName ); final Class<? extends TranscoderFactory> transcoderFactoryClass = loadTranscoderFactoryClass(); final TranscoderFactory transcoderFactory = transcoderFactoryClass.newInstance(); transcoderFactory.setCopyCollectionsForSerialization( _copyCollectionsForSerialization ); if ( _customConverterClassNames != null ) { _log.info( "Found configured custom converter classes, setting on transcoder factory: " + _customConverterClassNames ); transcoderFactory.setCustomConverterClassNames( _customConverterClassNames.split( ",\\s*" ) ); } return transcoderFactory; } private Class<? extends TranscoderFactory> loadTranscoderFactoryClass() throws ClassNotFoundException { Class<? extends TranscoderFactory> transcoderFactoryClass; final ClassLoader classLoader = _manager.getContainerClassLoader(); try { _log.debug( "Loading transcoder factory class " + _transcoderFactoryClassName + " using classloader " + classLoader ); transcoderFactoryClass = Class.forName( _transcoderFactoryClassName, false, classLoader ).asSubclass( TranscoderFactory.class ); } catch ( final ClassNotFoundException e ) { _log.info( "Could not load transcoderfactory class with classloader "+ classLoader +", trying " + getClass().getClassLoader() ); transcoderFactoryClass = Class.forName( _transcoderFactoryClassName, false, getClass().getClassLoader() ).asSubclass( TranscoderFactory.class ); } return transcoderFactoryClass; } /** * {@inheritDoc} */ public String newSessionId( @Nonnull final String sessionId ) { return _memcachedNodesManager.createSessionId( sessionId ); } /** * Return the active Session, associated with this Manager, with the * specified session id (if any); otherwise return <code>null</code>. * * @param id * The session id for the session to be returned * @return the session or <code>null</code> if no session was found locally * or in memcached. * * @exception IllegalStateException * if a new session cannot be instantiated for any reason * @exception IOException * if an input/output error occurs while processing this * request */ public MemcachedBackupSession findSession( final String id ) throws IOException { MemcachedBackupSession result = _manager.getSessionInternal( id ); if ( result != null ) { // TODO: document ignoring requests and container managed authentication // -> with container managed auth protected resources should not be ignored // TODO: check ignored resource also below if (!_sticky && !_trackingHostValve.isIgnoredRequest() && !isContainerSessionLookup()) { synchronized (_manager.getSessionsInternal()) { // the session could have been removed in the meantime... if(_manager.getSessionInternal(id) == null) { addValidLoadedSession(result); } result.registerReference(); } } } else if ( canHitMemcached( id ) && _invalidSessionsCache.get( id ) == null ) { // If no current request is set (RequestTrackerHostValve was not passed) we got invoked // by CoyoteAdapter.parseSessionCookiesId - here we can just return null, the requestedSessionId // will be accepted anyway. // If form based security is used, then AuthenticatorBase.invoke might ask for the session (to get the authenticated principal), // in this case we must return the session because valid requests would be rejected otherwise. if(!_sticky && ( isConnectorSessionLookup() || _trackingHostValve.isIgnoredRequest() && !_manager.contextHasFormBasedSecurityConstraint())) { if(_log.isDebugEnabled()) { _log.debug("Returning for session id " + id + " (isConnectorSessionLookup: "+ isConnectorSessionLookup() + ", isIgnoredRequest: " + _trackingHostValve.isIgnoredRequest() + ")"); } return null; } // else load the session from memcached result = loadFromMemcached( id ); // checking valid() would expire() the session if it's not valid! if ( result != null && result.isValid() ) { if(!_sticky) { // synchronized to have correct refcounts synchronized (_manager.getSessionsInternal()) { // in the meantime another request might have loaded and added the session, // and we must ensure to have a single session instance per id to have // correct refcounts (otherwise a session might be removed from the map at // the end of #backupSession if(_manager.getSessionInternal(id) != null) { result = _manager.getSessionInternal(id); } else { addValidLoadedSession(result); } result.registerReference(); // _log.info("Registering reference, isContainerSessionLookup(): " + isContainerSessionLookup(), new RuntimeException("foo")); } } else { addValidLoadedSession(result); } } } return result; } /** * Is used to determine if this thread / the current request already hit the application or if this method * invocation comes from the container. */ private boolean isContainerSessionLookup() { return !_trackingContextValve.wasInvokedWith(_currentRequest.get()); } /** * Determines if the request has already passed the RequestTrackerHostValve or not. * If not, e.g. CoyoteAdapter.parseSessionCookiesId (invoked from CoyoteAdapter.postParseRequest) might ask * for the session. */ private boolean isConnectorSessionLookup() { return _currentRequest.get() == null; } private void addValidLoadedSession(final MemcachedBackupSession result) { // When the sessionId will be changed later in changeSessionIdOnTomcatFailover/handleSessionTakeOver // (due to a tomcat failover) we don't want to notify listeners via session.activate for the // old sessionId but do that later (in handleSessionTakeOver) // See also http://code.google.com/p/memcached-session-manager/issues/detail?id=92 String jvmRoute; final boolean sessionIdWillBeChanged = _sticky && ( jvmRoute = _manager.getJvmRoute() ) != null && !jvmRoute.equals( getSessionIdFormat().extractJvmRoute( result.getId() ) ); final boolean activate = !sessionIdWillBeChanged; addValidLoadedSession( result, activate ); } private void addValidLoadedSession( final StandardSession session, final boolean activate ) { // make sure the listeners know about it. (as done by PersistentManagerBase) if ( session.isNew() ) { session.tellNew(); } _manager.add( session ); if ( activate ) { session.activate(); } // endAccess() to ensure timeouts happen correctly. // access() to keep access count correct or it will end up // negative session.access(); session.endAccess(); } /** * {@inheritDoc} */ public MemcachedBackupSession createSession( String sessionId ) { if ( _log.isDebugEnabled() ) { _log.debug( "createSession invoked: " + sessionId ); } checkMaxActiveSessions(); final MemcachedBackupSession session = createEmptySession(); session.setNew( true ); session.setValid( true ); session.setCreationTime( System.currentTimeMillis() ); session.setMaxInactiveInterval( _manager.isMaxInactiveIntervalSet() ? _manager.getMaxInactiveInterval() : _manager.getContext().getSessionTimeout() * 60 ); if ( sessionId == null || !_memcachedNodesManager.canHitMemcached( sessionId ) ) { sessionId = _manager.generateSessionId(); } session.setId( sessionId ); final Request request = _currentRequest.get(); if(request != null) { request.setNote(NEW_SESSION_ID, sessionId); } // we must register us as holding a reference, otherwise we might remove the session too early. (#283) if(!_sticky) { // synchronized to have correct refcounts synchronized (_manager.getSessionsInternal()) { session.registerReference(); } } if ( _log.isDebugEnabled() ) { _log.debug( "Created new session with id " + session.getId() ); } _manager.incrementSessionCounter(); //if the new session exist in _invalidSessionsCache, we should remove it marking this session valid.(#284) if( _invalidSessionsCache.containsKey(session.getId()) ){ if ( _log.isDebugEnabled() ) { _log.debug( "Remove session id " + session.getId() + " from _invalidSessionsCache, marking new session valid" ); } _invalidSessionsCache.remove(session.getId()); } return session; } /** * Is invoked when a session was removed from the manager, e.g. because the * session has been invalidated. * * Is used to release a lock if the non-stick session was locked * * It's also used to keep track of such sessions in non-sticky mode, so that * lockingStrategy.onBackupWithoutLoadedSession is not invoked (see issue 116). * * @param session the removed session. */ public void sessionRemoved(final MemcachedBackupSession session) { if(!_sticky) { if(session.isLocked()) { _lockingStrategy.releaseLock(session.getIdInternal()); session.releaseLock(); } _invalidSessionsCache.put(session.getIdInternal(), Boolean.TRUE); } } private void checkMaxActiveSessions() { if ( _manager.getMaxActiveSessions() >= 0 && _manager.getSessionsInternal().size() >= _manager.getMaxActiveSessions() ) { _manager.incrementRejectedSessions(); throw new IllegalStateException (_manager.getString("standardManager.createSession.ise")); } } /** * {@inheritDoc} */ public MemcachedBackupSession createEmptySession() { final MemcachedBackupSession result = _manager.newMemcachedBackupSession(); result.setSticky( _sticky ); return result; } /** * Check if the given session id does not belong to this tomcat (according to the * local jvmRoute and the jvmRoute in the session id). If the session contains a * different jvmRoute load if from memcached. If the session was found in memcached and * if it's valid it must be associated with this tomcat and therefore the session id has to * be changed. The new session id must be returned if it was changed. * <p> * This is only useful for sticky sessions, in non-sticky operation mode <code>null</code> should * always be returned. * </p> * * @param requestedSessionId * the sessionId that was requested. * * @return the new session id if the session is taken over and the id was changed. * Otherwise <code>null</code>. * * @see Request#getRequestedSessionId() */ public String changeSessionIdOnTomcatFailover( final String requestedSessionId ) { if ( !_sticky ) { return null; } final String localJvmRoute = _manager.getJvmRoute(); if ( localJvmRoute != null && !localJvmRoute.equals( getSessionIdFormat().extractJvmRoute( requestedSessionId ) ) ) { // the session might already be relocated, e.g. if some ajax calls are running concurrently. // if we'd run session takeover again, a new empty session would be created. // see https://github.com/magro/memcached-session-manager/issues/282 final String newSessionId = _memcachedNodesManager.changeSessionIdForTomcatFailover(requestedSessionId, _manager.getJvmRoute()); if (_manager.getSessionInternal(newSessionId) != null) { return newSessionId; } // the session might have been loaded already (by some valve), so let's check our session map MemcachedBackupSession session = _manager.getSessionInternal( requestedSessionId ); if ( session == null ) { session = loadFromMemcachedWithCheck( requestedSessionId ); } // checking valid() can expire() the session! if ( session != null && session.isValid() ) { return handleSessionTakeOver( session ); } else if (_manager.getSessionInternal(newSessionId) != null) { return newSessionId; } } return null; } @Nonnull private SessionIdFormat getSessionIdFormat() { return _memcachedNodesManager.getSessionIdFormat(); } private String handleSessionTakeOver( final MemcachedBackupSession session ) { checkMaxActiveSessions(); final String origSessionId = session.getIdInternal(); final String newSessionId = _memcachedNodesManager.changeSessionIdForTomcatFailover(session.getIdInternal(), _manager.getJvmRoute()); // If this session was already loaded we need to remove it from the session map // See http://code.google.com/p/memcached-session-manager/issues/detail?id=92 if ( _manager.getSessionsInternal().containsKey( origSessionId ) ) { _manager.getSessionsInternal().remove( origSessionId ); } session.setIdInternal( newSessionId ); // a concurrent/earlier request might already have added the session (#282) if ( !_manager.getSessionsInternal().containsKey( newSessionId ) ) { addValidLoadedSession(session, true); deleteFromMemcached(origSessionId); _statistics.requestWithTomcatFailover(); } return newSessionId; } protected void deleteFromMemcached(final String sessionId) { if ( _enabled.get() && _memcachedNodesManager.isValidForMemcached( sessionId ) ) { if ( _log.isDebugEnabled() ) { _log.debug( "Deleting session from memcached: " + sessionId ); } try { final long start = System.currentTimeMillis(); _storage.delete( _memcachedNodesManager.getStorageKeyFormat().format(sessionId) ).get(); _statistics.registerSince( DELETE_FROM_MEMCACHED, start ); if ( !_sticky ) { _lockingStrategy.onAfterDeleteFromMemcached( sessionId ); } } catch ( final Throwable e ) { _log.info( "Could not delete session from memcached.", e ); } } } /** * Check if the valid session associated with the provided * requested session Id will be relocated with the next {@link #backupSession(Session, boolean)} * and change the session id to the new one (containing the new memcached node). The * new session id must be returned if the session will be relocated and the id was changed. * * @param requestedSessionId * the sessionId that was requested. * * @return the new session id if the session will be relocated and the id was changed. * Otherwise <code>null</code>. * * @see Request#getRequestedSessionId() */ public String changeSessionIdOnMemcachedFailover( final String requestedSessionId ) { if ( !_memcachedNodesManager.isEncodeNodeIdInSessionId() ) { return null; } try { if ( _sticky ) { /* We can just lookup the session in the local session map, as we wouldn't get * the session from memcached if the node was not available - or, the other way round, * if we would get the session from memcached, the session would not have to be relocated. */ final MemcachedBackupSession session = _manager.getSessionInternal( requestedSessionId ); if ( session != null && session.isValid() ) { final String newSessionId = _memcachedNodesManager.getNewSessionIdIfNodeFromSessionIdUnavailable( session.getId() ); if ( newSessionId != null ) { _log.debug( "Session needs to be relocated, setting new id on session..." ); session.setIdForRelocate( newSessionId ); _statistics.requestWithMemcachedFailover(); return newSessionId; } } } else { /* for non-sticky sessions we check the validity info */ final String nodeId = getSessionIdFormat().extractMemcachedId( requestedSessionId ); if ( nodeId == null || _memcachedNodesManager.isNodeAvailable( nodeId ) ) { return null; } _log.info( "Session needs to be relocated as node "+ nodeId +" is not available, loading backup session for " + requestedSessionId ); final MemcachedBackupSession backupSession = loadBackupSession( requestedSessionId ); if ( backupSession != null ) { _log.debug( "Loaded backup session for " + requestedSessionId + ", adding locally with "+ backupSession.getIdInternal() +"." ); addValidLoadedSession( backupSession, true ); _statistics.requestWithMemcachedFailover(); return backupSession.getId(); } } } catch ( final RuntimeException e ) { _log.warn( "Could not find session in local session map.", e ); } return null; } @CheckForNull private MemcachedBackupSession loadBackupSession( @Nonnull final String requestedSessionId ) { final String nodeId = getSessionIdFormat().extractMemcachedId( requestedSessionId ); if ( nodeId == null ) { _log.info( "Cannot load backupSession for sessionId without nodeId: "+ requestedSessionId ); return null; } final String newNodeId = _memcachedNodesManager.getNextAvailableNodeId(nodeId); if ( newNodeId == null ) { _log.info( "No next available node found for nodeId "+ nodeId ); return null; } MemcachedBackupSession result = loadBackupSession(requestedSessionId, newNodeId); String nextNodeId = nodeId; // if we didn't find the backup in the next node, let's go through other nodes // to see if the backup is there. For this we have to fake the session id so that // the SuffixBasedNodeLocator selects another backup node. while(result == null && (nextNodeId = _memcachedNodesManager.getNextAvailableNodeId(nextNodeId)) != null && !nextNodeId.equals(nodeId)) { final String newSessionId = getSessionIdFormat().createNewSessionId(requestedSessionId, nextNodeId); result = loadBackupSession(newSessionId, newNodeId); } if ( result == null ) { _log.info( "No backup found for sessionId " + requestedSessionId ); return null; } return result; } private MemcachedBackupSession loadBackupSession(final String requestedSessionId, final String newNodeId) { try { final SessionValidityInfo validityInfo = _lockingStrategy.loadBackupSessionValidityInfo( requestedSessionId ); if ( validityInfo == null || !validityInfo.isValid() ) { if(_log.isDebugEnabled()) _log.debug( "No validity info (or no valid one) found for sessionId " + requestedSessionId ); return null; } final byte[] obj = _storage.get( getSessionIdFormat().createBackupKey( requestedSessionId ) ); if ( obj == null ) { if(_log.isDebugEnabled()) _log.debug( "No backup found for sessionId " + requestedSessionId ); return null; } final MemcachedBackupSession session = _transcoderService.deserialize( obj, _manager ); session.setSticky( _sticky ); session.setLastAccessedTimeInternal( validityInfo.getLastAccessedTime() ); session.setThisAccessedTimeInternal( validityInfo.getThisAccessedTime() ); final String newSessionId = getSessionIdFormat().createNewSessionId( requestedSessionId, newNodeId ); _log.info( "Session backup loaded from secondary memcached for "+ requestedSessionId +" (will be relocated)," + " setting new id "+ newSessionId +" on session..." ); session.setIdInternal( newSessionId ); return session; } catch( final Exception e ) { _log.error( "Could not get backup validityInfo or backup session for sessionId " + requestedSessionId, e ); return null; } } /** * Is invoked for requests matching {@link #setRequestUriIgnorePattern(String)} at the end * of the request. Any acquired resources should be freed. * @param sessionId the sessionId, must not be null. * @param requestId the uri/id of the request for that the session backup shall be performed, used for readonly tracking. */ public void requestFinished(final String sessionId, final String requestId) { if(!_sticky) { final MemcachedBackupSession msmSession = _manager.getSessionInternal( sessionId ); if ( msmSession == null ) { if(_log.isDebugEnabled()) _log.debug( "No session found in session map for " + sessionId ); return; } if ( !msmSession.isValidInternal() ) { if(_log.isDebugEnabled()) _log.debug( "Non valid session found in session map for " + sessionId ); return; } synchronized (_manager.getSessionsInternal()) { // if another thread in the meantime retrieved the session // we must not remove it as this would case session data loss // for the other request if ( msmSession.releaseReference() > 0 ) { if(_log.isDebugEnabled()) _log.debug( "Session " + sessionId + " is still used by another request, skipping backup and (optional) lock handling/release." ); return; } msmSession.passivate(); _manager.removeInternal( msmSession, false ); } if(msmSession.isLocked()) { _lockingStrategy.releaseLock(sessionId); msmSession.releaseLock(); _lockingStrategy.registerReadonlyRequest(requestId); } } } /** * Backup the session for the provided session id in memcached if the session was modified or * if the session needs to be relocated. In non-sticky session-mode the session should not be * loaded from memcached for just storing it again but only metadata should be updated. * * @param sessionId * the if of the session to backup * @param sessionIdChanged * specifies, if the session id was changed due to a memcached failover or tomcat failover. * @param requestId * the uri of the request for that the session backup shall be performed. * * @return a {@link Future} providing the {@link BackupResultStatus}. */ public Future<BackupResult> backupSession( final String sessionId, final boolean sessionIdChanged, final String requestId ) { if ( !_enabled.get() ) { return new SimpleFuture<BackupResult>( BackupResult.SKIPPED ); } final MemcachedBackupSession msmSession = _manager.getSessionInternal( sessionId ); if ( msmSession == null ) { if(_log.isDebugEnabled()) _log.debug( "No session found in session map for " + sessionId ); if ( !_sticky ) { // Issue 116/137: Only notify the lockingStrategy if the session was loaded and has not been removed/invalidated if(!_invalidSessionsCache.containsKey(sessionId)) { _lockingStrategy.onBackupWithoutLoadedSession( sessionId, requestId, _backupSessionService ); } } return new SimpleFuture<BackupResult>( BackupResult.SKIPPED ); } if ( !msmSession.isValidInternal() ) { if(_log.isDebugEnabled()) _log.debug( "Non valid session found in session map for " + sessionId ); return new SimpleFuture<BackupResult>( BackupResult.SKIPPED ); } if ( !_sticky ) { synchronized (_manager.getSessionsInternal()) { // if another thread in the meantime retrieved the session // we must not remove it as this would case session data loss // for the other request if ( msmSession.releaseReference() > 0 ) { if(_log.isDebugEnabled()) _log.debug( "Session " + sessionId + " is still used by another request, skipping backup and (optional) lock handling/release." ); return new SimpleFuture<BackupResult>( BackupResult.SKIPPED ); } msmSession.passivate(); _manager.removeInternal( msmSession, false ); } } final boolean force = sessionIdChanged || msmSession.isSessionIdChanged() || !_sticky && (msmSession.getSecondsSinceLastBackup() >= msmSession.getMaxInactiveInterval()); final Future<BackupResult> result = _backupSessionService.backupSession( msmSession, force ); if ( !_sticky ) { _lockingStrategy.onAfterBackupSession( msmSession, force, result, requestId, _backupSessionService ); } return result; } @Nonnull byte[] serialize( @Nonnull final MemcachedBackupSession session ) { return _transcoderService.serialize( session ); } protected MemcachedBackupSession loadFromMemcachedWithCheck( final String sessionId ) { if ( !canHitMemcached( sessionId ) || _invalidSessionsCache.get( sessionId ) != null ) { return null; } return loadFromMemcached( sessionId ); } /** * Checks if this manager {@link #isEnabled()}, if the given sessionId is valid (contains a memcached id) * and if this sessionId can access memcached. */ private boolean canHitMemcached( @Nonnull final String sessionId ) { return _enabled.get() && _memcachedNodesManager.canHitMemcached( sessionId ); } /** * Assumes that before you checked {@link #canHitMemcached(String)}. */ private MemcachedBackupSession loadFromMemcached( final String sessionId ) { if ( _log.isDebugEnabled() ) { _log.debug( "Loading session from memcached: " + sessionId ); } LockStatus lockStatus = null; try { if ( !_sticky ) { lockStatus = _lockingStrategy.onBeforeLoadFromMemcached( sessionId ); } final long start = System.currentTimeMillis(); /* In the previous version (<1.2) the session was completely serialized by * custom Transcoder implementations. * Such sessions have set the SERIALIZED flag (from SerializingTranscoder) so that * they get deserialized by BaseSerializingTranscoder.deserialize or the appropriate * specializations. */ final byte[] object = _storage.get( _memcachedNodesManager.getStorageKeyFormat().format( sessionId ) ); _memcachedNodesManager.onLoadFromMemcachedSuccess( sessionId ); if ( object != null ) { final long startDeserialization = System.currentTimeMillis(); final MemcachedBackupSession result = _transcoderService.deserialize( object, _manager ); _statistics.registerSince( SESSION_DESERIALIZATION, startDeserialization ); _statistics.registerSince( LOAD_FROM_MEMCACHED, start ); result.setSticky( _sticky ); if ( !_sticky ) { _lockingStrategy.onAfterLoadFromMemcached( result, lockStatus ); } if ( _log.isDebugEnabled() ) { _log.debug( "Found session with id " + sessionId ); } return result; } else { releaseIfLocked( sessionId, lockStatus ); _invalidSessionsCache.put( sessionId, Boolean.TRUE ); if ( _log.isDebugEnabled() ) { _log.debug( "Session " + sessionId + " not found in memcached." ); } return null; } } catch ( final TranscoderDeserializationException e ) { _log.warn( "Could not deserialize session with id " + sessionId + " from memcached, session will be purged from storage.", e ); releaseIfLocked( sessionId, lockStatus ); _storage.delete( _memcachedNodesManager.getStorageKeyFormat().format(sessionId) ); _invalidSessionsCache.put( sessionId, Boolean.TRUE ); } catch ( final Exception e ) { _log.warn( "Could not load session with id " + sessionId + " from memcached.", e ); releaseIfLocked( sessionId, lockStatus ); } finally { } return null; } protected void releaseIfLocked( final String sessionId, final LockStatus lockStatus ) { if ( lockStatus == LockStatus.LOCKED ) { _lockingStrategy.releaseLock( sessionId ); } } /** * Set the memcached nodes space or comma separated. * <p> * E.g. <code>n1.localhost:11211 n2.localhost:11212</code> * </p> * <p> * When the memcached nodes are set when this manager is already initialized, * the new configuration will be loaded. * </p> * * @param memcachedNodes * the memcached node definitions, whitespace or comma separated */ public void setMemcachedNodes( final String memcachedNodes ) { if ( _manager.isInitialized() ) { final MemcachedNodesManager config = reloadMemcachedConfig( memcachedNodes, _failoverNodes ); _log.info( "Loaded new memcached node configuration." + "\n- Former config: "+ _memcachedNodes + "\n- New config: " + memcachedNodes + "\n- New node ids: " + config.getPrimaryNodeIds() + "\n- New failover node ids: " + config.getFailoverNodeIds() ); } _memcachedNodes = memcachedNodes; } /** * The memcached nodes configuration as provided in the server.xml/context.xml. * <p> * This getter is there to make this configuration accessible via jmx. * </p> * @return the configuration string for the memcached nodes. */ public String getMemcachedNodes() { return _memcachedNodes; } private MemcachedNodesManager reloadMemcachedConfig( final String memcachedNodes, final String failoverNodes ) { /* first create all dependent services */ final MemcachedNodesManager memcachedNodesManager = createMemcachedNodesManager( memcachedNodes, failoverNodes ); final StorageClient storage = createStorageClient( memcachedNodesManager, _statistics ); final BackupSessionService backupSessionService = new BackupSessionService( _transcoderService, _sessionBackupAsync, _sessionBackupTimeout, _backupThreadCount, storage, memcachedNodesManager, _statistics ); /* then assign new services */ if ( _storage != null ) { _storage.shutdown(); } _storage = storage; _memcachedNodesManager = memcachedNodesManager; _backupSessionService = backupSessionService; initNonStickyLockingMode( memcachedNodesManager ); return memcachedNodesManager; } /** * The node ids of memcached nodes, that shall only be used for session * backup by this tomcat/manager, if there are no other memcached nodes * left. Node ids are separated by whitespace or comma. * <p> * E.g. <code>n1 n2</code> * </p> * <p> * When the failover nodes are set when this manager is already initialized, * the new configuration will be loaded. * </p> * * @param failoverNodes * the failoverNodes to set, whitespace or comma separated */ public void setFailoverNodes( final String failoverNodes ) { if ( _manager.isInitialized() ) { final MemcachedNodesManager config = reloadMemcachedConfig( _memcachedNodes, failoverNodes ); _log.info( "Loaded new memcached failover node configuration." + "\n- Former failover config: "+ _failoverNodes + "\n- New failover config: " + failoverNodes + "\n- New node ids: " + config.getPrimaryNodeIds() + "\n- New failover node ids: " + config.getFailoverNodeIds() ); } _failoverNodes = failoverNodes; } /** * The memcached failover nodes configuration as provided in the server.xml/context.xml. * <p> * This getter is there to make this configuration accessible via jmx. * </p> * @return the configuration string for the failover nodes. */ public String getFailoverNodes() { return _failoverNodes; } /** * Set the regular expression for request uris to ignore for session backup. * This should include static resources like images, in the case they are * served by tomcat. * <p> * E.g. <code>.*\.(png|gif|jpg|css|js)$</code> * </p> * * @param requestUriIgnorePattern * the requestUriIgnorePattern to set * @author Martin Grotzke */ public void setRequestUriIgnorePattern( final String requestUriIgnorePattern ) { _requestUriIgnorePattern = requestUriIgnorePattern; } /** * Return the compiled pattern used for including session attributes to a session-backup. * * @return the sessionAttributePattern */ @CheckForNull Pattern getSessionAttributePattern() { return _sessionAttributePattern; } /** * Return the string pattern used for including session attributes to a session-backup. * * @return the sessionAttributeFilter */ @CheckForNull public String getSessionAttributeFilter() { return _sessionAttributeFilter; } /** * Set the pattern used for including session attributes to a session-backup. * If not set, all session attributes will be part of the session-backup. * <p> * E.g. <code>^(userName|sessionHistory)$</code> * </p> * * @param sessionAttributeFilter * the sessionAttributeNames to set */ public void setSessionAttributeFilter( @Nullable final String sessionAttributeFilter ) { if ( sessionAttributeFilter == null || sessionAttributeFilter.trim().equals("") ) { _sessionAttributeFilter = null; _sessionAttributePattern = null; } else { _sessionAttributeFilter = sessionAttributeFilter; _sessionAttributePattern = Pattern.compile( sessionAttributeFilter ); } } /** * The class of the factory that creates the * {@link net.spy.memcached.transcoders.Transcoder} to use for serializing/deserializing * sessions to/from memcached (requires a default/no-args constructor). * The default value is the {@link JavaSerializationTranscoderFactory} class * (used if this configuration attribute is not specified). * <p> * After the {@link TranscoderFactory} instance was created from the specified class, * {@link TranscoderFactory#setCopyCollectionsForSerialization(boolean)} * will be invoked with the currently set <code>copyCollectionsForSerialization</code> propery, which * has either still the default value (<code>false</code>) or the value provided via * {@link #setCopyCollectionsForSerialization(boolean)}. * </p> * * @param transcoderFactoryClassName the {@link TranscoderFactory} class name. */ public void setTranscoderFactoryClass( final String transcoderFactoryClassName ) { _transcoderFactoryClassName = transcoderFactoryClassName; } /** * Specifies, if iterating over collection elements shall be done on a copy * of the collection or on the collection itself. The default value is <code>false</code> * (used if this configuration attribute is not specified). * <p> * This option can be useful if you have multiple requests running in * parallel for the same session (e.g. AJAX) and you are using * non-thread-safe collections (e.g. {@link java.util.ArrayList} or * {@link java.util.HashMap}). In this case, your application might modify a * collection while it's being serialized for backup in memcached. * </p> * <p> * <strong>Note:</strong> This must be supported by the {@link TranscoderFactory} * specified via {@link #setTranscoderFactoryClass(String)}: after the {@link TranscoderFactory} instance * was created from the specified class, {@link TranscoderFactory#setCopyCollectionsForSerialization(boolean)} * will be invoked with the provided <code>copyCollectionsForSerialization</code> value. * </p> * * @param copyCollectionsForSerialization * <code>true</code>, if iterating over collection elements shall be done * on a copy of the collection, <code>false</code> if the collections own iterator * shall be used. */ public void setCopyCollectionsForSerialization( final boolean copyCollectionsForSerialization ) { _copyCollectionsForSerialization = copyCollectionsForSerialization; } /** * Custom converter allow you to provide custom serialization of application specific * types. Multiple converter classes are separated by comma (with optional space following the comma). * <p> * This option is useful if reflection based serialization is very verbose and you want * to provide a more efficient serialization for a specific type. * </p> * <p> * <strong>Note:</strong> This must be supported by the {@link TranscoderFactory} * specified via {@link #setTranscoderFactoryClass(String)}: after the {@link TranscoderFactory} instance * was created from the specified class, {@link TranscoderFactory#setCustomConverterClassNames(String[])} * is invoked with the provided custom converter class names. * </p> * <p>Requirements regarding the specific custom converter classes depend on the * actual serialization strategy, but a common requirement would be that they must * provide a default/no-args constructor.<br/> * For more details have a look at * <a href="http://code.google.com/p/memcached-session-manager/wiki/SerializationStrategies">SerializationStrategies</a>. * </p> * * @param customConverterClassNames a list of class names separated by comma */ public void setCustomConverter( final String customConverterClassNames ) { _customConverterClassNames = customConverterClassNames; } /** * Specifies if statistics (like number of requests with/without session) shall be * gathered. Default value of this property is <code>true</code>. * <p> * Statistics will be available via jmx and the Manager mbean ( * e.g. in the jconsole mbean tab open the attributes node of the * <em>Catalina/Manager/<context-path>/<host name></em> * mbean and check for <em>msmStat*</em> values. * </p> * * @param enableStatistics <code>true</code> if statistics shall be gathered. */ public void setEnableStatistics( final boolean enableStatistics ) { final boolean oldEnableStatistics = _enableStatistics; _enableStatistics = enableStatistics; if ( oldEnableStatistics != enableStatistics && _manager.isInitialized() ) { _log.info( "Changed enableStatistics from " + oldEnableStatistics + " to " + enableStatistics + "." + " Reloading configuration..." ); reloadMemcachedConfig( _memcachedNodes, _failoverNodes ); } } /** * Specifies the number of threads that are used if {@link #setSessionBackupAsync(boolean)} * is set to <code>true</code>. * * @param backupThreadCount the number of threads to use for session backup. */ public void setBackupThreadCount( final int backupThreadCount ) { final int oldBackupThreadCount = _backupThreadCount; _backupThreadCount = backupThreadCount; if ( _manager.isInitialized() ) { _log.info( "Changed backupThreadCount from " + oldBackupThreadCount + " to " + _backupThreadCount + "." + " Reloading configuration..." ); reloadMemcachedConfig( _memcachedNodes, _failoverNodes ); _log.info( "Finished reloading configuration." ); } } /** * The number of threads to use for session backup if session backup shall be * done asynchronously. * @return the number of threads for session backup. */ public int getBackupThreadCount() { return _backupThreadCount; } /** * Specifies the memcached protocol to use, either "text" (default) or "binary". * * @param memcachedProtocol one of "text" or "binary". */ public void setMemcachedProtocol( final String memcachedProtocol ) { if ( !PROTOCOL_TEXT.equals( memcachedProtocol ) && !PROTOCOL_BINARY.equals( memcachedProtocol ) ) { _log.warn( "Illegal memcachedProtocol " + memcachedProtocol + ", using default (" + _memcachedProtocol + ")." ); return; } _memcachedProtocol = memcachedProtocol; } /** * Enable/disable memcached-session-manager (default <code>true</code> / enabled). * If disabled, sessions are neither looked up in memcached nor stored in memcached. * * @param enabled specifies if msm shall be disabled or not. * @throws IllegalStateException it's not allowed to disable this session manager when running in non-sticky mode. */ public void setEnabled( final boolean enabled ) throws IllegalStateException { if ( !enabled && !_sticky ) { throw new IllegalStateException( "Disabling this session manager is not allowed in non-sticky mode. You must switch to sticky operation mode before." ); } final boolean changed = _enabled.compareAndSet( !enabled, enabled ); if ( changed && _manager.isInitialized() ) { reloadMemcachedConfig( _memcachedNodes, _failoverNodes ); _log.info( "Changed enabled status to " + enabled + "." ); } } /** * Specifies, if msm is enabled or not. * * @return <code>true</code> if enabled, otherwise <code>false</code>. */ public boolean isEnabled() { return _enabled.get(); } public void setSticky( final boolean sticky ) { if ( sticky == _sticky ) { return; } if ( !sticky && _manager.getJvmRoute() != null ) { _log.warn( "Setting sticky to false while there's still a jvmRoute configured (" + _manager.getJvmRoute() + "), this might cause trouble." + " You should remve the jvmRoute configuration for non-sticky mode." ); } _sticky = sticky; if ( _manager.isInitialized() ) { _log.info( "Changed sticky to " + _sticky + ". Reloading configuration..." ); reloadMemcachedConfig( _memcachedNodes, _failoverNodes ); _log.info( "Finished reloading configuration." ); } } protected void setStickyInternal( final boolean sticky ) { _sticky = sticky; } public boolean isSticky() { return _sticky; } /** * Sets the session locking mode. Possible values: * <ul> * <li><code>none</code> - does not lock the session at all (default for non-sticky sessions).</li> * <li><code>all</code> - the session is locked for each request accessing the session.</li> * <li><code>auto</code> - locks the session for each request except for those the were detected to access the session only readonly.</li> * <li><code>uriPattern:<regexp></code> - locks the session for each request with a request uri (with appended querystring) matching * the provided regular expression.</li> * </ul> */ public void setLockingMode( @Nullable final String lockingMode ) { if ( lockingMode == null && _lockingMode == null || lockingMode != null && lockingMode.equals( _lockingMode ) ) { return; } _lockingMode = lockingMode; if ( _manager.isInitialized() ) { initNonStickyLockingMode( createMemcachedNodesManager( _memcachedNodes, _failoverNodes ) ); } } private void initNonStickyLockingMode( @Nonnull final MemcachedNodesManager config ) { if ( _sticky ) { setLockingMode( null, null, false ); return; } if ( _sessionAttributeFilter != null ) { _log.warn( "There's a sessionAttributesFilter configured ('" + _sessionAttributeFilter + "')," + " all other session attributes will be lost after the request due to non-sticky configuration!" ); } Pattern uriPattern = null; LockingMode lockingMode = null; if ( _lockingMode != null ) { if ( _lockingMode.startsWith( "uriPattern:" ) ) { lockingMode = LockingMode.URI_PATTERN; uriPattern = Pattern.compile( _lockingMode.substring( "uriPattern:".length() ) ); } else { lockingMode = LockingMode.valueOf( _lockingMode.toUpperCase() ); } } if ( lockingMode == null ) { lockingMode = LockingMode.NONE; } final boolean storeSecondaryBackup = config.getCountNodes() > 1 && !config.isCouchbaseBucketConfig(); setLockingMode( lockingMode, uriPattern, storeSecondaryBackup ); } public void setLockingMode( @Nullable final LockingMode lockingMode, @Nullable final Pattern uriPattern, final boolean storeSecondaryBackup ) { _log.info( "Setting lockingMode to " + lockingMode + ( uriPattern != null ? " with pattern " + uriPattern.pattern() : "" ) ); _lockingStrategy = LockingStrategy.create( lockingMode, uriPattern, _storage, this, _memcachedNodesManager, _invalidSessionsCache, storeSecondaryBackup, _statistics, _currentRequest ); } protected void updateExpirationInMemcached() { if ( _enabled.get() && _sticky ) { final Session[] sessions = _manager.findSessions(); final int delay = _manager.getContext().getBackgroundProcessorDelay(); for ( final Session s : sessions ) { final MemcachedBackupSession session = (MemcachedBackupSession) s; if ( _log.isDebugEnabled() ) { _log.debug( "Checking session " + session.getId() + ": " + "\n- isValid: " + session.isValidInternal() + "\n- isExpiring: " + session.isExpiring() + "\n- isBackupRunning: " + session.isBackupRunning() + "\n- isExpirationUpdateRunning: " + session.isExpirationUpdateRunning() + "\n- wasAccessedSinceLastBackup: " + session.wasAccessedSinceLastBackup() + "\n- memcachedExpirationTime: " + session.getMemcachedExpirationTime() ); } if ( session.isValidInternal() && !session.isExpiring() && !session.isBackupRunning() && !session.isExpirationUpdateRunning() && session.wasAccessedSinceLastBackup() && session.getMaxInactiveInterval() > 0 // for <= 0 the session was stored in memcached with expiration 0 && session.getMemcachedExpirationTime() <= 2 * delay ) { try { _backupSessionService.updateExpiration( session ); } catch ( final Throwable e ) { _log.info( "Could not update expiration in memcached for session " + session.getId(), e ); } } } } } /** * Specifies if the session shall be stored asynchronously in memcached as * {@link StorageClient#set(String, int, byte[])} supports it. If this is * false, the timeout set via {@link #setSessionBackupTimeout(int)} is * evaluated. If this is <code>true</code>, the {@link #setBackupThreadCount(int)} * is evaluated. * <p> * By default this property is set to <code>true</code> - the session * backup is performed asynchronously. * </p> * * @param sessionBackupAsync * the sessionBackupAsync to set */ public void setSessionBackupAsync( final boolean sessionBackupAsync ) { final boolean oldSessionBackupAsync = _sessionBackupAsync; _sessionBackupAsync = sessionBackupAsync; if ( ( oldSessionBackupAsync != sessionBackupAsync ) && _manager.isInitialized() ) { _log.info( "SessionBackupAsync was changed to " + sessionBackupAsync + ", creating new BackupSessionService with new configuration." ); _backupSessionService = new BackupSessionService( _transcoderService, _sessionBackupAsync, _sessionBackupTimeout, _backupThreadCount, _storage, _memcachedNodesManager, _statistics ); } } /** * Specifies if the session shall be stored asynchronously in memcached as * {@link StorageClient#set(String, int, byte[])} supports it. If this is * false, the timeout from {@link #getSessionBackupTimeout()} is * evaluated. */ public boolean isSessionBackupAsync() { return _sessionBackupAsync; } /** * The timeout in milliseconds after that a session backup is considered as * beeing failed. * <p> * This property is only evaluated if sessions are stored synchronously (set * via {@link #setSessionBackupAsync(boolean)}). * </p> * <p> * The default value is <code>100</code> millis. * * @param sessionBackupTimeout * the sessionBackupTimeout to set (milliseconds) */ public void setSessionBackupTimeout( final int sessionBackupTimeout ) { _sessionBackupTimeout = sessionBackupTimeout; } /** * The timeout in milliseconds after that a session backup is considered as * beeing failed when {@link #getSessionBackupAsync()}) is <code>false</code>. */ public long getSessionBackupTimeout() { return _sessionBackupTimeout; } public Statistics getStatistics() { return _statistics; } public long getOperationTimeout() { return _operationTimeout; } /** * This LockExpiration(seconds) avoid duplicated processing. * The session lock will automatically expire by memcached after this LockExpiration seconds, <br/> * when {@link #getLockingStrategy()} is <code>{@link LockingMode#AUTO} or {@link LockingMode#ALL}</code>. <br/> * If {@link #getOperationTimeout()} is less then this LockExpiration, * The other request accepted by other tomcat is available session backup before current request finished.<br/> * <code>lockExpiration > OperationTimeout : </code><br/> * - The other request wait for lockExpiration or unlocking <br/> * - The other request wait for as much as OperationTimeout, after then backup session used as {@link LockStatus#COULD_NOT_AQUIRE_LOCK}<br/> * <code>lockExpiration < OperationTimeout : </code><br/> * - The other request wait for as much as lockExpiration, that is accepted in lockExpiration <br/> * - The other request don't wait, that is accepted after lockExpiration <br/> */ public int getLockExpiration() { return _lockExpiration; } public void setOperationTimeout(final long operationTimeout ) { _operationTimeout = operationTimeout; } public void setLockExpiration(final int lockExpiration) { _lockExpiration = lockExpiration; } // ----------------------- protected getters/setters for testing ------------------ /** * Set the {@link TranscoderService} that is used by this manager and the {@link BackupSessionService}. * * @param transcoderService the transcoder service to use. */ void setTranscoderService( final TranscoderService transcoderService ) { _transcoderService = transcoderService; _backupSessionService = new BackupSessionService( transcoderService, _sessionBackupAsync, _sessionBackupTimeout, _backupThreadCount, _storage, _memcachedNodesManager, _statistics ); } /** * Return the memcached nodes manager. */ @Nonnull MemcachedNodesManager getMemcachedNodesManager() { return _memcachedNodesManager; } /** * Return the currently configured node ids - just for testing. * @return the list of node ids. */ List<String> getNodeIds() { return _memcachedNodesManager.getPrimaryNodeIds(); } /** * Return the currently configured failover node ids - just for testing. * @return the list of failover node ids. */ List<String> getFailoverNodeIds() { return _memcachedNodesManager.getFailoverNodeIds(); } /** * The storage client, this method is used in tests. */ public StorageClient getStorageClient() { return _storage; } /** * Set the given storage client, this method is used in tests. */ void setStorageClient(final StorageClient storage) { _storage = storage; } public RequestTrackingHostValve getTrackingHostValve() { return _trackingHostValve; } /** * The currently set locking strategy. */ @Nullable LockingStrategy getLockingStrategy() { return _lockingStrategy; } public void setUsername(final String username) { _username = username; } /** * username required for SASL Connection types * @return */ public String getUsername() { return _username; } public void setPassword(final String password) { _password = password; } /** * password required for SASL Connection types * @return */ public String getPassword() { return _password; } public String getStorageKeyPrefix() { return _storageKeyPrefix; } /** * Configure the storage key prefix, this is prepended to the session id in e.g. memcached. * * The configuration has the form <code>$token,$token</code> * * Some examples which config would create which output for the key / session id "foo" with context path "ctxt", * host "hst" and webappVersion "001" (webappVersion as specified for parallel deployment): * <dl> * <dt>static:x</dt><dd>x_foo</dd> * <dt>host</dt><dd>hst_foo</dd> * <dt>host.hash</dt><dd>e93c085e_foo</dd> * <dt>context</dt><dd>ctxt_foo</dd> * <dt>context.hash</dt><dd>45e6345f_foo</dd> * <dt>host,context</dt><dd>hst:ctxt_foo</dd> * <dt>webappVersion</dt><dd>001_foo</dd> * <dt>host.hash,context.hash,webappVersion</dt><dd>e93c085e:45e6345f:001_foo</dd> * </dl> * * @param storageKeyPrefix */ public void setStorageKeyPrefix(final String storageKeyPrefix) { _storageKeyPrefix = storageKeyPrefix; } }