package org.infinispan.interceptors.locking; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; import org.infinispan.commands.CommandsFactory; import org.infinispan.commands.DataCommand; import org.infinispan.commands.FlagAffectedCommand; import org.infinispan.commands.control.LockControlCommand; import org.infinispan.commands.read.GetAllCommand; import org.infinispan.commands.remote.recovery.TxCompletionNotificationCommand; import org.infinispan.commands.tx.PrepareCommand; import org.infinispan.commands.write.ApplyDeltaCommand; import org.infinispan.commands.write.DataWriteCommand; import org.infinispan.context.InvocationContext; import org.infinispan.context.impl.FlagBitSets; import org.infinispan.context.impl.TxInvocationContext; import org.infinispan.distribution.DistributionInfo; import org.infinispan.factories.annotations.Inject; import org.infinispan.remoting.inboundhandler.DeliverOrder; import org.infinispan.statetransfer.OutdatedTopologyException; import org.infinispan.statetransfer.StateTransferManager; import org.infinispan.transaction.impl.LocalTransaction; import org.infinispan.util.concurrent.locks.LockUtil; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; /** * Locking interceptor to be used by pessimistic caches. * Design note: when a lock "k" needs to be acquired (e.g. cache.put("k", "v")), if the lock owner is the local node, * no remote call is performed to migrate locking logic to the other (numOwners - 1) lock owners. This is a good * optimisation for in-vm transactions: if the local node crashes before prepare then the replicated lock information * would be useless as the tx is rolled back. OTOH for remote hotrod/transactions this additional RPC makes sense because * there's no such thing as transaction originator node, so this might become a configuration option when HotRod tx are * in place. * * Implementation note: current implementation acquires locks remotely first and then locally. This is required * by the deadlock detection logic, but might not be optimal: acquiring locks locally first might help to fail fast the * in the case of keys being locked. * * @author Mircea Markus */ public class PessimisticLockingInterceptor extends AbstractTxLockingInterceptor { private static final Log log = LogFactory.getLog(PessimisticLockingInterceptor.class); public static final boolean trace = log.isTraceEnabled(); private CommandsFactory cf; private StateTransferManager stateTransferManager; @Override protected Log getLog() { return log; } @Inject public void init(CommandsFactory factory, StateTransferManager stateTransferManager) { this.cf = factory; this.stateTransferManager = stateTransferManager; } @Override protected final Object visitDataReadCommand(InvocationContext ctx, DataCommand command) throws Throwable { if (!readNeedsLock(ctx, command)) { return invokeNext(ctx, command); } try { if (!readNeedsLock(ctx, command)) { return invokeNext(ctx, command); } Object key = command.getKey(); if (!needRemoteLocks(ctx, key, command)) { acquireLocalLock(ctx, command); return invokeNext(ctx, command); } TxInvocationContext txContext = (TxInvocationContext) ctx; LockControlCommand lcc = cf.buildLockControlCommand(key, command.getFlagsBitSet(), txContext.getGlobalTransaction()); Object result = invokeNextThenApply(ctx, lcc, (rCtx, rCommand, rv) -> invokeNext(rCtx, command)); return makeStage(result).andFinally(ctx, command, (rCtx, rCommand, rv, t) -> { if (t != null) { rethrowAndReleaseLocksIfNeeded(rCtx, t); } else { acquireLocalLock(rCtx, (DataCommand) rCommand); } }); } catch (Throwable t) { rethrowAndReleaseLocksIfNeeded(ctx, t); throw t; } } private boolean readNeedsLock(InvocationContext ctx, FlagAffectedCommand command) { return ctx.isInTxScope() && command.hasAnyFlag(FlagBitSets.FORCE_WRITE_LOCK) && !hasSkipLocking(command); } private void acquireLocalLock(InvocationContext ctx, DataCommand command) throws InterruptedException { if (trace) log.tracef("acquireLocalLock"); final TxInvocationContext txContext = (TxInvocationContext) ctx; Object key = command.getKey(); lockOrRegisterBackupLock(txContext, key, getLockTimeoutMillis(command)); txContext.addAffectedKey(key); } @Override public Object visitGetAllCommand(InvocationContext ctx, GetAllCommand command) throws Throwable { try { Object stage; if (!readNeedsLock(ctx, command)) { stage = invokeNext(ctx, command); } else { Collection<?> keys = command.getKeys(); if (!needRemoteLocks(ctx, keys, command)) { acquireLocalLocks(ctx, command, keys); stage = invokeNext(ctx, command); } else { // Acquire the remote locks first, then the local locks final TxInvocationContext txContext = (TxInvocationContext) ctx; LockControlCommand lcc = cf.buildLockControlCommand(keys, command.getFlagsBitSet(), txContext.getGlobalTransaction()); stage = invokeNextThenApply(ctx, lcc, (rCtx, rLockCommand, rv) -> { acquireLocalLocks(rCtx, command, keys); return invokeNext(rCtx, command); }); } } return makeStage(stage).andExceptionally(ctx, command, (rCtx, rCommand, t) -> { releaseLocksOnFailureBeforePrepare(rCtx); throw t; }); } catch (Throwable t) { releaseLocksOnFailureBeforePrepare(ctx); throw t; } } private void acquireLocalLocks(InvocationContext ctx, FlagAffectedCommand command, Collection<?> keys) throws InterruptedException { lockAllOrRegisterBackupLock((TxInvocationContext<?>) ctx, keys, getLockTimeoutMillis(command)); ((TxInvocationContext<?>) ctx).addAllAffectedKeys(keys); } @Override public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable { if (!command.isOnePhaseCommit()) { return invokeNext(ctx, command); } // Don't release the locks on exception, the RollbackCommand will do it return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> releaseLockOnTxCompletion(((TxInvocationContext) rCtx))); } @Override protected <K> Object handleWriteManyCommand(InvocationContext ctx, FlagAffectedCommand command, Collection<K> keys, boolean forwarded) throws Throwable { try { Object stage; if (hasSkipLocking(command)) { stage = invokeNext(ctx, command); } else { if (!needRemoteLocks(ctx, keys, command)) { acquireLocalLocks(ctx, command, keys); stage = invokeNext(ctx, command); } else { final TxInvocationContext txContext = (TxInvocationContext) ctx; LockControlCommand lcc = cf.buildLockControlCommand(keys, command.getFlagsBitSet(), txContext.getGlobalTransaction()); stage = invokeNextThenApply(ctx, lcc, (rCtx, rCommand, rv) -> { acquireLocalLocks(rCtx, command, keys); return invokeNext(rCtx, command); }); } } return makeStage(stage).andExceptionally(ctx, command, (rCtx, rCommand, t) -> { rethrowAndReleaseLocksIfNeeded(rCtx, t); throw t; }); } catch (Throwable t) { rethrowAndReleaseLocksIfNeeded(ctx, t); throw t; } } @Override protected Object visitDataWriteCommand(InvocationContext ctx, DataWriteCommand command) throws Throwable { try { Object stage; Object key = command.getKey(); if (hasSkipLocking(command)) { // Non-modifying functional write commands are executed in non-transactional context on non-originators if (ctx.isInTxScope()) { // Mark the key as affected even with SKIP_LOCKING ((TxInvocationContext<?>) ctx).addAffectedKey(key); } stage = invokeNext(ctx, command); } else { if (!needRemoteLocks(ctx, key, command)) { acquireLocalLock(ctx, command); stage = invokeNext(ctx, command); } else { final TxInvocationContext txContext = (TxInvocationContext) ctx; LockControlCommand lcc = cf.buildLockControlCommand(key, command.getFlagsBitSet(), txContext.getGlobalTransaction()); return invokeNextAndHandle(ctx, lcc, (rCtx, rCommand, rv, t) -> { rethrowAndReleaseLocksIfNeeded(rCtx, t); acquireLocalLock(rCtx, command); return invokeNext(rCtx, command); }); } } return makeStage(stage).andExceptionally(ctx, command, (rCtx, rCommand, t) -> { rethrowAndReleaseLocksIfNeeded(rCtx, t); throw t; }); } catch (Throwable t) { releaseLocksOnFailureBeforePrepare(ctx); throw t; } } @Override public Object visitApplyDeltaCommand(InvocationContext ctx, ApplyDeltaCommand command) throws Throwable { try { Object stage; if (hasSkipLocking(command)) { stage = invokeNext(ctx, command); } else { Object[] compositeKeys = command.getCompositeKeys(); Set<Object> keysToLock = new HashSet<>(Arrays.asList(compositeKeys)); if (!needRemoteLocks(ctx, keysToLock, command)) { ((TxInvocationContext<?>) ctx).addAllAffectedKeys(keysToLock); acquireLocalCompositeLocks(command, keysToLock, ctx); stage = invokeNext(ctx, command); } else { final TxInvocationContext txContext = (TxInvocationContext) ctx; LockControlCommand lcc = cf.buildLockControlCommand(keysToLock, command.getFlagsBitSet(), txContext.getGlobalTransaction()); stage = invokeNextThenApply(ctx, lcc, (rCtx, rCommand, rv) -> { ((TxInvocationContext<?>) rCtx).addAllAffectedKeys(keysToLock); acquireLocalCompositeLocks(command, keysToLock, rCtx); return invokeNext(rCtx, command); }); } } return makeStage(stage).andExceptionally(ctx, command, (rCtx, rCommand, t) -> { lockManager.unlockAll(rCtx); throw t; }); } catch (Throwable t) { lockManager.unlockAll(ctx); throw t; } } private void acquireLocalCompositeLocks(ApplyDeltaCommand command, Set<Object> keysToLock, InvocationContext ctx1) throws InterruptedException { DistributionInfo distributionInfo = cdl.getCacheTopology().getDistribution(command.getKey()); if (distributionInfo.isPrimary()) { lockAllAndRecord(ctx1, keysToLock, getLockTimeoutMillis(command)); } else if (distributionInfo.isWriteOwner()) { TxInvocationContext<?> txContext = (TxInvocationContext<?>) ctx1; for (Object key : keysToLock) { txContext.getCacheTransaction().addBackupLockForKey(key); } } } @Override public Object visitLockControlCommand(TxInvocationContext ctx, LockControlCommand command) throws Throwable { if (!ctx.isInTxScope()) throw new IllegalStateException("Locks should only be acquired within the scope of a transaction!"); boolean skipLocking = hasSkipLocking(command); if (skipLocking) { return false; } // First go through the distribution interceptor to acquire the remote lock - required by DLD. // Only acquire remote lock if multiple keys or the single key primary owner is not the local node. if (ctx.isOriginLocal()) { final boolean isSingleKeyAndLocal = !command.multipleKeys() && cdl.getCacheTopology().getDistribution(command.getSingleKey()).isPrimary(); boolean needBackupLocks = !isSingleKeyAndLocal || isStateTransferInProgress(); if (needBackupLocks && !command.hasAnyFlag(FlagBitSets.CACHE_MODE_LOCAL)) { LocalTransaction localTx = (LocalTransaction) ctx.getCacheTransaction(); if (localTx.getAffectedKeys().containsAll(command.getKeys())) { if (trace) log.tracef("Already own locks on keys: %s, skipping remote call", command.getKeys()); return true; } } else { if (trace) log.tracef("Single key %s and local, skipping remote call", command.getSingleKey()); return localLockCommandWork(ctx, command); } } return invokeNextAndHandle(ctx, command, (rCtx, rCommand, rv, t) -> { rethrowAndReleaseLocksIfNeeded(rCtx, t); return localLockCommandWork(rCtx, (LockControlCommand) rCommand); }); } private boolean localLockCommandWork(InvocationContext ctx, LockControlCommand command) throws InterruptedException { TxInvocationContext<?> txInvocationContext = (TxInvocationContext<?>) ctx; if (ctx.isOriginLocal()) { txInvocationContext.addAllAffectedKeys(command.getKeys()); } if (command.isUnlock()) { if (ctx.isOriginLocal()) throw new AssertionError( "There's no advancedCache.unlock so this must have originated remotely."); releaseLocksOnFailureBeforePrepare(ctx); return false; } try { lockAllOrRegisterBackupLock(txInvocationContext, command.getKeys(), getLockTimeoutMillis(command)); } catch (Throwable t) { releaseLocksOnFailureBeforePrepare(ctx); throw t; } return true; } private void rethrowAndReleaseLocksIfNeeded(InvocationContext ctx, Throwable throwable) throws Throwable { if (throwable != null) { // If the command will be retried, there is no need to release this or other locks if (!(throwable instanceof OutdatedTopologyException)) { releaseLocksOnFailureBeforePrepare(ctx); } throw throwable; } } private boolean needRemoteLocks(InvocationContext ctx, Collection<?> keys, FlagAffectedCommand command) throws Throwable { boolean needBackupLocks = ctx.isOriginLocal() && (!isLockOwner(keys) || isStateTransferInProgress()); boolean needRemoteLock = false; if (needBackupLocks && !command.hasAnyFlag(FlagBitSets.CACHE_MODE_LOCAL)) { final TxInvocationContext txContext = (TxInvocationContext) ctx; LocalTransaction localTransaction = (LocalTransaction) txContext.getCacheTransaction(); needRemoteLock = !localTransaction.getAffectedKeys().containsAll(keys); if (!needRemoteLock) { if (trace) log.tracef("We already have lock for keys %s, skip remote lock acquisition", keys); } } return needRemoteLock; } private boolean needRemoteLocks(InvocationContext ctx, Object key, FlagAffectedCommand command) throws Throwable { boolean needBackupLocks = ctx.isOriginLocal() && (!isLockOwner(key) || isStateTransferInProgress()); boolean needRemoteLock = false; if (needBackupLocks && !command.hasAnyFlag(FlagBitSets.CACHE_MODE_LOCAL)) { final TxInvocationContext txContext = (TxInvocationContext) ctx; LocalTransaction localTransaction = (LocalTransaction) txContext.getCacheTransaction(); needRemoteLock = !localTransaction.getAffectedKeys().contains(key); if (!needRemoteLock) { if (trace) log.tracef("We already have lock for key %s, skip remote lock acquisition", key); } } else { if (trace) log.tracef("Don't need backup locks %s", needBackupLocks); } return needRemoteLock; } private boolean isLockOwner(Collection<?> keys) { for (Object key : keys) { if (!LockUtil.isLockOwner(key, cdl)) { return false; } } return true; } private boolean isLockOwner(Object key) { return LockUtil.isLockOwner(key, cdl); } private boolean isStateTransferInProgress() { return stateTransferManager != null && stateTransferManager.isStateTransferInProgress(); } private void releaseLocksOnFailureBeforePrepare(InvocationContext ctx) { TxInvocationContext txContext = (TxInvocationContext) ctx; try { lockManager.unlockAll(ctx); if (ctx.isOriginLocal() && ctx.isInTxScope() && rpcManager != null) { TxCompletionNotificationCommand command = cf.buildTxCompletionNotificationCommand(null, txContext.getGlobalTransaction()); final LocalTransaction cacheTransaction = (LocalTransaction) txContext.getCacheTransaction(); if (!cacheTransaction.getRemoteLocksAcquired().isEmpty()) { rpcManager.invokeRemotely(cacheTransaction.getRemoteLocksAcquired(), command, rpcManager.getDefaultRpcOptions(false, DeliverOrder.NONE)); } } } catch (Throwable t) { log.debugf(t, "Error releasing locks for failure before prepare for transaction %s", txContext.getCacheTransaction()); } } }