/*
* 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.MemcachedUtil.toMemcachedExpiration;
import static de.javakaffee.web.msm.Statistics.StatsType.ATTRIBUTES_SERIALIZATION;
import static de.javakaffee.web.msm.Statistics.StatsType.BACKUP;
import static de.javakaffee.web.msm.Statistics.StatsType.MEMCACHED_UPDATE;
import static de.javakaffee.web.msm.Statistics.StatsType.RELEASE_LOCK;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
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;
/**
* Stores the provided session in memcached if the session was modified
* or if the session needs to be relocated (set <code>force</code> to <code>true</code>).
*
* @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a>
*/
public class BackupSessionTask implements Callable<BackupResult> {
private static final Log _log = LogFactory.getLog( BackupSessionTask.class );
private final MemcachedBackupSession _session;
private final boolean _force;
private final TranscoderService _transcoderService;
private final boolean _sessionBackupAsync;
private final int _sessionBackupTimeout;
private final StorageClient _storage;
private final MemcachedNodesManager _memcachedNodesManager;
private final Statistics _statistics;
/**
* @param session
* the session to save
* @param sessionIdChanged
* specifies, if the session needs to be saved by all means, e.g.
* as it has to be relocated to another memcached
* node (the session id had been changed before in this case).
* @param sessionBackupAsync
* @param sessionBackupTimeout
* @param storage
* @param memcachedNodesManager
* @param statistics
*/
public BackupSessionTask( final MemcachedBackupSession session,
final boolean sessionIdChanged,
final TranscoderService transcoderService,
final boolean sessionBackupAsync,
final int sessionBackupTimeout,
final StorageClient storage,
final MemcachedNodesManager memcachedNodesManager,
final Statistics statistics ) {
_session = session;
_force = sessionIdChanged;
_transcoderService = transcoderService;
_sessionBackupAsync = sessionBackupAsync;
_sessionBackupTimeout = sessionBackupTimeout;
_storage = storage;
_memcachedNodesManager = memcachedNodesManager;
_statistics = statistics;
}
/**
* {@inheritDoc}
*/
@Override
public BackupResult call() throws Exception {
if ( _log.isDebugEnabled() ) {
_log.debug( "Starting for session id " + _session.getId() );
}
_session.setBackupRunning( true );
try {
final long startBackup = System.currentTimeMillis();
final ConcurrentMap<String, Object> attributes = _session.getAttributesFiltered();
final byte[] attributesData = serializeAttributes( _session, attributes );
final int hashCode = Arrays.hashCode( attributesData );
final BackupResult result;
if ( _session.getDataHashCode() != hashCode
|| _force
|| _session.authenticationChanged() ) {
_session.setLastBackupTime( System.currentTimeMillis() );
final byte[] data = _transcoderService.serialize( _session, attributesData );
result = doBackupSession( _session, data, attributesData );
if ( result.isSuccess() ) {
_session.setDataHashCode( hashCode );
}
} else {
result = new BackupResult( BackupResultStatus.SKIPPED );
}
switch ( result.getStatus() ) {
case FAILURE:
_statistics.requestWithBackupFailure();
_session.backupFailed();
break;
case SKIPPED:
_statistics.requestWithoutSessionModification();
_session.storeThisAccessedTimeFromLastBackupCheck();
break;
case SUCCESS:
_statistics.registerSince( BACKUP, startBackup );
_session.storeThisAccessedTimeFromLastBackupCheck();
_session.backupFinished();
break;
}
if ( _log.isDebugEnabled() ) {
_log.debug( "Finished for session id " + _session.getId() +
", returning status " + result.getStatus() );
}
return result;
} catch (Exception e) {
_log.warn("FAILED for session id " + _session.getId(), e);
throw e;
}
finally {
_session.setBackupRunning( false );
releaseLock();
}
}
private void releaseLock() {
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 );
}
}
}
private byte[] serializeAttributes( final MemcachedBackupSession session, final ConcurrentMap<String, Object> attributes ) {
final long start = System.currentTimeMillis();
final byte[] attributesData = _transcoderService.serializeAttributes( session, attributes );
_statistics.registerSince( ATTRIBUTES_SERIALIZATION, start );
return attributesData;
}
/**
* Store the provided session in memcached.
* @param session the session to backup
* @param data the serialized session data (session fields and session attributes).
* @param attributesData just the serialized session attributes.
*
* @return the {@link BackupResultStatus}
*/
BackupResult doBackupSession( final MemcachedBackupSession session, final byte[] data, final byte[] attributesData ) throws InterruptedException {
if ( _log.isDebugEnabled() ) {
_log.debug( "Trying to store session in memcached: " + session.getId() );
}
try {
storeSessionInMemcached( session, data );
return new BackupResult( BackupResultStatus.SUCCESS, data, attributesData );
} catch (final ExecutionException e) {
handleException(session, e);
return new BackupResult(BackupResultStatus.FAILURE, data, null);
} catch (final TimeoutException e) {
handleException(session, e);
return new BackupResult(BackupResultStatus.FAILURE, data, null);
}
}
private void handleException(final MemcachedBackupSession session, final Exception e) {
//if ( _log.isWarnEnabled() ) {
String msg = "Could not store session " + session.getId() + " in memcached.";
if ( _force ) {
msg += "\nNote that this session was relocated to this node because the" +
" original node was not available.";
}
_log.warn(msg, e);
//}
_memcachedNodesManager.setNodeAvailableForSessionId(session.getId(), false);
}
private void storeSessionInMemcached( final MemcachedBackupSession session, final byte[] data) throws InterruptedException, ExecutionException, TimeoutException {
/* calculate the expiration time (instead of using just maxInactiveInterval), as
* this is relevant for the update of the expiration time: if we would just use
* maxInactiveInterval, the session would exist longer in memcached than it would
* be valid in tomcat
*/
final int expirationTime = session.getMemcachedExpirationTimeToSet();
final long start = System.currentTimeMillis();
try {
final Future<Boolean> future = _storage.set(
_memcachedNodesManager.getStorageKeyFormat().format(session.getId()),
toMemcachedExpiration(expirationTime), data );
if ( !_sessionBackupAsync ) {
future.get( _sessionBackupTimeout, TimeUnit.MILLISECONDS );
session.setLastMemcachedExpirationTime( expirationTime );
session.setLastBackupTime( System.currentTimeMillis() );
}
else {
/* in async mode, we asume the session was stored successfully
*/
session.setLastMemcachedExpirationTime( expirationTime );
session.setLastBackupTime( System.currentTimeMillis() );
}
} finally {
_statistics.registerSince( MEMCACHED_UPDATE, start );
}
}
static final class BackupResult {
public static final BackupResult SKIPPED = new BackupResult( BackupResultStatus.SKIPPED );
public static final BackupResult FAILURE = new BackupResult( BackupResultStatus.FAILURE );
private final BackupResultStatus _status;
private final byte[] _data;
private final byte[] _attributesData;
public BackupResult( @Nonnull final BackupResultStatus status ) {
this( status, null, null );
}
public BackupResult( @Nonnull final BackupResultStatus status, @Nullable final byte[] data, @Nullable final byte[] attributesData ) {
_status = status;
_data = data;
_attributesData = attributesData;
}
/**
* The status/result of the backup operation.
* @return the status
*/
@Nonnull
BackupResultStatus getStatus() {
return _status;
}
/**
* The serialized session data (session fields and session attributes).
* This can be <code>null</code> (if {@link #getStatus()} is {@link BackupResultStatus#SKIPPED}).
*
* @return the session data
*/
@CheckForNull
byte[] getData() {
return _data;
}
/**
* The serialized attributes that were actually stored in memcached with the
* full serialized session data. This can be <code>null</code>, e.g. if
* {@link #getStatus()} is {@link BackupResultStatus#FAILURE} or {@link BackupResultStatus#SKIPPED}.
*
* @return the attributesData
*/
@CheckForNull
byte[] getAttributesData() {
return _attributesData;
}
/**
* Specifies if the backup was performed successfully.
*
* @return <code>true</code> if the status is {@link BackupResultStatus#SUCCESS},
* otherwise <code>false</code>.
*/
public boolean isSuccess() {
return _status == BackupResultStatus.SUCCESS;
}
@Override
public String toString() {
return "BackupResult [_status=" + _status + ", _data="
+ (_data != null ? "byte[" + _data.length + "]" : "null") + ", _attributesData="
+ (_attributesData != null ? "byte[" + _attributesData.length + "]" : "null") + "]";
}
}
}