package org.infinispan.commands.tx;
import static org.infinispan.commons.util.InfinispanCollections.forEach;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.infinispan.commands.Visitor;
import org.infinispan.commands.write.ApplyDeltaCommand;
import org.infinispan.commands.write.DataWriteCommand;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.commands.write.PutMapCommand;
import org.infinispan.commands.write.RemoveCommand;
import org.infinispan.commands.write.RemoveExpiredCommand;
import org.infinispan.commands.write.ReplaceCommand;
import org.infinispan.commands.write.WriteCommand;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.impl.FlagBitSets;
import org.infinispan.context.impl.RemoteTxInvocationContext;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.notifications.cachelistener.CacheNotifier;
import org.infinispan.transaction.impl.RemoteTransaction;
import org.infinispan.transaction.xa.GlobalTransaction;
import org.infinispan.transaction.xa.recovery.RecoveryManager;
import org.infinispan.util.ByteString;
import org.infinispan.util.concurrent.CompletableFutures;
import org.infinispan.util.concurrent.locks.TransactionalRemoteLockCommand;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
/**
* Command corresponding to the 1st phase of 2PC.
*
* @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
* @author Mircea.Markus@jboss.com
* @since 4.0
*/
public class PrepareCommand extends AbstractTransactionBoundaryCommand implements TransactionalRemoteLockCommand {
private static final Log log = LogFactory.getLog(PrepareCommand.class);
private static boolean trace = log.isTraceEnabled();
public static final byte COMMAND_ID = 12;
protected WriteCommand[] modifications;
protected boolean onePhaseCommit;
protected CacheNotifier notifier;
protected RecoveryManager recoveryManager;
private transient boolean replayEntryWrapping = false;
protected boolean retriedCommand;
private static final WriteCommand[] EMPTY_WRITE_COMMAND_ARRAY = new WriteCommand[0];
public void initialize(CacheNotifier notifier, RecoveryManager recoveryManager) {
this.notifier = notifier;
this.recoveryManager = recoveryManager;
}
private PrepareCommand() {
super(null); // For command id uniqueness test
}
public PrepareCommand(ByteString cacheName, GlobalTransaction gtx, boolean onePhaseCommit, WriteCommand... modifications) {
super(cacheName);
this.globalTx = gtx;
this.modifications = modifications;
this.onePhaseCommit = onePhaseCommit;
}
public PrepareCommand(ByteString cacheName, GlobalTransaction gtx, List<WriteCommand> commands, boolean onePhaseCommit) {
super(cacheName);
this.globalTx = gtx;
this.modifications = commands == null || commands.isEmpty() ? null : commands.toArray(new WriteCommand[commands.size()]);
this.onePhaseCommit = onePhaseCommit;
}
public PrepareCommand(ByteString cacheName) {
super(cacheName);
}
@Override
public CompletableFuture<Object> invokeAsync() throws Throwable {
RemoteTxInvocationContext ctx = createContext();
if (ctx == null) {
return CompletableFutures.completedNull();
}
if (trace)
log.tracef("Invoking remotely originated prepare: %s with invocation context: %s", this, ctx);
notifier.notifyTransactionRegistered(ctx.getGlobalTransaction(), false);
return invoker.invokeAsync(ctx, this);
}
@Override
public RemoteTxInvocationContext createContext() {
if (recoveryManager != null && recoveryManager.isTransactionPrepared(globalTx)) {
log.tracef("The transaction %s is already prepared. Skipping prepare call.", globalTx);
return null;
}
// 1. first create a remote transaction (or get the existing one)
RemoteTransaction remoteTransaction = getRemoteTransaction();
//set the list of modifications anyway, as the transaction might have already been created by a previous
//LockControlCommand with null modifications.
if (hasModifications()) {
remoteTransaction.setModifications(Arrays.asList(modifications));
}
// 2. then set it on the invocation context
return icf.createRemoteTxInvocationContext(remoteTransaction, getOrigin());
}
@Override
public Collection<?> getKeysToLock() {
if (modifications == null || modifications.length == 0) {
return Collections.emptyList();
}
final Set<Object> set = new HashSet<>(modifications.length);
forEach(modifications, writeCommand -> {
if (writeCommand.hasAnyFlag(FlagBitSets.SKIP_LOCKING)) {
return;
}
switch (writeCommand.getCommandId()) {
case PutKeyValueCommand.COMMAND_ID:
case RemoveCommand.COMMAND_ID:
case RemoveExpiredCommand.COMMAND_ID:
case ReplaceCommand.COMMAND_ID:
set.add(((DataWriteCommand) writeCommand).getKey());
break;
case PutMapCommand.COMMAND_ID:
set.addAll(writeCommand.getAffectedKeys());
break;
case ApplyDeltaCommand.COMMAND_ID:
ApplyDeltaCommand command = (ApplyDeltaCommand) writeCommand;
Object[] compositeKeys = command.getCompositeKeys();
set.addAll(Arrays.asList(compositeKeys));
break;
default:
break;
}
});
return set;
}
@Override
public Object getKeyLockOwner() {
return globalTx;
}
@Override
public boolean hasZeroLockAcquisition() {
return false;
}
@Override
public boolean hasSkipLocking() {
return false;
}
@Override
protected RemoteTransaction getRemoteTransaction() {
return txTable.getOrCreateRemoteTransaction(globalTx, modifications);
}
@Override
public Object acceptVisitor(InvocationContext ctx, Visitor visitor) throws Throwable {
return visitor.visitPrepareCommand((TxInvocationContext) ctx, this);
}
public WriteCommand[] getModifications() {
return modifications == null ? EMPTY_WRITE_COMMAND_ARRAY : modifications;
}
public boolean isOnePhaseCommit() {
return onePhaseCommit;
}
@Override
public byte getCommandId() {
return COMMAND_ID;
}
@Override
public void writeTo(ObjectOutput output) throws IOException {
super.writeTo(output); //global tx
output.writeBoolean(onePhaseCommit);
output.writeBoolean(retriedCommand);
MarshallUtil.marshallArray(modifications, output);
}
@Override
public void readFrom(ObjectInput input) throws IOException, ClassNotFoundException {
super.readFrom(input);
onePhaseCommit = input.readBoolean();
retriedCommand = input.readBoolean();
modifications = MarshallUtil.unmarshallArray(input, WriteCommand[]::new);
}
public PrepareCommand copy() {
PrepareCommand copy = new PrepareCommand(cacheName);
copy.globalTx = globalTx;
copy.modifications = modifications == null ? null : modifications.clone();
copy.onePhaseCommit = onePhaseCommit;
return copy;
}
@Override
public String toString() {
return "PrepareCommand {" +
"modifications=" + (modifications == null ? null : Arrays.asList(modifications)) +
", onePhaseCommit=" + onePhaseCommit +
", retried=" + retriedCommand +
", " + super.toString();
}
public boolean hasModifications() {
return modifications != null && modifications.length > 0;
}
public Collection<?> getAffectedKeys() {
if (modifications == null || modifications.length == 0)
return Collections.emptySet();
if (modifications.length == 1) return modifications[0].getAffectedKeys();
Set<Object> keys = new HashSet<>(modifications.length);
for (WriteCommand wc: modifications) keys.addAll(wc.getAffectedKeys());
return keys;
}
/**
* If set to true, then the keys touched by this transaction are to be wrapped again and original ones discarded.
*/
public boolean isReplayEntryWrapping() {
return replayEntryWrapping;
}
/**
* @see #isReplayEntryWrapping()
*/
public void setReplayEntryWrapping(boolean replayEntryWrapping) {
this.replayEntryWrapping = replayEntryWrapping;
}
@Override
public boolean isReturnValueExpected() {
return false;
}
public boolean isRetriedCommand() {
return retriedCommand;
}
public void setRetriedCommand(boolean retriedCommand) {
this.retriedCommand = retriedCommand;
}
}