package org.infinispan.interceptors.impl; import static org.infinispan.commons.util.Util.toStr; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.infinispan.commands.AbstractVisitor; import org.infinispan.commands.DataCommand; import org.infinispan.commands.FlagAffectedCommand; import org.infinispan.commands.VisitableCommand; 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.functional.ReadWriteManyCommand; import org.infinispan.commands.functional.ReadWriteManyEntriesCommand; import org.infinispan.commands.functional.TxReadOnlyKeyCommand; import org.infinispan.commands.functional.TxReadOnlyManyCommand; import org.infinispan.commands.functional.WriteOnlyKeyCommand; import org.infinispan.commands.functional.WriteOnlyKeyValueCommand; import org.infinispan.commands.functional.WriteOnlyManyCommand; import org.infinispan.commands.functional.WriteOnlyManyEntriesCommand; import org.infinispan.commands.read.AbstractDataCommand; 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.commands.tx.CommitCommand; import org.infinispan.commands.tx.PrepareCommand; import org.infinispan.commands.write.AbstractDataWriteCommand; import org.infinispan.commands.write.ApplyDeltaCommand; import org.infinispan.commands.write.ClearCommand; import org.infinispan.commands.write.DataWriteCommand; 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.configuration.cache.Configurations; import org.infinispan.container.DataContainer; import org.infinispan.container.EntryFactory; import org.infinispan.container.entries.CacheEntry; import org.infinispan.container.entries.MVCCEntry; import org.infinispan.container.versioning.EntryVersion; import org.infinispan.container.versioning.VersionGenerator; import org.infinispan.context.Flag; import org.infinispan.context.InvocationContext; import org.infinispan.context.SingleKeyNonTxInvocationContext; import org.infinispan.context.impl.FlagBitSets; import org.infinispan.context.impl.TxInvocationContext; import org.infinispan.distribution.DistributionManager; import org.infinispan.distribution.group.impl.GroupFilter; import org.infinispan.distribution.group.impl.GroupManager; import org.infinispan.factories.annotations.Inject; import org.infinispan.factories.annotations.Start; import org.infinispan.filter.CollectionKeyFilter; import org.infinispan.filter.CompositeKeyFilter; import org.infinispan.filter.KeyFilter; import org.infinispan.interceptors.DDAsyncInterceptor; import org.infinispan.interceptors.InvocationFinallyAction; import org.infinispan.interceptors.InvocationStage; import org.infinispan.interceptors.InvocationSuccessAction; import org.infinispan.interceptors.locking.ClusteringDependentLogic; import org.infinispan.metadata.Metadata; import org.infinispan.notifications.cachelistener.CacheNotifier; import org.infinispan.remoting.responses.Response; import org.infinispan.statetransfer.OutdatedTopologyException; import org.infinispan.statetransfer.StateConsumer; import org.infinispan.statetransfer.StateTransferLock; import org.infinispan.statetransfer.StateTransferManager; import org.infinispan.util.concurrent.IsolationLevel; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import org.infinispan.xsite.statetransfer.XSiteStateConsumer; /** * Interceptor in charge with wrapping entries and add them in caller's context. * * @see EntryFactory for overview of entry wrapping. * * @author Mircea Markus * @author Pedro Ruivo * @since 9.0 */ public class EntryWrappingInterceptor extends DDAsyncInterceptor { private EntryFactory entryFactory; private DataContainer<Object, Object> dataContainer; protected ClusteringDependentLogic cdl; private VersionGenerator versionGenerator; private DistributionManager distributionManager; private final EntryWrappingVisitor entryWrappingVisitor = new EntryWrappingVisitor(); private boolean isInvalidation; private boolean isSync; private StateConsumer stateConsumer; // optional private StateTransferLock stateTransferLock; private XSiteStateConsumer xSiteStateConsumer; private GroupManager groupManager; private CacheNotifier notifier; private StateTransferManager stateTransferManager; private boolean useRepeatableRead; private boolean isVersioned; private static final Log log = LogFactory.getLog(EntryWrappingInterceptor.class); private static final boolean trace = log.isTraceEnabled(); private static final long EVICT_FLAGS_BITSET = FlagBitSets.SKIP_OWNERSHIP_CHECK | FlagBitSets.CACHE_MODE_LOCAL; private boolean totalOrder; private final InvocationSuccessAction dataReadReturnHandler = (rCtx, rCommand, rv) -> { AbstractDataCommand dataCommand = (AbstractDataCommand) rCommand; if (rCtx.isInTxScope() && useRepeatableRead) { // The entry must be in the context CacheEntry cacheEntry = rCtx.lookupEntry(dataCommand.getKey()); cacheEntry.setSkipLookup(true); if (isVersioned && ((MVCCEntry) cacheEntry).isRead()) { addVersionRead((TxInvocationContext) rCtx, cacheEntry, dataCommand.getKey()); } } // Entry visit notifications used to happen in the CallInterceptor // We do it at the end to avoid adding another try/finally block around the notifications if (rv != null && !(rv instanceof Response)) { Object value = dataCommand instanceof GetCacheEntryCommand ? ((CacheEntry) rv).getValue() : rv; notifier.notifyCacheEntryVisited(dataCommand.getKey(), value, true, rCtx, dataCommand); notifier.notifyCacheEntryVisited(dataCommand.getKey(), value, false, rCtx, dataCommand); } }; private final InvocationSuccessAction commitEntriesSuccessHandler = (rCtx, rCommand, rv) -> commitContextEntries(rCtx, null, null); private final InvocationFinallyAction commitEntriesFinallyHandler = (rCtx, rCommand, rv, t) -> commitContextEntries(rCtx, null, null); protected Log getLog() { return log; } @Inject public void init(EntryFactory entryFactory, DataContainer<Object, Object> dataContainer, ClusteringDependentLogic cdl, StateConsumer stateConsumer, StateTransferLock stateTransferLock, XSiteStateConsumer xSiteStateConsumer, GroupManager groupManager, CacheNotifier notifier, StateTransferManager stateTransferManager, VersionGenerator versionGenerator, DistributionManager distributionManager) { this.entryFactory = entryFactory; this.dataContainer = dataContainer; this.cdl = cdl; this.stateConsumer = stateConsumer; this.stateTransferLock = stateTransferLock; this.xSiteStateConsumer = xSiteStateConsumer; this.groupManager = groupManager; this.notifier = notifier; this.stateTransferManager = stateTransferManager; this.versionGenerator = versionGenerator; this.distributionManager = distributionManager; } @Start public void start() { isInvalidation = cacheConfiguration.clustering().cacheMode().isInvalidation(); isSync = cacheConfiguration.clustering().cacheMode().isSynchronous(); // isolation level makes no sense without transactions useRepeatableRead = cacheConfiguration.transaction().transactionMode().isTransactional() && cacheConfiguration.locking().isolationLevel() == IsolationLevel.REPEATABLE_READ; isVersioned = Configurations.isTxVersioned(cacheConfiguration); totalOrder = cacheConfiguration.transaction().transactionProtocol().isTotalOrder(); } private boolean ignoreOwnership(FlagAffectedCommand command) { return stateTransferManager == null || command.hasAnyFlag(FlagBitSets.CACHE_MODE_LOCAL | FlagBitSets.SKIP_OWNERSHIP_CHECK); } private boolean canRead(Object key) { return distributionManager.getCacheTopology().isReadOwner(key); } @Override public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable { wrapEntriesForPrepare(ctx, command); if (!shouldCommitDuringPrepare(command, ctx)) { return invokeNext(ctx, command); } return invokeNextThenAccept(ctx, command, commitEntriesSuccessHandler); } @Override public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable { return invokeNextAndFinally(ctx, command, commitEntriesFinallyHandler); } @Override public final Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable { return visitDataReadCommand(ctx, command); } @Override public final Object visitGetCacheEntryCommand(InvocationContext ctx, GetCacheEntryCommand command) throws Throwable { return visitDataReadCommand(ctx, command); } private Object visitDataReadCommand(InvocationContext ctx, AbstractDataCommand command) { final Object key = command.getKey(); entryFactory.wrapEntryForReading(ctx, key, ignoreOwnership(command) || canRead(key)); return invokeNextThenAccept(ctx, command, dataReadReturnHandler); } @Override public Object visitGetAllCommand(InvocationContext ctx, GetAllCommand command) throws Throwable { boolean ignoreOwnership = ignoreOwnership(command); for (Object key : command.getKeys()) { entryFactory.wrapEntryForReading(ctx, key, ignoreOwnership || canRead(key)); } return invokeNextAndFinally(ctx, command, (rCtx, rCommand, rv, t) -> { GetAllCommand getAllCommand = (GetAllCommand) rCommand; if (useRepeatableRead) { for (Object key : getAllCommand.getKeys()) { rCtx.lookupEntry(key).setSkipLookup(true); } } // Entry visit notifications used to happen in the CallInterceptor // instanceof check excludes the case when the command returns UnsuccessfulResponse if (t == null && rv instanceof Map) { log.tracef("Notifying getAll? %s; result %s", !command.hasAnyFlag(FlagBitSets.SKIP_LISTENER_NOTIFICATION), rv); Map<Object, Object> map = (Map<Object, Object>) rv; // TODO: it would be nice to know if a listener was registered for this and // not do the full iteration if there was no visitor listener registered if (!command.hasAnyFlag(FlagBitSets.SKIP_LISTENER_NOTIFICATION)) { for (Map.Entry<Object, Object> entry : map.entrySet()) { Object value = entry.getValue(); if (value != null) { value = command.isReturnEntries() ? ((CacheEntry) value).getValue() : entry.getValue(); notifier.notifyCacheEntryVisited(entry.getKey(), value, true, rCtx, getAllCommand); notifier.notifyCacheEntryVisited(entry.getKey(), value, false, rCtx, getAllCommand); } } } } }); } @Override public final Object visitInvalidateCommand(InvocationContext ctx, InvalidateCommand command) throws Throwable { if (command.getKeys() != null) { for (Object key : command.getKeys()) { // TODO: move this to distribution interceptors? // we need to try to wrap the entry to get it removed // for the removal itself, wrapping null would suffice, but listeners need previous value entryFactory.wrapEntryForWriting(ctx, key, true, false); } } return setSkipRemoteGetsAndInvokeNextForManyEntriesCommand(ctx, command); } @Override public final Object visitClearCommand(InvocationContext ctx, ClearCommand command) throws Throwable { return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> { // If we are committing a ClearCommand now then no keys should be written by state transfer from // now on until current rebalance ends. if (stateConsumer != null) { stateConsumer.stopApplyingState(); } if (xSiteStateConsumer != null) { xSiteStateConsumer.endStateTransfer(null); } if (!rCtx.isInTxScope()) { ClearCommand clearCommand = (ClearCommand) rCommand; applyChanges(rCtx, clearCommand, null); } if (trace) log.tracef("The return value is %s", rv); }); } @Override public Object visitInvalidateL1Command(InvocationContext ctx, InvalidateL1Command command) throws Throwable { for (Object key : command.getKeys()) { // TODO: move to distribution interceptors? // we need to try to wrap the entry to get it removed // for the removal itself, wrapping null would suffice, but listeners need previous value entryFactory.wrapEntryForWriting(ctx, key, false, false); if (trace) log.tracef("Entry to be removed: %s", toStr(key)); } return setSkipRemoteGetsAndInvokeNextForManyEntriesCommand(ctx, command); } @Override public final Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { wrapEntryIfNeeded(ctx, command); return setSkipRemoteGetsAndInvokeNextForDataCommand(ctx, command, command.getMetadata()); } private void wrapEntryIfNeeded(InvocationContext ctx, AbstractDataWriteCommand command) throws Throwable { if (command.hasAnyFlag(FlagBitSets.COMMAND_RETRY)) { removeFromContextOnRetry(ctx, command.getKey()); } entryFactory.wrapEntryForWriting(ctx, command.getKey(), ignoreOwnership(command) || canRead(command.getKey()), command.loadType() != VisitableCommand.LoadType.DONT_LOAD); } private void removeFromContextOnRetry(InvocationContext ctx, Object key) { // When originator is a backup and it becomes primary (and we retry the command), the context already // contains the value before the command started to be executed. However, another modification could happen // after this node became an owner, so we have to force a reload. // With repeatable reads, we cannot just remove the entry from context; instead of we will rely // on the write skew check to do the reload & comparison in the end. // With pessimistic locking, there's no WSC but as we have the entry locked, there shouldn't be any // modification concurrent to the retried operation, therefore we don't have to deal with this problem. if (useRepeatableRead) { MVCCEntry entry = (MVCCEntry) ctx.lookupEntry(key); if (trace) { log.tracef("This is a retry - resetting previous value in entry ", entry); } entry.resetCurrentValue(); } else { if (trace) { log.tracef("This is a retry - removing looked up entry " + ctx.lookupEntry(key)); } ctx.removeLookedUpEntry(key); } } private void removeFromContextOnRetry(InvocationContext ctx, Collection<?> keys) { if (useRepeatableRead) { for (Object key : keys) { MVCCEntry entry = (MVCCEntry) ctx.lookupEntry(key); if (trace) { log.tracef("This is a retry - resetting previous value in entry ", entry); } entry.resetCurrentValue(); } } else { for (Object key : keys) { if (trace) { log.tracef("This is a retry - removing looked up entry " + ctx.lookupEntry(key)); } ctx.removeLookedUpEntry(key); } } } @Override public Object visitApplyDeltaCommand(InvocationContext ctx, ApplyDeltaCommand command) throws Throwable { if (command.hasAnyFlag(FlagBitSets.COMMAND_RETRY)) { removeFromContextOnRetry(ctx, command.getKey()); } entryFactory.wrapEntryForDelta(ctx, command.getKey(), command.getDelta(), ignoreOwnership(command) || canRead(command.getKey())); return invokeNext(ctx, command); } @Override public final Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable { wrapEntryIfNeeded(ctx, command); return setSkipRemoteGetsAndInvokeNextForDataCommand(ctx, command, null); } @Override public final Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable { if (command.hasAnyFlag(FlagBitSets.COMMAND_RETRY)) { removeFromContextOnRetry(ctx, command.getKey()); } // When retrying, we might still need to perform the command even if the previous value was removed entryFactory.wrapEntryForWriting(ctx, command.getKey(), ignoreOwnership(command) || canRead(command.getKey()), command.loadType() != VisitableCommand.LoadType.DONT_LOAD); return setSkipRemoteGetsAndInvokeNextForDataCommand(ctx, command, command.getMetadata()); } @Override public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable { boolean ignoreOwnership = ignoreOwnership(command); if (command.hasAnyFlag(FlagBitSets.COMMAND_RETRY)) { removeFromContextOnRetry(ctx, command.getAffectedKeys()); } for (Object key : command.getMap().keySet()) { // as listeners may need the value, we'll load the previous value entryFactory.wrapEntryForWriting(ctx, key, ignoreOwnership || canRead(key), command.loadType() != VisitableCommand.LoadType.DONT_LOAD); } return setSkipRemoteGetsAndInvokeNextForManyEntriesCommand(ctx, command); } @Override public Object visitEvictCommand(InvocationContext ctx, EvictCommand command) throws Throwable { command.setFlagsBitSet(EVICT_FLAGS_BITSET); //to force the wrapping return visitRemoveCommand(ctx, command); } @Override public Object visitGetKeysInGroupCommand(final InvocationContext ctx, GetKeysInGroupCommand command) throws Throwable { final String groupName = command.getGroupName(); if (command.isGroupOwner()) { final KeyFilter<Object> keyFilter = new CompositeKeyFilter<>(new GroupFilter<>(groupName, groupManager), new CollectionKeyFilter<>(ctx.getLookedUpEntries().keySet())); dataContainer.executeTask(keyFilter, (o, internalCacheEntry) -> { synchronized (ctx) { //the process can be made in multiple threads, so we need to synchronize in the context. entryFactory.wrapExternalEntry(ctx, internalCacheEntry.getKey(), internalCacheEntry, true, false); } }); } // We don't make sure that all read entries have skipLookup here, since EntryFactory does that // for those we have really read, and there shouldn't be any null-read entries. if (ctx.isInTxScope() && useRepeatableRead) { return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> { TxInvocationContext txCtx = (TxInvocationContext) rCtx; for (Map.Entry<Object, CacheEntry> keyEntry : txCtx.getLookedUpEntries().entrySet()) { CacheEntry cacheEntry = keyEntry.getValue(); cacheEntry.setSkipLookup(true); if (isVersioned && ((MVCCEntry) cacheEntry).isRead()) { addVersionRead(txCtx, cacheEntry, keyEntry.getKey()); } } }); } else { return invokeNext(ctx, command); } } @Override public Object visitReadOnlyKeyCommand(InvocationContext ctx, ReadOnlyKeyCommand command) throws Throwable { if (command instanceof TxReadOnlyKeyCommand) { // TxReadOnlyKeyCommand may apply some mutations on the entry in context so we need to always wrap it entryFactory.wrapEntryForWriting(ctx, command.getKey(), ignoreOwnership(command) || canRead(command.getKey()), true); } else { entryFactory.wrapEntryForReading(ctx, command.getKey(), ignoreOwnership(command) || canRead(command.getKey())); } // Repeatable reads are not achievable with functional commands, as we don't store the value locally // and we don't "fix" it on the remote node; therefore, the value will be able to change and identity read // could return different values in the same transaction. // (Note: at this point TX mode is not implemented for functional commands anyway). return invokeNext(ctx, command); } @Override public Object visitReadOnlyManyCommand(InvocationContext ctx, ReadOnlyManyCommand command) throws Throwable { boolean ignoreOwnership = ignoreOwnership(command); if (command instanceof TxReadOnlyManyCommand) { // TxReadOnlyManyCommand may apply some mutations on the entry in context so we need to always wrap it for (Object key : command.getKeys()) { entryFactory.wrapEntryForWriting(ctx, key, ignoreOwnership(command) || canRead(key), true); } } else { for (Object key : command.getKeys()) { entryFactory.wrapEntryForReading(ctx, key, ignoreOwnership || canRead(key)); } } // Repeatable reads are not achievable with functional commands, see visitReadOnlyKeyCommand return invokeNext(ctx, command); } @Override public Object visitWriteOnlyKeyCommand(InvocationContext ctx, WriteOnlyKeyCommand command) throws Throwable { wrapEntryIfNeeded(ctx, command); return setSkipRemoteGetsAndInvokeNextForDataCommand(ctx, command, null); } @Override public Object visitReadWriteKeyValueCommand(InvocationContext ctx, ReadWriteKeyValueCommand command) throws Throwable { wrapEntryIfNeeded(ctx, command); return setSkipRemoteGetsAndInvokeNextForDataCommand(ctx, command, null); } @Override public Object visitReadWriteKeyCommand(InvocationContext ctx, ReadWriteKeyCommand command) throws Throwable { wrapEntryIfNeeded(ctx, command); return setSkipRemoteGetsAndInvokeNextForDataCommand(ctx, command, null); } @Override public Object visitWriteOnlyManyEntriesCommand(InvocationContext ctx, WriteOnlyManyEntriesCommand command) throws Throwable { if (command.hasAnyFlag(FlagBitSets.COMMAND_RETRY)) { removeFromContextOnRetry(ctx, command.getAffectedKeys()); } boolean ignoreOwnership = ignoreOwnership(command); for (Object key : command.getEntries().keySet()) { //the put map never reads the keys entryFactory.wrapEntryForWriting(ctx, key, ignoreOwnership || canRead(key), false); } return setSkipRemoteGetsAndInvokeNextForManyEntriesCommand(ctx, command); } @Override public Object visitWriteOnlyManyCommand(InvocationContext ctx, WriteOnlyManyCommand command) throws Throwable { if (command.hasAnyFlag(FlagBitSets.COMMAND_RETRY)) { removeFromContextOnRetry(ctx, command.getAffectedKeys()); } boolean ignoreOwnership = ignoreOwnership(command); for (Object key : command.getAffectedKeys()) { entryFactory.wrapEntryForWriting(ctx, key, ignoreOwnership || canRead(key), false); } return setSkipRemoteGetsAndInvokeNextForManyEntriesCommand(ctx, command); } @Override public Object visitWriteOnlyKeyValueCommand(InvocationContext ctx, WriteOnlyKeyValueCommand command) throws Throwable { wrapEntryIfNeeded(ctx, command); return setSkipRemoteGetsAndInvokeNextForDataCommand(ctx, command, null); } @Override public Object visitReadWriteManyCommand(InvocationContext ctx, ReadWriteManyCommand command) throws Throwable { if (command.hasAnyFlag(FlagBitSets.COMMAND_RETRY)) { removeFromContextOnRetry(ctx, command.getAffectedKeys()); } boolean ignoreOwnership = ignoreOwnership(command); for (Object key : command.getAffectedKeys()) { entryFactory.wrapEntryForWriting(ctx, key, ignoreOwnership || canRead(key), true); } return setSkipRemoteGetsAndInvokeNextForManyEntriesCommand(ctx, command); } @Override public Object visitReadWriteManyEntriesCommand(InvocationContext ctx, ReadWriteManyEntriesCommand command) throws Throwable { if (command.hasAnyFlag(FlagBitSets.COMMAND_RETRY)) { removeFromContextOnRetry(ctx, command.getAffectedKeys()); } boolean ignoreOwnership = ignoreOwnership(command); for (Object key : command.getAffectedKeys()) { entryFactory.wrapEntryForWriting(ctx, key, ignoreOwnership || canRead(key), true); } return setSkipRemoteGetsAndInvokeNextForManyEntriesCommand(ctx, command); } private Flag extractStateTransferFlag(InvocationContext ctx, FlagAffectedCommand command) { if (command == null) { //commit command return ctx instanceof TxInvocationContext ? ((TxInvocationContext) ctx).getCacheTransaction().getStateTransferFlag() : null; } else { if (command.hasAnyFlag(FlagBitSets.PUT_FOR_STATE_TRANSFER)) { return Flag.PUT_FOR_STATE_TRANSFER; } else if (command.hasAnyFlag(FlagBitSets.PUT_FOR_X_SITE_STATE_TRANSFER)) { return Flag.PUT_FOR_X_SITE_STATE_TRANSFER; } } return null; } protected final void commitContextEntries(InvocationContext ctx, FlagAffectedCommand command, Metadata metadata) { final Flag stateTransferFlag = extractStateTransferFlag(ctx, command); if (ctx instanceof SingleKeyNonTxInvocationContext) { SingleKeyNonTxInvocationContext singleKeyCtx = (SingleKeyNonTxInvocationContext) ctx; commitEntryIfNeeded(ctx, command, singleKeyCtx.getCacheEntry(), stateTransferFlag, metadata); } else { Set<Map.Entry<Object, CacheEntry>> entries = ctx.getLookedUpEntries().entrySet(); Iterator<Map.Entry<Object, CacheEntry>> it = entries.iterator(); final Log log = getLog(); while (it.hasNext()) { Map.Entry<Object, CacheEntry> e = it.next(); CacheEntry entry = e.getValue(); if (!commitEntryIfNeeded(ctx, command, entry, stateTransferFlag, metadata)) { if (trace) { if (entry == null) log.tracef("Entry for key %s is null : not calling commitUpdate", toStr(e.getKey())); else log.tracef("Entry for key %s is not changed(%s): not calling commitUpdate", toStr(e.getKey()), entry); } } } } } protected void commitContextEntry(CacheEntry entry, InvocationContext ctx, FlagAffectedCommand command, Metadata metadata, Flag stateTransferFlag, boolean l1Invalidation) { cdl.commitEntry(entry, metadata, command, ctx, stateTransferFlag, l1Invalidation); } private void applyChanges(InvocationContext ctx, WriteCommand command, Metadata metadata) { stateTransferLock.acquireSharedTopologyLock(); try { // We only retry non-tx write commands if (!isInvalidation) { // Can't perform the check during preload or if the cache isn't clustered boolean syncRpc = isSync && !command.hasAnyFlag(FlagBitSets.FORCE_ASYNCHRONOUS) || command.hasAnyFlag(FlagBitSets.FORCE_SYNCHRONOUS); if (command.isSuccessful() && stateConsumer != null && stateConsumer.getCacheTopology() != null) { int commandTopologyId = command.getTopologyId(); int currentTopologyId = stateConsumer.getCacheTopology().getTopologyId(); // TotalOrderStateTransferInterceptor doesn't set the topology id for PFERs. if (syncRpc && currentTopologyId != commandTopologyId && commandTopologyId != -1) { // If we were the originator of a data command which we didn't own the key at the time means it // was already committed, so there is no need to throw the OutdatedTopologyException // This will happen if we submit a command to the primary owner and it responds and then a topology // change happens before we get here if (!ctx.isOriginLocal() || !(command instanceof DataCommand) || ctx.hasLockedKey(((DataCommand)command).getKey())) { if (trace) log.tracef("Cache topology changed while the command was executing: expected %d, got %d", commandTopologyId, currentTopologyId); // This shouldn't be necessary, as we'll have a fresh command instance when retrying command.setValueMatcher(command.getValueMatcher().matcherForRetry()); throw new OutdatedTopologyException("Cache topology changed while the command was executing: expected " + commandTopologyId + ", got " + currentTopologyId); } } } } commitContextEntries(ctx, command, metadata); } finally { stateTransferLock.releaseSharedTopologyLock(); } } /** * Locks the value for the keys accessed by the command to avoid being override from a remote get. */ private Object setSkipRemoteGetsAndInvokeNextForManyEntriesCommand(InvocationContext ctx, WriteCommand command) { return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> { WriteCommand writeCommand = (WriteCommand) rCommand; if (!rCtx.isInTxScope()) { applyChanges(rCtx, writeCommand, null); return; } if (trace) log.tracef("The return value is %s", toStr(rv)); if (useRepeatableRead) { boolean addVersionRead = isVersioned && writeCommand.loadType() != VisitableCommand.LoadType.DONT_LOAD; TxInvocationContext txCtx = (TxInvocationContext) rCtx; for (Object key : writeCommand.getAffectedKeys()) { CacheEntry cacheEntry = rCtx.lookupEntry(key); if (cacheEntry != null) { cacheEntry.setSkipLookup(true); if (addVersionRead && ((MVCCEntry) cacheEntry).isRead()) { addVersionRead(txCtx, cacheEntry, key); } ((MVCCEntry) cacheEntry).updatePreviousValue(); } } } }); } private void addVersionRead(TxInvocationContext rCtx, CacheEntry cacheEntry, Object key) { EntryVersion version; if (cacheEntry != null && cacheEntry.getMetadata() != null) { version = cacheEntry.getMetadata().version(); if (trace) log.tracef("Adding version read %s for key %s", version, key); } else { version = versionGenerator.nonExistingVersion(); if (trace) log.tracef("Adding non-existent version read for key %s", key); } rCtx.getCacheTransaction().addVersionRead(key, version); } /** * Locks the value for the keys accessed by the command to avoid being override from a remote get. */ private Object setSkipRemoteGetsAndInvokeNextForDataCommand(InvocationContext ctx, DataWriteCommand command, Metadata metadata) { return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> { DataWriteCommand dataWriteCommand = (DataWriteCommand) rCommand; if (!rCtx.isInTxScope()) { applyChanges(rCtx, dataWriteCommand, metadata); return; } if (trace) log.tracef("The return value is %s", rv); if (useRepeatableRead) { CacheEntry cacheEntry = rCtx.lookupEntry(dataWriteCommand.getKey()); // The entry is not in context when the command's execution type does not contain origin if (cacheEntry != null) { cacheEntry.setSkipLookup(true); if (isVersioned && dataWriteCommand.loadType() != VisitableCommand.LoadType.DONT_LOAD && ((MVCCEntry) cacheEntry).isRead()) { addVersionRead((TxInvocationContext) rCtx, cacheEntry, dataWriteCommand.getKey()); } ((MVCCEntry) cacheEntry).updatePreviousValue(); } } }); } // This visitor replays the entry wrapping during remote prepare. // The command is passed down the stack even if its keys do not belong to this node according // to writeCH, it's a role of TxDistributionInterceptor to ignore such command. // The entry is wrapped only if it's available for reading, otherwise it has to be wrapped // in TxDistributionInterceptor. When the entry is not wrapped, the value is not loaded in // CacheLoaderInterceptor, therefore passing the command down the stack causes only minimal overhead. private final class EntryWrappingVisitor extends AbstractVisitor { @Override public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable { return handleWriteManyCommand(ctx, command); } @Override public Object visitInvalidateCommand(InvocationContext ctx, InvalidateCommand command) throws Throwable { return handleWriteManyCommand(ctx, command); } @Override public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public Object visitApplyDeltaCommand(InvocationContext ctx, ApplyDeltaCommand command) throws Throwable { entryFactory.wrapEntryForDelta(ctx, command.getKey(), command.getDelta(), ignoreOwnership(command) || canRead(command.getKey())); return invokeNext(ctx, command); } @Override public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public Object visitWriteOnlyKeyCommand(InvocationContext ctx, WriteOnlyKeyCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public Object visitReadWriteKeyValueCommand(InvocationContext ctx, ReadWriteKeyValueCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public Object visitReadWriteKeyCommand(InvocationContext ctx, ReadWriteKeyCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public Object visitWriteOnlyManyEntriesCommand(InvocationContext ctx, WriteOnlyManyEntriesCommand command) throws Throwable { return handleWriteManyCommand(ctx, command); } @Override public Object visitWriteOnlyKeyValueCommand(InvocationContext ctx, WriteOnlyKeyValueCommand command) throws Throwable { return handleWriteCommand(ctx, command); } @Override public Object visitWriteOnlyManyCommand(InvocationContext ctx, WriteOnlyManyCommand command) throws Throwable { return handleWriteManyCommand(ctx, command); } @Override public Object visitReadWriteManyCommand(InvocationContext ctx, ReadWriteManyCommand command) throws Throwable { return handleWriteManyCommand(ctx, command); } @Override public Object visitReadWriteManyEntriesCommand(InvocationContext ctx, ReadWriteManyEntriesCommand command) throws Throwable { return handleWriteManyCommand(ctx, command); } private Object handleWriteCommand(InvocationContext ctx, DataWriteCommand command) throws Throwable { entryFactory.wrapEntryForWriting(ctx, command.getKey(), ignoreOwnership(command) || canRead(command.getKey()), command.loadType() != VisitableCommand.LoadType.DONT_LOAD); return invokeNext(ctx, command); } private Object handleWriteManyCommand(InvocationContext ctx, WriteCommand command) { boolean ignoreOwnership = ignoreOwnership(command); for (Object key : command.getAffectedKeys()) { entryFactory.wrapEntryForWriting(ctx, key, ignoreOwnership || canRead(key), command.loadType() != VisitableCommand.LoadType.DONT_LOAD); } return invokeNext(ctx, command); } } private boolean commitEntryIfNeeded(final InvocationContext ctx, final FlagAffectedCommand command, final CacheEntry entry, final Flag stateTransferFlag, final Metadata metadata) { if (entry == null) { return false; } final boolean l1Invalidation = command instanceof InvalidateL1Command; if (entry.isChanged()) { if (trace) log.tracef("About to commit entry %s", entry); commitContextEntry(entry, ctx, command, metadata, stateTransferFlag, l1Invalidation); return true; } return false; } /** * total order condition: only commits when it is remote context and the prepare has the flag 1PC set * * @param command the prepare command * @param ctx the invocation context * @return true if the modification should be committed, false otherwise */ protected boolean shouldCommitDuringPrepare(PrepareCommand command, TxInvocationContext ctx) { return totalOrder ? command.isOnePhaseCommit() && (!ctx.isOriginLocal() || !command.hasModifications()) : command.isOnePhaseCommit(); } protected final void wrapEntriesForPrepare(TxInvocationContext ctx, PrepareCommand command) throws Throwable { if (!ctx.isOriginLocal() || command.isReplayEntryWrapping()) { for (WriteCommand c : command.getModifications()) { c.setTopologyId(command.getTopologyId()); // TODO: we need async invocation since the interceptors may do remote get InvocationStage visitorStage = makeStage(c.acceptVisitor(ctx, entryWrappingVisitor)); // Wait for the sub-command to finish. If there was an exception, rethrow it. visitorStage.get(); if (c.hasAnyFlag(FlagBitSets.PUT_FOR_X_SITE_STATE_TRANSFER)) { ctx.getCacheTransaction().setStateTransferFlag(Flag.PUT_FOR_X_SITE_STATE_TRANSFER); } } } } }