package org.infinispan.interceptors.totalorder; import static org.infinispan.commons.util.Util.toStr; import java.util.Collection; import org.infinispan.commands.control.LockControlCommand; import org.infinispan.commands.tx.AbstractTransactionBoundaryCommand; import org.infinispan.commands.tx.CommitCommand; import org.infinispan.commands.tx.PrepareCommand; import org.infinispan.commands.tx.RollbackCommand; import org.infinispan.commands.tx.totalorder.TotalOrderPrepareCommand; import org.infinispan.commons.CacheException; import org.infinispan.context.InvocationContext; import org.infinispan.context.impl.TxInvocationContext; import org.infinispan.factories.KnownComponentNames; import org.infinispan.factories.annotations.ComponentName; import org.infinispan.factories.annotations.Inject; import org.infinispan.interceptors.DDAsyncInterceptor; import org.infinispan.interceptors.locking.ClusteringDependentLogic; import org.infinispan.transaction.impl.RemoteTransaction; import org.infinispan.transaction.impl.TotalOrderRemoteTransactionState; import org.infinispan.transaction.impl.TransactionTable; import org.infinispan.transaction.totalorder.TotalOrderManager; import org.infinispan.transaction.xa.GlobalTransaction; import org.infinispan.util.concurrent.BlockingTaskAwareExecutorService; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; /** * Created to control the total order validation. It disable the possibility of acquiring locks during execution through * the cache API * * @author Pedro Ruivo * @author Mircea.Markus@jboss.com */ public class TotalOrderInterceptor extends DDAsyncInterceptor { private static final Log log = LogFactory.getLog(TotalOrderInterceptor.class); private static final boolean trace = log.isTraceEnabled(); private TransactionTable transactionTable; private TotalOrderManager totalOrderManager; private ClusteringDependentLogic clusteringDependentLogic; private BlockingTaskAwareExecutorService executorService; @Inject public void inject(TransactionTable transactionTable, TotalOrderManager totalOrderManager, ClusteringDependentLogic clusteringDependentLogic, @ComponentName(value = KnownComponentNames.REMOTE_COMMAND_EXECUTOR) BlockingTaskAwareExecutorService executorService) { this.transactionTable = transactionTable; this.totalOrderManager = totalOrderManager; this.clusteringDependentLogic = clusteringDependentLogic; this.executorService = executorService; } @Override public final Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable { if (log.isDebugEnabled()) { log.debugf("Prepare received. Transaction=%s, Affected keys=%s, Local=%s", command.getGlobalTransaction().globalId(), toStr(command.getAffectedKeys()), ctx.isOriginLocal()); } if (!(command instanceof TotalOrderPrepareCommand)) { throw new IllegalStateException("TotalOrderInterceptor can only handle TotalOrderPrepareCommand"); } TotalOrderRemoteTransactionState state = getTransactionState(ctx); try { simulateLocking(ctx, command, clusteringDependentLogic); if (ctx.isOriginLocal()) { return invokeNextAndFinally(ctx, command, (rCtx, rCommand, rv, t) -> { if (t != null) { rollbackTxOnPrepareException(rCtx, (PrepareCommand) rCommand, t); } }); } state.preparing(); if (state.isRollbackReceived()) { //this means that rollback has already been received transactionTable.removeRemoteTransaction(command.getGlobalTransaction()); throw new CacheException("Cannot prepare transaction" + command.getGlobalTransaction().globalId() + ". it was already marked as rollback"); } if (state.isCommitReceived()) { log.tracef("Transaction %s marked for commit, skipping the write skew check and forcing 1PC", command.getGlobalTransaction().globalId()); ((TotalOrderPrepareCommand) command).markSkipWriteSkewCheck(); ((TotalOrderPrepareCommand) command).markAsOnePhaseCommit(); } if (trace) { log.tracef("Validating transaction %s ", command.getGlobalTransaction().globalId()); } return invokeNextAndFinally(ctx, command, (rCtx, rCommand, rv, t) -> { afterPrepare((TxInvocationContext) rCtx, (PrepareCommand) rCommand, state, t); }); } catch (Throwable t) { afterPrepare(ctx, command, state, t); throw t; } } private void rollbackTxOnPrepareException(InvocationContext ctx, PrepareCommand command, Throwable throwable) { if (log.isDebugEnabled()) { log.debugf(throwable, "Exception while preparing for transaction %s. Local=%s", command.getGlobalTransaction().globalId(), ctx.isOriginLocal()); } if (command.isOnePhaseCommit()) { transactionTable.remoteTransactionRollback(command.getGlobalTransaction()); } } private void afterPrepare(TxInvocationContext ctx, PrepareCommand command, TotalOrderRemoteTransactionState state, Throwable t) { if (t == null && command.isOnePhaseCommit()) { totalOrderManager.release(state); } state.prepared(); if (t != null) { rollbackTxOnPrepareException(ctx, command, t); } } @Override public Object visitRollbackCommand(TxInvocationContext ctx, RollbackCommand command) throws Throwable { return visitSecondPhaseCommand(ctx, command, false); } @Override public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable { return visitSecondPhaseCommand(ctx, command, true); } @Override public final Object visitLockControlCommand(TxInvocationContext ctx, LockControlCommand command) throws Throwable { throw new UnsupportedOperationException("Lock interface not supported with total order protocol"); } private Object visitSecondPhaseCommand(TxInvocationContext context, AbstractTransactionBoundaryCommand command, boolean commit) throws Throwable { GlobalTransaction gtx = command.getGlobalTransaction(); if (trace) { log.tracef("Second phase command received. Commit?=%s Transaction=%s, Local=%s", commit, gtx.globalId(), context.isOriginLocal()); } TotalOrderRemoteTransactionState state = getTransactionState(context); try { if (!processSecondCommand(state, commit) && !context.isOriginLocal()) { //we can return here, because we set onePhaseCommit to prepare and it will release all the resources return null; } } catch (Throwable t) { finishSecondPhaseCommand(commit, state, context, command); throw t; } return invokeNextAndFinally(context, command, (rCtx, rCommand, rv, t) -> finishSecondPhaseCommand(commit, state, rCtx, (AbstractTransactionBoundaryCommand) rCommand)); } private void finishSecondPhaseCommand(boolean commit, TotalOrderRemoteTransactionState state, InvocationContext ctx, AbstractTransactionBoundaryCommand txCommand) { if (state != null && state.isFinished()) { totalOrderManager.release(state); if (commit) { transactionTable.remoteTransactionCommitted(txCommand.getGlobalTransaction(), false); } else { transactionTable.remoteTransactionRollback(txCommand.getGlobalTransaction()); } if (ctx.isOriginLocal()) { executorService.checkForReadyTasks(); } } } private TotalOrderRemoteTransactionState getTransactionState(TxInvocationContext context) { if (!context.isOriginLocal()) { return ((RemoteTransaction) context.getCacheTransaction()).getTransactionState(); } RemoteTransaction remoteTransaction = transactionTable.getRemoteTransaction(context.getGlobalTransaction()); return remoteTransaction == null ? null : remoteTransaction.getTransactionState(); } private boolean processSecondCommand(TotalOrderRemoteTransactionState state, boolean commit) { //it means that the transaction was not prepared (i.e. a suicide transaction) if (state == null) { return true; } try { return state.waitUntilPrepared(commit); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.timeoutWaitingUntilTransactionPrepared(state.getGlobalTransaction().globalId()); } return false; } private void simulateLocking(TxInvocationContext context, PrepareCommand command, ClusteringDependentLogic clusteringDependentLogic) { Collection<?> affectedKeys = command.getAffectedKeys(); //this map is only populated after locks are acquired. However, no locks are acquired when total order is enabled //so we need to populate it here context.addAllAffectedKeys(command.getAffectedKeys()); //prepare can be send more than once if we have a state transfer context.clearLockedKeys(); for (Object k : affectedKeys) { if (clusteringDependentLogic.getCacheTopology().getDistribution(k).isPrimary()) { context.addLockedKey(k); } } } }