/* * 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.Statistics.StatsType.EFFECTIVE_BACKUP; import static de.javakaffee.web.msm.Statistics.StatsType.RELEASE_LOCK; import java.util.Collection; import java.util.List; import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; 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 org.apache.catalina.Session; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import de.javakaffee.web.msm.BackupSessionTask.BackupResult; import de.javakaffee.web.msm.storage.StorageClient; /** * This service is responsible for storing sessions memcached. This includes * serialization (which is delegated to the {@link TranscoderService}) and * the communication with memcached (using a provided {@link StorageClient}). * * @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a> */ public class BackupSessionService { private static final Log _log = LogFactory.getLog( BackupSessionService.class ); private final TranscoderService _transcoderService; private final boolean _sessionBackupAsync; private final int _sessionBackupTimeout; private final StorageClient _storage; private final MemcachedNodesManager _memcachedNodesManager; private final Statistics _statistics; private final ExecutorService _executorService; /** * @param sessionBackupAsync * @param sessionBackupTimeout * @param backupThreadCount TODO * @param storage * @param memcachedNodesManager * @param failoverNodeIds */ public BackupSessionService( final TranscoderService transcoderService, final boolean sessionBackupAsync, final int sessionBackupTimeout, final int backupThreadCount, final StorageClient storage, final MemcachedNodesManager memcachedNodesManager, final Statistics statistics ) { _transcoderService = transcoderService; _sessionBackupAsync = sessionBackupAsync; _sessionBackupTimeout = sessionBackupTimeout; _storage = storage; _memcachedNodesManager = memcachedNodesManager; _statistics = statistics; _executorService = sessionBackupAsync ? Executors.newFixedThreadPool( backupThreadCount, new NamedThreadFactory("msm-storage") ) : new SynchronousExecutorService(); } /** * Shutdown this service, this stops the possibly existing threads used for session backup. */ public void shutdown() { _executorService.shutdown(); } /** * Update the expiration for the session associated with this {@link BackupSessionService} * in memcached, so that the session will expire in * <em>session.maxInactiveInterval - timeIdle</em> * seconds in memcached (whereas timeIdle is calculated as * <em>System.currentTimeMillis - session.thisAccessedTime</em>). * <p> * <strong>IMPLEMENTATION NOTE</strong>: right now this performs a new backup of the session * in memcached. Once the touch command is available in memcached * (see <a href="http://code.google.com/p/memcached/issues/detail?id=110">issue #110</a> in memcached), * we can consider to use this. * </p> * * @param session the session for that the expiration shall be updated in memcached. * * @see Session#getMaxInactiveInterval() * @see MemcachedBackupSession#getThisAccessedTimeInternal() */ public void updateExpiration( final MemcachedBackupSession session ) throws InterruptedException { if ( _log.isDebugEnabled() ) { _log.debug( "Updating expiration time for session " + session.getId() ); } if ( !_memcachedNodesManager.getSessionIdFormat().isValid( session.getId() ) ) { return; } session.setExpirationUpdateRunning( true ); session.setLastBackupTime( System.currentTimeMillis() ); try { final ConcurrentMap<String, Object> attributes = session.getAttributesFiltered(); final byte[] attributesData = _transcoderService.serializeAttributes( session, attributes ); final byte[] data = _transcoderService.serialize( session, attributesData ); createBackupSessionTask( session, true ).doBackupSession( session, data, attributesData ); } finally { session.setExpirationUpdateRunning( false ); } } /** * Store the provided session in memcached if the session was modified * or if the session needs to be relocated. * <p> * The session backup is done asynchronously according to the provided * <em>sessionBackupAsynch</em> flag (in the constructor). * </p> * <p> * Before a new {@link BackupSessionTask} is created for session backup the following * checks are done: * <ul> * <li>check if the session id contains a memcached id, otherwise abort</li> * <li>check if the session was accessed during this request</li> * <li>check if session attributes were accessed during this request</li> * </ul> * </p> * * @param session * the session to save * @param force * specifies, if session backup shall be forced, e.g. because the * session id was changed due to a memcached failover or tomcat failover. * @return a {@link Future} providing the result of the backup task. * * @see MemcachedSessionService#setSessionBackupAsync(boolean) * @see BackupSessionTask#call() */ public Future<BackupResult> backupSession( final MemcachedBackupSession session, final boolean force ) { if ( _log.isDebugEnabled() ) { _log.debug( "Starting for session id " + session.getId() ); } final long start = System.currentTimeMillis(); try { if ( !_memcachedNodesManager.getSessionIdFormat().isValid( session.getId() ) ) { if ( _log.isDebugEnabled() ) { _log.debug( "Skipping backup for session id " + session.getId() + " as the session id is not usable for memcached." ); } _statistics.requestWithBackupFailure(); return new SimpleFuture<BackupResult>( BackupResult.FAILURE ); } /* Check if the session was accessed at all since the last backup/check. * If this is not the case, we even don't have to check if attributes * have changed (and can skip serialization and hash calucation) */ if ( !session.wasAccessedSinceLastBackupCheck() && !force ) { _log.debug( "Session was not accessed since last backup/check, therefore we can skip this" ); _statistics.requestWithoutSessionAccess(); releaseLock( session ); return new SimpleFuture<BackupResult>( BackupResult.SKIPPED ); } if ( !session.attributesAccessedSinceLastBackup() && !force && !session.authenticationChanged() && !session.isNewInternal() ) { _log.debug( "Session attributes were not accessed since last backup/check, therefore we can skip this" ); _statistics.requestWithoutAttributesAccess(); releaseLock( session ); return new SimpleFuture<BackupResult>( BackupResult.SKIPPED ); } final BackupSessionTask task = createBackupSessionTask( session, force ); final Future<BackupResult> result = _executorService.submit( task ); if ( !_sessionBackupAsync ) { try { result.get( _sessionBackupTimeout, TimeUnit.MILLISECONDS ); } catch ( final Exception e ) { if ( _log.isInfoEnabled() ) { _log.info( "Could not store session " + session.getId() + " in memcached.", e ); } } } return result; } finally { _statistics.registerSince( EFFECTIVE_BACKUP, start ); } } private BackupSessionTask createBackupSessionTask( final MemcachedBackupSession session, final boolean force ) { return new BackupSessionTask( session, force, _transcoderService, _sessionBackupAsync, _sessionBackupTimeout, _storage, _memcachedNodesManager, _statistics ); } private void releaseLock( @Nonnull final MemcachedBackupSession session ) { if ( session.isLocked() ) { try { if ( _log.isDebugEnabled() ) { _log.debug( "Releasing lock for session " + session.getIdInternal() ); } final long start = System.currentTimeMillis(); _storage.delete( _memcachedNodesManager.getSessionIdFormat().createLockName( session.getIdInternal() ) ).get(); _statistics.registerSince( RELEASE_LOCK, start ); session.releaseLock(); } catch( final Exception e ) { _log.warn( "Caught exception when trying to release lock for session " + session.getIdInternal(), e ); } } } /** * An implementation of {@link ExecutorService} that executes submitted {@link Callable}s * and {@link Runnable}s in the caller thread. * <p> * Implementation note: It does not extend {@link AbstractExecutorService} for performance * reasons, as the {@link AbstractExecutorService} internals and the used {@link Future} * implementations provide an overhead due to concurrency handling. * </p> */ static class SynchronousExecutorService implements ExecutorService { private boolean _shutdown; /** * {@inheritDoc} */ @Override public boolean awaitTermination( final long timeout, final TimeUnit unit ) throws InterruptedException { return true; } /** * {@inheritDoc} */ @Override public <T> List<Future<T>> invokeAll( final Collection<? extends Callable<T>> tasks ) throws InterruptedException { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public <T> List<Future<T>> invokeAll( final Collection<? extends Callable<T>> tasks, final long timeout, final TimeUnit unit ) throws InterruptedException { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public <T> T invokeAny( final Collection<? extends Callable<T>> tasks ) throws InterruptedException, ExecutionException { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public <T> T invokeAny( final Collection<? extends Callable<T>> tasks, final long timeout, final TimeUnit unit ) throws InterruptedException, ExecutionException, TimeoutException { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public boolean isShutdown() { return _shutdown; } /** * {@inheritDoc} */ @Override public boolean isTerminated() { return _shutdown; } /** * {@inheritDoc} */ @Override public void shutdown() { _shutdown = true; } /** * {@inheritDoc} */ @Override public List<Runnable> shutdownNow() { shutdown(); return null; } /** * {@inheritDoc} */ @Override public <T> Future<T> submit( final Callable<T> task ) { try { return new SimpleFuture<T>( task.call() ); } catch ( final Exception e ) { return new SimpleFuture<T>( new ExecutionException( e ) ); } } /** * {@inheritDoc} */ @Override public Future<?> submit( final Runnable task ) { try { task.run(); return new SimpleFuture<Object>( null ); } catch ( final Exception e ) { return new SimpleFuture<Object>( new ExecutionException( e ) ); } } /** * {@inheritDoc} */ @Override public <T> Future<T> submit( final Runnable task, final T result ) { try { task.run(); return new SimpleFuture<T>( result ); } catch ( final Exception e ) { return new SimpleFuture<T>( new ExecutionException( e ) ); } } /** * {@inheritDoc} */ @Override public void execute( final Runnable command ) { command.run(); } } /** * A future implementations that wraps an already existing result * or a caught exception. * * @param <T> the result type */ static class SimpleFuture<T> implements Future<T> { private final T _result; private final ExecutionException _e; /** * @param result */ public SimpleFuture( final T result ) { _result = result; _e = null; } /** * @param e */ public SimpleFuture( final ExecutionException e ) { _result = null; _e = e; } /** * {@inheritDoc} */ @Override public boolean cancel( final boolean mayInterruptIfRunning ) { return true; } /** * {@inheritDoc} */ @Override public T get() throws InterruptedException, ExecutionException { if ( _e != null ) { throw _e; } return _result; } /** * {@inheritDoc} */ @Override public T get( final long timeout, final TimeUnit unit ) throws InterruptedException, ExecutionException, TimeoutException { if ( _e != null ) { throw _e; } return _result; } /** * {@inheritDoc} */ @Override public boolean isCancelled() { return false; } /** * {@inheritDoc} */ @Override public boolean isDone() { return true; } } }