package org.infinispan.interceptors.impl;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import org.infinispan.commands.AbstractVisitor;
import org.infinispan.commands.CommandsFactory;
import org.infinispan.commands.FlagAffectedCommand;
import org.infinispan.commands.ReplicableCommand;
import org.infinispan.commands.control.LockControlCommand;
import org.infinispan.commands.tx.CommitCommand;
import org.infinispan.commands.tx.PrepareCommand;
import org.infinispan.commands.write.ClearCommand;
import org.infinispan.commands.write.InvalidateCommand;
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.commons.util.EnumUtil;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.impl.FlagBitSets;
import org.infinispan.context.impl.LocalTxInvocationContext;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.factories.annotations.Start;
import org.infinispan.jmx.JmxStatisticsExposer;
import org.infinispan.jmx.annotations.DataType;
import org.infinispan.jmx.annotations.MBean;
import org.infinispan.jmx.annotations.ManagedAttribute;
import org.infinispan.jmx.annotations.ManagedOperation;
import org.infinispan.jmx.annotations.MeasurementType;
import org.infinispan.jmx.annotations.Parameter;
import org.infinispan.remoting.responses.Response;
import org.infinispan.remoting.rpc.ResponseMode;
import org.infinispan.remoting.rpc.RpcOptions;
import org.infinispan.remoting.transport.Address;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
/**
* This interceptor acts as a replacement to the replication interceptor when the CacheImpl is configured with
* ClusteredSyncMode as INVALIDATE.
* <p/>
* The idea is that rather than replicating changes to all caches in a cluster when write methods are called, simply
* broadcast an {@link InvalidateCommand} on the remote caches containing all keys modified. This allows the remote
* cache to look up the value in a shared cache loader which would have been updated with the changes.
*
* @author Manik Surtani
* @author Galder ZamarreƱo
* @author Mircea.Markus@jboss.com
* @since 9.0
*/
@MBean(objectName = "Invalidation", description = "Component responsible for invalidating entries on remote" +
" caches when entries are written to locally.")
public class InvalidationInterceptor extends BaseRpcInterceptor implements JmxStatisticsExposer {
private final AtomicLong invalidations = new AtomicLong(0);
private CommandsFactory commandsFactory;
private boolean statisticsEnabled;
private static final Log log = LogFactory.getLog(InvalidationInterceptor.class);
@Override
protected Log getLog() {
return log;
}
@Inject
public void injectDependencies(CommandsFactory commandsFactory) {
this.commandsFactory = commandsFactory;
}
@Start
private void start() {
this.setStatisticsEnabled(cacheConfiguration.jmxStatistics().enabled());
}
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
if (!isPutForExternalRead(command)) {
return handleInvalidate(ctx, command, command.getKey());
}
return invokeNext(ctx, command);
}
@Override
public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable {
return handleInvalidate(ctx, command, command.getKey());
}
@Override
public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable {
return handleInvalidate(ctx, command, command.getKey());
}
@Override
public Object visitClearCommand(InvocationContext ctx, ClearCommand command) throws Throwable {
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
ClearCommand clearCommand = (ClearCommand) rCommand;
if (!isLocalModeForced(clearCommand)) {
// just broadcast the clear command - this is simplest!
if (rCtx.isOriginLocal()) {
CompletableFuture<Map<Address, Response>> remoteInvocation =
rpcManager.invokeRemotelyAsync(null, clearCommand, getBroadcastRpcOptions(defaultSynchronous));
return asyncValue(remoteInvocation.thenApply(responses -> null));
}
}
return rv;
});
}
@Override
public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable {
Object[] keys = command.getMap() == null ? null : command.getMap().keySet().toArray();
return handleInvalidate(ctx, command, keys);
}
@Override
public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable {
if (!command.isOnePhaseCommit()) {
return invokeNext(ctx, command);
}
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
log.tracef("Entering InvalidationInterceptor's prepare phase. Ctx flags are empty");
// fetch the modifications before the transaction is committed (and thus removed from the txTable)
TxInvocationContext txInvocationContext = (TxInvocationContext) rCtx;
if (!shouldInvokeRemoteTxCommand(txInvocationContext)) {
log.tracef("Nothing to invalidate - no modifications in the transaction.");
return rv;
}
if (txInvocationContext.getTransaction() == null)
throw new IllegalStateException("We must have an associated transaction");
PrepareCommand prepareCommand = (PrepareCommand) rCommand;
List<WriteCommand> mods = Arrays.asList(prepareCommand.getModifications());
Collection<Object> remoteKeys = keysToInvalidateForPrepare(mods, txInvocationContext);
if (remoteKeys == null) {
return rv;
}
CompletableFuture<Map<Address, Response>> remoteInvocation =
invalidateAcrossCluster(defaultSynchronous, remoteKeys.toArray(), txInvocationContext);
return asyncValue(remoteInvocation.handle((responses, t) -> {
if (t == null) {
return null;
}
log.unableToRollbackEvictionsDuringPrepare(t);
if (t instanceof RuntimeException)
throw ((RuntimeException) t);
else
throw new RuntimeException("Unable to broadcast invalidation messages", t);
}));
});
}
@Override
public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable {
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
Set<Object> affectedKeys = ctx.getAffectedKeys();
log.tracef("On commit, send invalidate for keys: %s", affectedKeys);
CompletableFuture<Map<Address, Response>> remoteInvocation = null;
try {
remoteInvocation = invalidateAcrossCluster(defaultSynchronous, affectedKeys.toArray(), rCtx);
return asyncValue(remoteInvocation.handle((responses, t) -> {
if (t != null) throw wrapException(t);
return null;
}));
} catch (Throwable t) {
throw wrapException(t);
}
});
}
private RuntimeException wrapException(Throwable t) {
if (t instanceof RuntimeException)
return ((RuntimeException) t);
else
return log.unableToBroadcastInvalidation(t);
}
@Override
public Object visitLockControlCommand(TxInvocationContext ctx, LockControlCommand command)
throws Throwable {
if (!ctx.isOriginLocal()) {
return invokeNext(ctx, command);
}
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
//unlock will happen async as it is a best effort
LockControlCommand lockControlCommand = (LockControlCommand) rCommand;
boolean sync = !lockControlCommand.isUnlock();
((LocalTxInvocationContext) rCtx).remoteLocksAcquired(rpcManager.getTransport().getMembers());
CompletableFuture<Map<Address, Response>> remoteInvocation =
rpcManager.invokeRemotelyAsync(null, lockControlCommand, getBroadcastRpcOptions(sync));
return asyncValue(remoteInvocation.thenApply(responses -> null));
});
}
private Object handleInvalidate(InvocationContext ctx, WriteCommand command, Object... keys)
throws Throwable {
if (ctx.isInTxScope()) {
return invokeNext(ctx, command);
}
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
WriteCommand writeCommand = (WriteCommand) rCommand;
if (writeCommand.isSuccessful()) {
if (keys != null && keys.length != 0) {
if (!isLocalModeForced(writeCommand)) {
CompletableFuture<Map<Address, Response>> remoteInvocation =
invalidateAcrossCluster(isSynchronous(writeCommand), keys, rCtx);
return asyncValue(remoteInvocation.thenApply(responses -> rv));
}
}
}
return rv;
});
}
private Collection<Object> keysToInvalidateForPrepare(List<WriteCommand> modifications,
InvocationContext ctx)
throws Throwable {
// A prepare does not carry flags, so skip checking whether is local or not
if (!ctx.isInTxScope()) return null;
if (modifications.isEmpty()) return null;
InvalidationFilterVisitor filterVisitor = new InvalidationFilterVisitor(modifications.size());
filterVisitor.visitCollection(ctx, modifications);
if (filterVisitor.containsPutForExternalRead) {
log.debug("Modification list contains a putForExternalRead operation. Not invalidating.");
} else if (filterVisitor.containsLocalModeFlag) {
log.debug("Modification list contains a local mode flagged operation. Not invalidating.");
} else {
return filterVisitor.result;
}
return null;
}
private static class InvalidationFilterVisitor extends AbstractVisitor {
Set<Object> result;
boolean containsPutForExternalRead = false;
boolean containsLocalModeFlag = false;
InvalidationFilterVisitor(int maxSetSize) {
result = new HashSet<Object>(maxSetSize);
}
private void processCommand(FlagAffectedCommand command) {
containsLocalModeFlag = containsLocalModeFlag || command.hasAnyFlag(FlagBitSets.CACHE_MODE_LOCAL);
}
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
processCommand(command);
containsPutForExternalRead = containsPutForExternalRead || command.hasAnyFlag(FlagBitSets.PUT_FOR_EXTERNAL_READ);
result.add(command.getKey());
return null;
}
@Override
public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable {
processCommand(command);
result.add(command.getKey());
return null;
}
@Override
public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable {
processCommand(command);
result.addAll(command.getAffectedKeys());
return null;
}
}
private CompletableFuture<Map<Address, Response>> invalidateAcrossCluster(boolean synchronous, Object[] keys,
InvocationContext ctx) throws Throwable {
// increment invalidations counter if statistics maintained
incrementInvalidations();
final InvalidateCommand invalidateCommand = commandsFactory.buildInvalidateCommand(EnumUtil.EMPTY_BIT_SET, keys);
if (log.isDebugEnabled())
log.debug("Cache [" + rpcManager.getAddress() + "] replicating " + invalidateCommand);
ReplicableCommand command = invalidateCommand;
if (ctx.isInTxScope()) {
TxInvocationContext txCtx = (TxInvocationContext) ctx;
// A Prepare command containing the invalidation command in its 'modifications' list is sent to the remote nodes
// so that the invalidation is executed in the same transaction and locks can be acquired and released properly.
// This is 1PC on purpose, as an optimisation, even if the current TX is 2PC.
// If the cache uses 2PC it's possible that the remotes will commit the invalidation and the originator rolls back,
// but this does not impact consistency and the speed benefit is worth it.
command = commandsFactory.buildPrepareCommand(txCtx.getGlobalTransaction(), Collections.singletonList(invalidateCommand), true);
}
return rpcManager.invokeRemotelyAsync(null, command, getBroadcastRpcOptions(synchronous));
}
private RpcOptions getBroadcastRpcOptions(boolean synchronous) {
return rpcManager.getRpcOptionsBuilder(
synchronous ? ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS : ResponseMode.ASYNCHRONOUS).build();
}
private void incrementInvalidations() {
if (statisticsEnabled) invalidations.incrementAndGet();
}
private boolean isPutForExternalRead(FlagAffectedCommand command) {
if (command.hasAnyFlag(FlagBitSets.PUT_FOR_EXTERNAL_READ)) {
log.trace("Put for external read called. Suppressing clustered invalidation.");
return true;
}
return false;
}
@Override
@ManagedOperation(
description = "Resets statistics gathered by this component",
displayName = "Reset statistics"
)
public void resetStatistics() {
invalidations.set(0);
}
@Override
@ManagedAttribute(
displayName = "Statistics enabled",
description = "Enables or disables the gathering of statistics by this component",
dataType = DataType.TRAIT,
writable = true
)
public boolean getStatisticsEnabled() {
return this.statisticsEnabled;
}
@Override
public void setStatisticsEnabled(@Parameter(name = "enabled", description = "Whether statistics should be enabled or disabled (true/false)") boolean enabled) {
this.statisticsEnabled = enabled;
}
@ManagedAttribute(
description = "Number of invalidations",
displayName = "Number of invalidations",
measurementType = MeasurementType.TRENDSUP
)
public long getInvalidations() {
return invalidations.get();
}
}