All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.infinispan.statetransfer.StateTransferInterceptor Maven / Gradle / Ivy

There is a newer version: 9.1.7.Final
Show newest version
package org.infinispan.statetransfer;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import org.infinispan.commands.AbstractTopologyAffectedCommand;
import org.infinispan.commands.FlagAffectedCommand;
import org.infinispan.commands.TopologyAffectedCommand;
import org.infinispan.commands.VisitableCommand;
import org.infinispan.commands.control.LockControlCommand;
import org.infinispan.commands.functional.ReadOnlyKeyCommand;
import org.infinispan.commands.functional.ReadOnlyManyCommand;
import org.infinispan.commands.functional.ReadWriteKeyCommand;
import org.infinispan.commands.functional.ReadWriteKeyValueCommand;
import org.infinispan.commands.read.GetAllCommand;
import org.infinispan.commands.read.GetCacheEntryCommand;
import org.infinispan.commands.read.GetKeyValueCommand;
import org.infinispan.commands.tx.CommitCommand;
import org.infinispan.commands.tx.PrepareCommand;
import org.infinispan.commands.tx.RollbackCommand;
import org.infinispan.commands.tx.TransactionBoundaryCommand;
import org.infinispan.commands.write.ApplyDeltaCommand;
import org.infinispan.commands.write.ClearCommand;
import org.infinispan.commands.write.EvictCommand;
import org.infinispan.commands.write.InvalidateCommand;
import org.infinispan.commands.write.InvalidateL1Command;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.commands.write.PutMapCommand;
import org.infinispan.commands.write.RemoveCommand;
import org.infinispan.commands.write.ReplaceCommand;
import org.infinispan.commands.write.WriteCommand;
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.factories.annotations.Start;
import org.infinispan.interceptors.BasicInvocationStage;
import org.infinispan.interceptors.InvocationComposeHandler;
import org.infinispan.interceptors.InvocationStage;
import org.infinispan.interceptors.impl.BaseStateTransferInterceptor;
import org.infinispan.remoting.RemoteException;
import org.infinispan.remoting.responses.UnsureResponse;
import org.infinispan.remoting.transport.Address;
import org.infinispan.remoting.transport.jgroups.SuspectException;
import org.infinispan.topology.CacheTopology;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

//todo [anistor] command forwarding breaks the rule that we have only one originator for a command. this opens now the possibility to have two threads processing incoming remote commands for the same TX
/**
 * This interceptor has two tasks:
 * 
    *
  1. If the command's topology id is higher than the current topology id, * wait for the node to receive transaction data for the new topology id.
  2. *
  3. If the topology id changed during a command's execution, retry the command, but only on the * originator (which replicates it to the new owners).
  4. *
* * If the cache is configured with asynchronous replication, owners cannot signal to the originator that they * saw a new topology, so instead each owner forwards the command to all the other owners in the new topology. * * @author [email protected] */ public class StateTransferInterceptor extends BaseStateTransferInterceptor { private static final Log log = LogFactory.getLog(StateTransferInterceptor.class); private static boolean trace = log.isTraceEnabled(); private StateTransferManager stateTransferManager; private boolean syncCommitPhase; private boolean defaultSynchronous; private final AffectedKeysVisitor affectedKeysVisitor = new AffectedKeysVisitor(); private final InvocationComposeHandler handleReadCommandReturn = this::handleReadCommandReturn; private final InvocationComposeHandler handleTxReturn = this::handleTxReturn; private final InvocationComposeHandler handleTxWriteReturn = this::handleTxWriteReturn; private final InvocationComposeHandler handleNonTxWriteReturn = this::handleNonTxWriteReturn; @Inject public void init(StateTransferManager stateTransferManager) { this.stateTransferManager = stateTransferManager; } @Start public void start() { syncCommitPhase = cacheConfiguration.transaction().syncCommitPhase(); defaultSynchronous = cacheConfiguration.clustering().cacheMode().isSynchronous(); } @Override public BasicInvocationStage visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable { return handleTxCommand(ctx, command); } @Override public BasicInvocationStage visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable { return handleTxCommand(ctx, command); } @Override public BasicInvocationStage visitRollbackCommand(TxInvocationContext ctx, RollbackCommand command) throws Throwable { return handleTxCommand(ctx, command); } @Override public BasicInvocationStage visitLockControlCommand(TxInvocationContext ctx, LockControlCommand command) throws Throwable { return handleTxCommand(ctx, command); } @Override public BasicInvocationStage visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitApplyDeltaCommand(InvocationContext ctx, ApplyDeltaCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitClearCommand(InvocationContext ctx, ClearCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitInvalidateCommand(InvocationContext ctx, InvalidateCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitInvalidateL1Command(InvocationContext ctx, InvalidateL1Command command) throws Throwable { // no need to forward this command return invokeNext(ctx, command); } @Override public BasicInvocationStage visitEvictCommand(InvocationContext ctx, EvictCommand command) throws Throwable { // it's not necessary to propagate eviction to the new owners in case of state transfer return invokeNext(ctx, command); } @Override public BasicInvocationStage visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable { return handleReadCommand(ctx, command); } @Override public BasicInvocationStage visitGetCacheEntryCommand(InvocationContext ctx, GetCacheEntryCommand command) throws Throwable { return handleReadCommand(ctx, command); } @Override public BasicInvocationStage visitGetAllCommand(InvocationContext ctx, GetAllCommand command) throws Throwable { return handleReadCommand(ctx, command); } private InvocationStage handleReadCommand(InvocationContext ctx, AbstractTopologyAffectedCommand command) throws Throwable { if (isLocalOnly(command)) { return invokeNext(ctx, command); } updateTopologyId(command); return invokeNext(ctx, command) .compose(handleReadCommandReturn); } private BasicInvocationStage handleReadCommandReturn(BasicInvocationStage stage, InvocationContext rCtx, VisitableCommand rCommand, Object rv, Throwable t) throws Throwable { if (t == null) return stage; Throwable ce = t; while (ce instanceof RemoteException) { ce = ce.getCause(); } final CacheTopology cacheTopology = stateTransferManager.getCacheTopology(); int currentTopologyId = cacheTopology == null ? -1 : cacheTopology.getTopologyId(); AbstractTopologyAffectedCommand cmd = (AbstractTopologyAffectedCommand) rCommand; if (ce instanceof SuspectException) { if (trace) log.tracef("Retrying command because of suspected node, current topology is %d: %s", currentTopologyId, rCommand); // It is possible that current topology is actual but the view still contains a node that's about to leave; // a broadcast to all nodes then can end with suspect exception, but we won't get any new topology. // An example of this situation is when a node sends leave - topology can be installed before the new view. // To prevent suspect exceptions use SYNCHRONOUS_IGNORE_LEAVERS response mode. if (currentTopologyId == cmd.getTopologyId() && !cacheTopology.getActualMembers().contains(((SuspectException) ce).getSuspect())) { // TODO: provide a test case throw new IllegalStateException("Command was not sent with SYNCHRONOUS_IGNORE_LEAVERS?"); } } else if (ce instanceof OutdatedTopologyException) { if (trace) log.tracef("Retrying command because of topology change, current topology is %d: %s", currentTopologyId, cmd); } else { return stage; } // We increment the topology to wait for the next topology. // Without this, we could retry the command too fast and we could get the OutdatedTopologyException again. int newTopologyId = getNewTopologyId(ce, currentTopologyId, cmd); cmd.setTopologyId(newTopologyId); cmd.addFlags(FlagBitSets.COMMAND_RETRY); CompletableFuture topologyFuture = stateTransferLock.topologyFuture(newTopologyId); return retryWhenDone(topologyFuture, newTopologyId, rCtx, cmd) .compose(handleReadCommandReturn); } @Override public BasicInvocationStage visitReadWriteKeyValueCommand(InvocationContext ctx, ReadWriteKeyValueCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitReadWriteKeyCommand(InvocationContext ctx, ReadWriteKeyCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public BasicInvocationStage visitReadOnlyKeyCommand(InvocationContext ctx, ReadOnlyKeyCommand command) throws Throwable { return handleReadCommand(ctx, command); } @Override public BasicInvocationStage visitReadOnlyManyCommand(InvocationContext ctx, ReadOnlyManyCommand command) throws Throwable { return handleReadCommand(ctx, command); } /** * Special processing required for transaction commands. * */ private BasicInvocationStage handleTxCommand(TxInvocationContext ctx, TransactionBoundaryCommand command) throws Throwable { if (trace) log.tracef("handleTxCommand for command %s, origin %s", command, getOrigin(ctx)); if (isLocalOnly(command)) { return invokeNext(ctx, command); } updateTopologyId(command); return invokeNext(ctx, command) .compose(handleTxReturn); } private Address getOrigin(TxInvocationContext ctx) { // For local commands we may not have a GlobalTransaction yet return ctx.isOriginLocal() ? ctx.getOrigin() : ctx.getGlobalTransaction().getAddress(); } private BasicInvocationStage handleTxReturn(BasicInvocationStage stage, InvocationContext ctx, VisitableCommand command, Object rv, Throwable t) throws Throwable { TransactionBoundaryCommand txCommand = (TransactionBoundaryCommand) command; int retryTopologyId = -1; int currentTopology = currentTopologyId(); if (t instanceof OutdatedTopologyException) { // This can only happen on the originator retryTopologyId = Math.max(currentTopology, txCommand.getTopologyId() + 1); } else if (t != null) { return stage; } // We need to forward the command to the new owners, if the command was asynchronous boolean async = isTxCommandAsync(txCommand); if (async) { stateTransferManager.forwardCommandIfNeeded(txCommand, getAffectedKeys(ctx, txCommand), getOrigin((TxInvocationContext) ctx)); return stage; } if (ctx.isOriginLocal()) { // On the originator, we only retry if we got an OutdatedTopologyException // Which could be caused either by an owner leaving or by an owner having a newer topology // No need to retry just because we have a new topology on the originator, all entries were // wrapped anyway if (retryTopologyId > 0) { // Only the originator can retry the command txCommand.setTopologyId(retryTopologyId); if (txCommand instanceof PrepareCommand) { ((PrepareCommand) txCommand).setRetriedCommand(true); } CompletableFuture transactionDataFuture = stateTransferLock.transactionDataFuture(retryTopologyId); return retryWhenDone(transactionDataFuture, retryTopologyId, ctx, txCommand) .compose(handleTxReturn); } } else { if (currentTopology > txCommand.getTopologyId()) { // Signal the originator to retry return returnWith(UnsureResponse.INSTANCE); } } return stage; } private boolean isTxCommandAsync(TransactionBoundaryCommand command) { boolean async = false; if (command instanceof CommitCommand || command instanceof RollbackCommand) { async = !syncCommitPhase; } else if (command instanceof PrepareCommand) { async = !defaultSynchronous; } return async; } protected BasicInvocationStage handleWriteCommand(InvocationContext ctx, WriteCommand command) throws Throwable { if (ctx.isInTxScope()) { return handleTxWriteCommand(ctx, command); } else { return handleNonTxWriteCommand(ctx, command); } } private BasicInvocationStage handleTxWriteCommand(InvocationContext ctx, WriteCommand command) throws Throwable { if (trace) log.tracef("handleTxWriteCommand for command %s, origin %s", command, ctx.getOrigin()); if (isLocalOnly(command)) { return invokeNext(ctx, command); } updateTopologyId(command); return invokeNext(ctx, command) .compose(handleTxWriteReturn); } private BasicInvocationStage handleTxWriteReturn(BasicInvocationStage stage, InvocationContext rCtx, VisitableCommand rCommand, Object rv, Throwable t) throws Throwable { int retryTopologyId = -1; WriteCommand writeCommand = (WriteCommand) rCommand; if (t instanceof OutdatedTopologyException) { // This can only happen on the originator retryTopologyId = Math.max(currentTopologyId(), writeCommand.getTopologyId() + 1); } else if (t != null) { throw t; } if (rCtx.isOriginLocal()) { // On the originator, we only retry if we got an OutdatedTopologyException // Which could be caused either by an owner leaving or by an owner having a newer topology // No need to retry just because we have a new topology on the originator, all entries were // wrapped anyway if (retryTopologyId > 0) { // Only the originator can retry the command writeCommand.setTopologyId(retryTopologyId); CompletableFuture transactionDataFuture = stateTransferLock.transactionDataFuture(retryTopologyId); return retryWhenDone(transactionDataFuture, retryTopologyId, rCtx, writeCommand) .compose(handleTxWriteReturn); } } else { if (currentTopologyId() > writeCommand.getTopologyId()) { // Signal the originator to retry return returnWith(UnsureResponse.INSTANCE); } } return stage; } /** * For non-tx write commands, we retry the command locally if the topology changed. * But we only retry on the originator, and only if the command doesn't have * the {@code CACHE_MODE_LOCAL} flag. */ private BasicInvocationStage handleNonTxWriteCommand(InvocationContext ctx, WriteCommand command) throws Throwable { if (trace) log.tracef("handleNonTxWriteCommand for command %s, topology id %d", command, command.getTopologyId()); if (isLocalOnly(command)) { return invokeNext(ctx, command); } updateTopologyId(command); // Only catch OutdatedTopologyExceptions on the originator if (!ctx.isOriginLocal()) { return invokeNext(ctx, command); } return invokeNext(ctx, command) .compose(handleNonTxWriteReturn); } private BasicInvocationStage handleNonTxWriteReturn(BasicInvocationStage stage, InvocationContext rCtx, VisitableCommand rCommand, Object rv, Throwable t) throws Throwable { if (t == null) return stage; Throwable ce = t; while (ce instanceof RemoteException) { ce = ce.getCause(); } if (!(ce instanceof OutdatedTopologyException) && !(ce instanceof SuspectException)) throw t; // We increment the topology id so that updateTopologyIdAndWaitForTransactionData waits for the // next topology. // Without this, we could retry the command too fast and we could get the // OutdatedTopologyException again. int currentTopologyId = currentTopologyId(); WriteCommand writeCommand = (WriteCommand) rCommand; if (trace) log.tracef("Retrying command because of topology change, current topology is %d: %s", currentTopologyId, writeCommand); int commandTopologyId = writeCommand.getTopologyId(); int newTopologyId = getNewTopologyId(ce, currentTopologyId, writeCommand); writeCommand.setTopologyId(newTopologyId); writeCommand.addFlags(FlagBitSets.COMMAND_RETRY); // In non-tx context, waiting for transaction data is equal to waiting for topology CompletableFuture transactionDataFuture = stateTransferLock.transactionDataFuture(newTopologyId); return retryWhenDone(transactionDataFuture, newTopologyId, rCtx, writeCommand) .compose(handleNonTxWriteReturn); } private int getNewTopologyId(Throwable ce, int currentTopologyId, TopologyAffectedCommand command) { int requestedTopologyId = command.getTopologyId() + 1; if (ce instanceof OutdatedTopologyException) { OutdatedTopologyException ote = (OutdatedTopologyException) ce; if (ote.requestedTopologyId >= 0) { requestedTopologyId = ote.requestedTopologyId; } } return Math.max(currentTopologyId, requestedTopologyId); } @Override public BasicInvocationStage handleDefault(InvocationContext ctx, VisitableCommand command) throws Throwable { if (command instanceof TopologyAffectedCommand) { return handleTopologyAffectedCommand(ctx, command, ctx.getOrigin()); } else { return invokeNext(ctx, command); } } private BasicInvocationStage handleTopologyAffectedCommand(InvocationContext ctx, VisitableCommand command, Address origin) throws Throwable { if (trace) log.tracef("handleTopologyAffectedCommand for command %s, origin %s", command, origin); if (isLocalOnly(command)) { return invokeNext(ctx, command); } updateTopologyId((TopologyAffectedCommand) command); return invokeNext(ctx, command); } private boolean isLocalOnly(VisitableCommand command) { boolean cacheModeLocal = false; if (command instanceof FlagAffectedCommand) { cacheModeLocal = ((FlagAffectedCommand)command).hasAnyFlag(FlagBitSets.CACHE_MODE_LOCAL); } return cacheModeLocal; } @SuppressWarnings("unchecked") private Set getAffectedKeys(InvocationContext ctx, VisitableCommand command) { Set affectedKeys = null; try { affectedKeys = (Set) command.acceptVisitor(ctx, affectedKeysVisitor); } catch (Throwable throwable) { // impossible to reach this } if (affectedKeys == null) { affectedKeys = Collections.emptySet(); } return affectedKeys; } @Override protected Log getLog() { return log; } }