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

org.infinispan.interceptors.impl.BaseStateTransferInterceptor Maven / Gradle / Ivy

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

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.BiFunction;

import org.infinispan.commands.AbstractVisitor;
import org.infinispan.commands.FlagAffectedCommand;
import org.infinispan.commands.TopologyAffectedCommand;
import org.infinispan.commands.VisitableCommand;
import org.infinispan.commands.functional.ReadOnlyKeyCommand;
import org.infinispan.commands.functional.ReadOnlyManyCommand;
import org.infinispan.commands.read.GetAllCommand;
import org.infinispan.commands.read.GetCacheEntryCommand;
import org.infinispan.commands.read.GetKeyValueCommand;
import org.infinispan.commands.remote.GetKeysInGroupCommand;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.impl.FlagBitSets;
import org.infinispan.distribution.DistributionManager;
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.InvocationFinallyFunction;
import org.infinispan.remoting.RemoteException;
import org.infinispan.remoting.transport.jgroups.SuspectException;
import org.infinispan.statetransfer.AllOwnersLostException;
import org.infinispan.statetransfer.OutdatedTopologyException;
import org.infinispan.statetransfer.StateTransferLock;
import org.infinispan.statetransfer.StateTransferManager;
import org.infinispan.topology.CacheTopology;
import org.infinispan.util.concurrent.CompletableFutures;
import org.infinispan.util.concurrent.TimeoutException;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * A base class for a state transfer interceptor. It contains the base code to avoid duplicating in the two current
 * different implementations.
 * 

* Also, it has some utilities methods with the most common logic. * * @author Pedro Ruivo * @since 9.0 */ public abstract class BaseStateTransferInterceptor extends DDAsyncInterceptor { private final boolean trace = getLog().isTraceEnabled(); private final InvocationFinallyFunction handleReadCommandReturn = this::handleReadCommandReturn; private StateTransferManager stateTransferManager; protected StateTransferLock stateTransferLock; private Executor remoteExecutor; private DistributionManager distributionManager; private ScheduledExecutorService timeoutExecutor; private long transactionDataTimeout; private final InvocationFinallyFunction handleLocalGetKeysInGroupReturn = this::handleLocalGetKeysInGroupReturn; @Inject public void init(StateTransferLock stateTransferLock, Configuration configuration, StateTransferManager stateTransferManager, DistributionManager distributionManager, @ComponentName(KnownComponentNames.TIMEOUT_SCHEDULE_EXECUTOR) ScheduledExecutorService timeoutExecutor, @ComponentName(KnownComponentNames.REMOTE_COMMAND_EXECUTOR) Executor remoteExecutor) { this.stateTransferLock = stateTransferLock; this.stateTransferManager = stateTransferManager; this.distributionManager = distributionManager; this.timeoutExecutor = timeoutExecutor; this.remoteExecutor = remoteExecutor; transactionDataTimeout = configuration.clustering().remoteTimeout(); } @Override public Object visitGetKeysInGroupCommand(InvocationContext ctx, GetKeysInGroupCommand command) throws Throwable { updateTopologyId(command); if (ctx.isOriginLocal()) { return invokeNextAndHandle(ctx, command, handleLocalGetKeysInGroupReturn); } else { return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> { GetKeysInGroupCommand cmd = (GetKeysInGroupCommand) rCommand; final int commandTopologyId = cmd.getTopologyId(); if (currentTopologyId() != commandTopologyId && distributionManager.getCacheTopology().isWriteOwner(cmd.getGroupName())) { throw new OutdatedTopologyException( "Cache topology changed while the command was executing: expected " + commandTopologyId + ", got " + currentTopologyId()); } }); } } private Object handleLocalGetKeysInGroupReturn(InvocationContext ctx, VisitableCommand command, Object rv, Throwable throwable) throws Throwable { GetKeysInGroupCommand cmd = (GetKeysInGroupCommand) command; final int commandTopologyId = cmd.getTopologyId(); boolean shouldRetry; if (throwable != null) { // Must retry if we got an OutdatedTopologyException or SuspectException Throwable ce = throwable; while (ce instanceof RemoteException) { ce = ce.getCause(); } shouldRetry = ce instanceof OutdatedTopologyException || ce instanceof SuspectException; } else { // Only check the topology id if if we are an owner shouldRetry = currentTopologyId() != commandTopologyId && distributionManager.getCacheTopology().isWriteOwner(cmd.getGroupName()); } if (shouldRetry) { logRetry(currentTopologyId(), cmd); // 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 newTopologyId = Math.max(currentTopologyId(), commandTopologyId + 1); cmd.setTopologyId(newTopologyId); CompletableFuture transactionDataFuture = stateTransferLock.transactionDataFuture(newTopologyId); return retryWhenDone(transactionDataFuture, newTopologyId, ctx, command, handleLocalGetKeysInGroupReturn); } else { return valueOrException(rv, throwable); } } protected final void logRetry(int currentTopologyId, TopologyAffectedCommand cmd) { if (trace) getLog().tracef("Retrying command because of topology change, current topology is %d, command topology %d: %s", currentTopologyId, cmd.getTopologyId(), cmd); } protected final int currentTopologyId() { final CacheTopology cacheTopology = stateTransferManager.getCacheTopology(); return cacheTopology == null ? -1 : cacheTopology.getTopologyId(); } protected final void updateTopologyId(TopologyAffectedCommand command) { // set the topology id if it was not set before (ie. this is local command) // TODO Make tx commands extend FlagAffectedCommand so we can use CACHE_MODE_LOCAL in TransactionTable.cleanupStaleTransactions if (command.getTopologyId() == -1) { CacheTopology cacheTopology = stateTransferManager.getCacheTopology(); // Before the topology is set in STM/StateConsumer the topology in DistributionManager is 0 int topologyId = cacheTopology == null ? 0 : cacheTopology.getTopologyId(); if (trace) getLog().tracef("Setting command topology to %d", topologyId); command.setTopologyId(topologyId); } } protected Object retryWhenDone(CompletableFuture future, int topologyId, InvocationContext ctx, T command, InvocationFinallyFunction callback) { if (future.isDone()) { getLog().tracef("Retrying command %s for topology %d", command, topologyId); return invokeNextAndHandle(ctx, command, callback); } else { CancellableRetry cancellableRetry = new CancellableRetry<>(command, topologyId); // We have to use handleAsync and rethrow the exception in the handler, rather than // thenComposeAsync(), because if `future` completes with an exception we want to continue in remoteExecutor CompletableFuture retryFuture = future.handleAsync(cancellableRetry, remoteExecutor); cancellableRetry.setRetryFuture(retryFuture); // We want to time out the current command future, not the main topology-waiting future, // but the command future can take longer time to finish. ScheduledFuture timeoutFuture = timeoutExecutor.schedule(cancellableRetry, transactionDataTimeout, TimeUnit.MILLISECONDS); cancellableRetry.setTimeoutFuture(timeoutFuture); return makeStage(asyncInvokeNext(ctx, command, retryFuture)).andHandle(ctx, command, callback); } } @Override public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable { return handleReadCommand(ctx, command); } @Override public Object visitGetCacheEntryCommand(InvocationContext ctx, GetCacheEntryCommand command) throws Throwable { return handleReadCommand(ctx, command); } @Override public Object visitGetAllCommand(InvocationContext ctx, GetAllCommand command) throws Throwable { return handleReadCommand(ctx, command); } protected Object handleReadCommand( InvocationContext ctx, C command) { updateTopologyId(command); return invokeNextAndHandle(ctx, command, handleReadCommandReturn); } private Object handleReadCommandReturn(InvocationContext rCtx, VisitableCommand rCommand, Object rv, Throwable t) throws Throwable { if (t == null) return rv; Throwable ce = t; while (ce instanceof RemoteException) { ce = ce.getCause(); } TopologyAffectedCommand cmd = (TopologyAffectedCommand) rCommand; final CacheTopology cacheTopology = stateTransferManager.getCacheTopology(); int currentTopologyId = cacheTopology == null ? -1 : cacheTopology.getTopologyId(); int requestedTopologyId = currentTopologyId; if (ce instanceof SuspectException) { if (trace) getLog().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())) { throw new IllegalStateException("Command was not sent with SYNCHRONOUS_IGNORE_LEAVERS?"); } } else if (ce instanceof OutdatedTopologyException) { logRetry(currentTopologyId, cmd); // In scattered cache, when we have contacted the primary owner in current topology and this does respond // with UnsureResponse we don't know about any other read owners; we need to wait for the next topology if (cacheConfiguration.clustering().cacheMode().isScattered()) { OutdatedTopologyException ote = (OutdatedTopologyException) ce; if (ote.requestedTopologyId >= 0) { requestedTopologyId = Math.max(currentTopologyId, ote.requestedTopologyId); } else { // OTEs without requested topology are a result of UnsureResponse/CNFR as OTE based on old command // topology would be prone to race between SDI and STI requestedTopologyId = Math.max(currentTopologyId, cmd.getTopologyId() + 1); } } } else if (ce instanceof AllOwnersLostException) { if (trace) getLog().tracef("All owners for command %s have been lost.", cmd); // In scattered cache it might be common to lose the single owner, we need to retry. We will find out that // we can return null only after the next topology is installed. If partition handling is enabled we decide // only based on the availability status. // In other cache modes, during partition the exception is already handled in PartitionHandlingInterceptor, // and if the handling is not enabled, we can't but return null. if (cacheConfiguration.clustering().cacheMode().isScattered()) { requestedTopologyId = Math.max(currentTopologyId, cmd.getTopologyId() + 1); } else { return rCommand.acceptVisitor(rCtx, LostDataVisitor.INSTANCE); } } else { throw t; } // We can get OTE even if current topology information is sufficient: // 1. A has topology in phase READ_ALL_WRITE_ALL, sends message to both old owner B and new C // 2. C has old topology with READ_OLD_WRITE_ALL, so it responds with UnsureResponse // 3. C updates topology to READ_ALL_WRITE_ALL, B updates to READ_NEW_WRITE_ALL // 4. B receives the read, but it already can't read: responds with UnsureResponse // 5. A receives two unsure responses and throws OTE // However, now we are sure that we can immediately retry the request, because C must have updated its topology cmd.setTopologyId(requestedTopologyId); ((FlagAffectedCommand) cmd).addFlags(FlagBitSets.COMMAND_RETRY); if (requestedTopologyId == currentTopologyId) { return invokeNextAndHandle(rCtx, rCommand, handleReadCommandReturn); } else { return makeStage(asyncInvokeNext(rCtx, rCommand, stateTransferLock.transactionDataFuture(requestedTopologyId))) .andHandle(rCtx, rCommand, handleReadCommandReturn); } } protected 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 Object visitReadOnlyKeyCommand(InvocationContext ctx, ReadOnlyKeyCommand command) throws Throwable { return handleReadCommand(ctx, command); } @Override public Object visitReadOnlyManyCommand(InvocationContext ctx, ReadOnlyManyCommand command) throws Throwable { return handleReadCommand(ctx, command); } protected abstract Log getLog(); private static class CancellableRetry implements BiFunction, Runnable { private static final AtomicReferenceFieldUpdater cancellableRetryUpdater = AtomicReferenceFieldUpdater.newUpdater(CancellableRetry.class, Throwable.class, "cancelled"); private static final AtomicReferenceFieldUpdater timeoutFutureUpdater = AtomicReferenceFieldUpdater.newUpdater(CancellableRetry.class, Object.class, "timeoutFuture"); private static final Log log = LogFactory.getLog(CancellableRetry.class); private static final Throwable DUMMY = new Throwable("Command is retried"); // should not be ever thrown private final T command; private final int topologyId; private volatile Throwable cancelled = null; // retryFuture is not volatile because it is used only in the timeout handler = run() // and that is scheduled after retryFuture is set private CompletableFuture retryFuture; // ScheduledFuture does not have any dummy implementations, so we'll use plain Object as the field @SuppressWarnings("unused") private volatile Object timeoutFuture; CancellableRetry(T command, int topologyId) { this.command = command; this.topologyId = topologyId; } /** * This is called when the topology future completes (successfully or exceptionally) */ @Override public Void apply(Void nil, Throwable throwable) { if (!timeoutFutureUpdater.compareAndSet(this, null, DUMMY)) { ((ScheduledFuture) timeoutFuture).cancel(false); } if (throwable != null) { throw CompletableFutures.asCompletionException(throwable); } if (!cancellableRetryUpdater.compareAndSet(this, null, DUMMY)) { log.tracef("Not retrying command %s as it has been cancelled.", command); throw CompletableFutures.asCompletionException(cancelled); } log.tracef("Retrying command %s for topology %d", command, topologyId); return null; } /** * This is called when the timeout elapses. */ @Override public void run() { TimeoutException timeoutException = new TimeoutException("Timed out waiting for topology " + topologyId); if (cancellableRetryUpdater.compareAndSet(this, null, timeoutException)) { retryFuture.completeExceptionally(timeoutException); } } void setRetryFuture(CompletableFuture retryFuture) { this.retryFuture = retryFuture; } void setTimeoutFuture(ScheduledFuture timeoutFuture) { if (!timeoutFutureUpdater.compareAndSet(this, null, timeoutFuture)) { timeoutFuture.cancel(false); } } } // We don't need to implement GetAllCommand or ReadManyCommand here because these don't throw AllOwnersLostException protected static class LostDataVisitor extends AbstractVisitor { public static final LostDataVisitor INSTANCE = new LostDataVisitor(); @Override public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable { return null; } @Override public Object visitGetCacheEntryCommand(InvocationContext ctx, GetCacheEntryCommand command) throws Throwable { return null; } @Override public Object visitReadOnlyKeyCommand(InvocationContext ctx, ReadOnlyKeyCommand command) throws Throwable { return command.performOnLostData(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy