package org.infinispan.interceptors.locking;
import static org.infinispan.commons.util.Util.toStr;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import org.infinispan.commands.read.GetAllCommand;
import org.infinispan.commands.tx.CommitCommand;
import org.infinispan.commands.tx.RollbackCommand;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.impl.FlagBitSets;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.partitionhandling.impl.PartitionHandlingManager;
import org.infinispan.remoting.rpc.RpcManager;
import org.infinispan.statetransfer.OutdatedTopologyException;
import org.infinispan.util.concurrent.locks.LockUtil;
import org.infinispan.util.concurrent.locks.PendingLockManager;
import org.infinispan.util.logging.Log;
/**
* Base class for transaction based locking interceptors.
*
* @author Mircea.Markus@jboss.com
*/
public abstract class AbstractTxLockingInterceptor extends AbstractLockingInterceptor {
protected final boolean trace = getLog().isTraceEnabled();
protected RpcManager rpcManager;
private PartitionHandlingManager partitionHandlingManager;
private PendingLockManager pendingLockManager;
@Inject
public void setDependencies(RpcManager rpcManager,
PartitionHandlingManager partitionHandlingManager,
PendingLockManager pendingLockManager) {
this.rpcManager = rpcManager;
this.partitionHandlingManager = partitionHandlingManager;
this.pendingLockManager = pendingLockManager;
}
@Override
public Object visitRollbackCommand(TxInvocationContext ctx, RollbackCommand command) throws Throwable {
return invokeNextAndFinally(ctx, command, unlockAllReturnHandler);
}
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
if (command.hasAnyFlag(FlagBitSets.PUT_FOR_EXTERNAL_READ)) {
// Cache.putForExternalRead() is non-transactional
return visitNonTxDataWriteCommand(ctx, command);
}
return visitDataWriteCommand(ctx, command);
}
@Override
public Object visitGetAllCommand(InvocationContext ctx, GetAllCommand command) throws Throwable {
if (ctx.isInTxScope())
return invokeNext(ctx, command);
return invokeNextAndFinally(ctx, command, unlockAllReturnHandler);
}
@Override
public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable {
return invokeNextAndFinally(ctx, command, (rCtx, rCommand, rv, t) -> {
if (t instanceof OutdatedTopologyException)
throw t;
releaseLockOnTxCompletion(((TxInvocationContext) rCtx));
});
}
/**
* The backup (non-primary) owners keep a "backup lock" for each key they received in a lock/prepare command.
* Normally there can be many transactions holding the backup lock at the same time, but when the secondary owner
* becomes a primary owner a new transaction trying to obtain the "real" lock will have to wait for all backup
* locks to be released. The backup lock will be released either by a commit/rollback/unlock command or by
* the originator leaving the cluster (if recovery is disabled).
*
* @return {@code true} if the key was really locked.
*/
protected final boolean lockOrRegisterBackupLock(TxInvocationContext<?> ctx, Object key, long lockTimeout)
throws InterruptedException {
switch (LockUtil.getLockOwnership(key, cdl)) {
case PRIMARY:
if (trace) {
getLog().tracef("Acquiring locks on %s.", toStr(key));
}
checkPendingAndLockKey(ctx, key, lockTimeout);
return true;
case BACKUP:
if (trace) {
getLog().tracef("Acquiring backup locks on %s.", key);
}
ctx.getCacheTransaction().addBackupLockForKey(key);
return false;
default:
return false;
}
}
/**
* Same as {@link #lockOrRegisterBackupLock(TxInvocationContext, Object, long)}
*
* @return a collection with the keys locked.
*/
protected final Collection<Object> lockAllOrRegisterBackupLock(TxInvocationContext<?> ctx, Collection<?> keys,
long lockTimeout) throws InterruptedException {
if (keys.isEmpty()) {
return Collections.emptyList();
}
final Log log = getLog();
Collection<Object> keysToLock = new ArrayList<>(keys.size());
for (Object key : keys) {
switch (LockUtil.getLockOwnership(key, cdl)) {
case PRIMARY:
if (trace) {
log.tracef("Acquiring locks on %s.", toStr(key));
}
keysToLock.add(key);
break;
case BACKUP:
if (trace) {
log.tracef("Acquiring backup locks on %s.", toStr(key));
}
ctx.getCacheTransaction().addBackupLockForKey(key);
break;
default:
break;
}
}
if (keysToLock.isEmpty()) {
return Collections.emptyList();
}
checkPendingAndLockAllKeys(ctx, keysToLock, lockTimeout);
return keysToLock;
}
/**
* Besides acquiring a lock, this method also handles the following situation:
* 1. consistentHash("k") == {A, B}, tx1 prepared on A and B. Then node A crashed (A == single lock owner)
* 2. at this point tx2 which also writes "k" tries to prepare on B.
* 3. tx2 has to determine that "k" is already locked by another tx (i.e. tx1) and it has to wait for that tx to finish before acquiring the lock.
*
* The algorithm used at step 3 is:
* - the transaction table(TT) associates the current topology id with every remote and local transaction it creates
* - TT also keeps track of the minimal value of all the topology ids of all the transactions still present in the cache (minTopologyId)
* - when a tx wants to acquire lock "k":
* - if tx.topologyId > TT.minTopologyId then "k" might be a key whose owner crashed. If so:
* - obtain the list LT of transactions that started in a previous topology (txTable.getTransactionsPreparedBefore)
* - for each t in LT:
* - if t wants to write "k" then block until t finishes (CacheTransaction.waitForTransactionsToFinishIfItWritesToKey)
* - only then try to acquire lock on "k"
* - if tx.topologyId == TT.minTopologyId try to acquire lock straight away.
*
* Note: The algorithm described below only when nodes leave the cluster, so it doesn't add a performance burden
* when the cluster is stable.
*/
private void checkPendingAndLockKey(InvocationContext ctx, Object key, long lockTimeout) throws InterruptedException {
final long remaining = pendingLockManager.awaitPendingTransactionsForKey((TxInvocationContext<?>) ctx, key,
lockTimeout, TimeUnit.MILLISECONDS);
lockAndRecord(ctx, key, remaining);
}
private void checkPendingAndLockAllKeys(InvocationContext ctx, Collection<Object> keys, long lockTimeout)
throws InterruptedException {
final long remaining = pendingLockManager.awaitPendingTransactionsForAllKeys((TxInvocationContext<?>) ctx, keys,
lockTimeout, TimeUnit.MILLISECONDS);
lockAllAndRecord(ctx, keys, remaining);
}
protected void releaseLockOnTxCompletion(TxInvocationContext ctx) {
boolean shouldReleaseLocks = ctx.isOriginLocal() &&
!partitionHandlingManager.isTransactionPartiallyCommitted(ctx.getGlobalTransaction());
if (shouldReleaseLocks) {
lockManager.unlockAll(ctx);
}
}
}