All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.javakaffee.web.msm.BackupSessionService Maven / Gradle / Ivy

There is a newer version: 2.3.2
Show newest version
/*
 * 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> tasks ) throws InterruptedException { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public List> invokeAll( final Collection> tasks, final long timeout, final TimeUnit unit ) throws InterruptedException { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public T invokeAny( final Collection> tasks ) throws InterruptedException, ExecutionException { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public T invokeAny( final Collection> 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( null ); } catch ( final Exception e ) { return new SimpleFuture( new ExecutionException( e ) ); } } /** * {@inheritDoc} */ @Override public Future submit( final Runnable task, final T result ) { try { task.run(); return new SimpleFuture( result ); } catch ( final Exception e ) { return new SimpleFuture( 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 the result type */ static class SimpleFuture implements Future { 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; } } }