/*
* INESC-ID, Instituto de Engenharia de Sistemas e Computadores Investigação e Desevolvimento em Lisboa
* Copyright 2013 INESC-ID and/or its affiliates and other
* contributors as indicated by the @author tags. All rights reserved.
* See the copyright.txt in the distribution for a full listing of
* individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 3.0 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.infinispan.interceptors.gmu;
import org.infinispan.CacheException;
import org.infinispan.commands.read.GetKeyValueCommand;
import org.infinispan.commands.tx.CommitCommand;
import org.infinispan.commands.tx.GMUCommitCommand;
import org.infinispan.commands.tx.GMUPrepareCommand;
import org.infinispan.commands.tx.PrepareCommand;
import org.infinispan.commands.tx.RollbackCommand;
import org.infinispan.commands.write.ApplyDeltaCommand;
import org.infinispan.commands.write.ClearCommand;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.commands.write.RemoveCommand;
import org.infinispan.commands.write.ReplaceCommand;
import org.infinispan.commands.write.WriteCommand;
import org.infinispan.container.DataContainer;
import org.infinispan.container.entries.gmu.InternalGMUCacheEntry;
import org.infinispan.container.versioning.EntryVersion;
import org.infinispan.container.versioning.VersionGenerator;
import org.infinispan.container.versioning.gmu.GMUVersionGenerator;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.SingleKeyNonTxInvocationContext;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.interceptors.EntryWrappingInterceptor;
import org.infinispan.transaction.gmu.CommitLog;
import org.infinispan.transaction.gmu.manager.TransactionCommitManager;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
import org.jgroups.blocks.RequestHandler;
import java.util.LinkedList;
import java.util.List;
import static org.infinispan.transaction.gmu.GMUHelper.*;
/**
* @author Pedro Ruivo
* @since 5.2
*/
public class GMUEntryWrappingInterceptor extends EntryWrappingInterceptor {
private static final Log log = LogFactory.getLog(GMUEntryWrappingInterceptor.class);
protected GMUVersionGenerator versionGenerator;
private TransactionCommitManager transactionCommitManager;
@Inject
public void inject(TransactionCommitManager transactionCommitManager, DataContainer dataContainer,
CommitLog commitLog, VersionGenerator versionGenerator) {
this.transactionCommitManager = transactionCommitManager;
this.versionGenerator = toGMUVersionGenerator(versionGenerator);
}
@Override
public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable {
GMUPrepareCommand spc = convert(command, GMUPrepareCommand.class);
if (ctx.isOriginLocal()) {
spc.setVersion(ctx.getTransactionVersion());
spc.setReadSet(ctx.getReadSet());
} else {
ctx.setTransactionVersion(spc.getPrepareVersion());
}
wrapEntriesForPrepare(ctx, command);
performValidation(ctx, spc);
Object retVal = invokeNextInterceptor(ctx, command);
if (ctx.isOriginLocal() && command.getModifications().length > 0) {
EntryVersion commitVersion = calculateCommitVersion(ctx.getTransactionVersion(), versionGenerator,
cll.getWriteOwners(ctx.getCacheTransaction()));
ctx.setTransactionVersion(commitVersion);
} else {
retVal = ctx.getTransactionVersion();
}
if (command.isOnePhaseCommit()) {
commitContextEntries.commitContextEntries(ctx);
}
return retVal;
}
@Override
public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable {
GMUCommitCommand gmuCommitCommand = convert(command, GMUCommitCommand.class);
if (ctx.isOriginLocal()) {
gmuCommitCommand.setCommitVersion(ctx.getTransactionVersion());
} else {
ctx.setTransactionVersion(gmuCommitCommand.getCommitVersion());
}
transactionCommitManager.commitTransaction(ctx.getCacheTransaction(), gmuCommitCommand.getCommitVersion());
Object retVal = null;
try {
retVal = invokeNextInterceptor(ctx, command);
} catch (Throwable throwable) {
//let ignore the exception. we cannot have some nodes applying the write set and another not another one
//receives the rollback and don't applies the write set
} finally {
transactionCommitManager.awaitUntilCommitted(ctx.getCacheTransaction(), ctx.isOriginLocal() ? null : gmuCommitCommand);
}
return ctx.isOriginLocal() ? retVal : RequestHandler.DO_NOT_REPLY;
}
@Override
public Object visitRollbackCommand(TxInvocationContext ctx, RollbackCommand command) throws Throwable {
try {
return invokeNextInterceptor(ctx, command);
} finally {
transactionCommitManager.rollbackTransaction(ctx.getCacheTransaction());
}
}
/*
* NOTE: these are the only commands that passes values to the application and these keys needs to be validated
* and added to the transaction read set.
*/
@Override
public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable {
ctx.clearKeyReadInCommand();
Object retVal = super.visitGetKeyValueCommand(ctx, command);
updateTransactionVersion(ctx);
return retVal;
}
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
ctx.clearKeyReadInCommand();
Object retVal = super.visitPutKeyValueCommand(ctx, command);
updateTransactionVersion(ctx);
return retVal;
}
@Override
public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable {
ctx.clearKeyReadInCommand();
Object retVal = super.visitRemoveCommand(ctx, command);
updateTransactionVersion(ctx);
return retVal;
}
@Override
public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable {
ctx.clearKeyReadInCommand();
Object retVal = super.visitReplaceCommand(ctx, command);
updateTransactionVersion(ctx);
return retVal;
}
/**
* validates the read set and returns the prepare version from the commit queue
*
* @param ctx the context
* @param command the prepare command
* @throws InterruptedException if interrupted
*/
protected void performValidation(TxInvocationContext ctx, GMUPrepareCommand command) throws InterruptedException {
boolean hasToUpdateLocalKeys = hasLocalKeysToUpdate(command.getModifications());
boolean isReadOnly = command.getModifications().length == 0;
if (!isReadOnly) {
cll.performReadSetValidation(ctx, command);
if (hasToUpdateLocalKeys) {
transactionCommitManager.prepareTransaction(ctx.getCacheTransaction());
} else {
transactionCommitManager.prepareReadOnlyTransaction(ctx.getCacheTransaction());
}
}
if (log.isDebugEnabled()) {
log.debugf("Transaction %s can commit on this node. Prepare Version is %s",
command.getGlobalTransaction().prettyPrint(), ctx.getTransactionVersion());
}
}
private void updateTransactionVersion(InvocationContext context) {
if (!context.isInTxScope() && !context.isOriginLocal()) {
return;
}
if (context instanceof SingleKeyNonTxInvocationContext) {
if (log.isDebugEnabled()) {
log.debugf("Received a SingleKeyNonTxInvocationContext... This should be a single read operation");
}
return;
}
TxInvocationContext txInvocationContext = (TxInvocationContext) context;
List<EntryVersion> entryVersionList = new LinkedList<EntryVersion>();
entryVersionList.add(txInvocationContext.getTransactionVersion());
if (log.isTraceEnabled()) {
log.tracef("[%s] Keys read in this command: %s", txInvocationContext.getGlobalTransaction().prettyPrint(),
txInvocationContext.getKeysReadInCommand());
}
for (InternalGMUCacheEntry internalGMUCacheEntry : txInvocationContext.getKeysReadInCommand().values()) {
Object key = internalGMUCacheEntry.getKey();
boolean local = cll.localNodeIsOwner(key);
if (log.isTraceEnabled()) {
log.tracef("[%s] Analyze entry [%s]: local?=%s",
txInvocationContext.getGlobalTransaction().prettyPrint(),
internalGMUCacheEntry, local);
}
if (txInvocationContext.hasModifications() && !internalGMUCacheEntry.isMostRecent()) {
throw new CacheException("Read-Write transaction read an old value and should rollback");
}
if (internalGMUCacheEntry.getMaximumTransactionVersion() != null) {
entryVersionList.add(internalGMUCacheEntry.getMaximumTransactionVersion());
}
txInvocationContext.getCacheTransaction().addReadKey(key);
if (local) {
txInvocationContext.setAlreadyReadOnThisNode(true);
txInvocationContext.addReadFrom(cll.getAddress());
}
}
if (entryVersionList.size() > 1) {
EntryVersion[] txVersionArray = new EntryVersion[entryVersionList.size()];
txInvocationContext.setTransactionVersion(versionGenerator.mergeAndMax(entryVersionList.toArray(txVersionArray)));
}
}
private boolean hasLocalKeysToUpdate(WriteCommand[] modifications) {
for (WriteCommand writeCommand : modifications) {
if (writeCommand instanceof ClearCommand) {
return true;
} else if (writeCommand instanceof ApplyDeltaCommand) {
if (cll.localNodeIsOwner(((ApplyDeltaCommand) writeCommand).getDeltaAwareKey())) {
return true;
}
} else {
for (Object key : writeCommand.getAffectedKeys()) {
if (cll.localNodeIsOwner(key)) {
return true;
}
}
}
}
return false;
}
}