de.javakaffee.web.msm.BackupSessionService Maven / Gradle / Ivy
Show all versions of memcached-session-manager Show documentation
/*
* 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 Martin Grotzke
*/
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
* session.maxInactiveInterval - timeIdle
* seconds in memcached (whereas timeIdle is calculated as
* System.currentTimeMillis - session.thisAccessedTime).
*
* IMPLEMENTATION NOTE: right now this performs a new backup of the session
* in memcached. Once the touch command is available in memcached
* (see issue #110 in memcached),
* we can consider to use this.
*
*
* @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 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.
*
* The session backup is done asynchronously according to the provided
* sessionBackupAsynch flag (in the constructor).
*
*
* Before a new {@link BackupSessionTask} is created for session backup the following
* checks are done:
*
* - check if the session id contains a memcached id, otherwise abort
* - check if the session was accessed during this request
* - check if session attributes were accessed during this request
*
*
*
* @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 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.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.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.SKIPPED );
}
final BackupSessionTask task = createBackupSessionTask( session, force );
final Future 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.
*
* 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.
*
*/
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 List> invokeAll( final Collection extends Callable> tasks ) throws InterruptedException {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
@Override
public List> invokeAll( final Collection extends Callable> tasks, final long timeout, final TimeUnit unit )
throws InterruptedException {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
@Override
public T invokeAny( final Collection extends Callable> tasks ) throws InterruptedException, ExecutionException {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
@Override
public T invokeAny( final Collection extends Callable> 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 shutdownNow() {
shutdown();
return null;
}
/**
* {@inheritDoc}
*/
@Override
public Future submit( final Callable task ) {
try {
return new SimpleFuture( task.call() );
} catch ( final Exception e ) {
return new SimpleFuture( new ExecutionException( e ) );
}
}
/**
* {@inheritDoc}
*/
@Override
public Future> submit( final Runnable task ) {
try {
task.run();
return new SimpleFuture