package org.infinispan.interceptors.distribution; import static java.lang.String.format; import static org.infinispan.util.DeltaCompositeKeyUtil.filterDeltaCompositeKeys; import static org.infinispan.util.DeltaCompositeKeyUtil.getAffectedKeysFromContext; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import org.infinispan.commands.FlagAffectedCommand; import org.infinispan.commands.ReplicableCommand; import org.infinispan.commands.TopologyAffectedCommand; import org.infinispan.commands.VisitableCommand; import org.infinispan.commands.control.LockControlCommand; import org.infinispan.commands.functional.FunctionalCommand; import org.infinispan.commands.functional.Mutation; 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.GetAllCommand; 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.tx.VersionedCommitCommand; import org.infinispan.commands.write.AbstractDataWriteCommand; 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.ValueMatcher; import org.infinispan.commands.write.WriteCommand; import org.infinispan.commons.api.functional.EntryView; import org.infinispan.container.entries.CacheEntry; import org.infinispan.container.entries.MVCCEntry; import org.infinispan.container.versioning.EntryVersionsMap; 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.distribution.LocalizedCacheTopology; import org.infinispan.factories.annotations.Inject; import org.infinispan.functional.impl.EntryViews; import org.infinispan.partitionhandling.impl.PartitionHandlingManager; import org.infinispan.remoting.inboundhandler.DeliverOrder; import org.infinispan.remoting.responses.CacheNotFoundResponse; import org.infinispan.remoting.responses.Response; import org.infinispan.remoting.responses.SuccessfulResponse; import org.infinispan.remoting.responses.UnsureResponse; import org.infinispan.remoting.rpc.ResponseMode; import org.infinispan.remoting.rpc.RpcOptions; import org.infinispan.remoting.transport.Address; import org.infinispan.statetransfer.OutdatedTopologyException; import org.infinispan.transaction.impl.LocalTransaction; import org.infinispan.transaction.xa.GlobalTransaction; import org.infinispan.util.concurrent.CompletableFutures; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; /** * Handles the distribution of the transactional caches. * * @author Mircea Markus * @author Dan Berindei */ public class TxDistributionInterceptor extends BaseDistributionInterceptor { private static final Log log = LogFactory.getLog(TxDistributionInterceptor.class); private static final boolean trace = log.isTraceEnabled(); private PartitionHandlingManager partitionHandlingManager; private final TxReadOnlyManyHelper txReadOnlyManyHelper = new TxReadOnlyManyHelper(); private final ReadWriteManyHelper readWriteManyHelper = new ReadWriteManyHelper(); private final ReadWriteManyEntriesHelper readWriteManyEntriesHelper = new ReadWriteManyEntriesHelper(); @Inject public void inject(PartitionHandlingManager partitionHandlingManager) { this.partitionHandlingManager = partitionHandlingManager; } @Override public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable { return handleTxWriteCommand(ctx, command, command.getKey()); } private void updateMatcherForRetry(WriteCommand command) { // The command is already included in PrepareCommand.modifications - when the command is executed on the remote // owners it should not behave conditionally anymore because its success/failure is defined on originator. command.setValueMatcher(command.isSuccessful() ? ValueMatcher.MATCH_ALWAYS : ValueMatcher.MATCH_NEVER); } @Override public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable { return handleTxWriteCommand(ctx, command, command.getKey()); } @Override public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { if (command.hasAnyFlag(FlagBitSets.PUT_FOR_EXTERNAL_READ)) { return handleNonTxWriteCommand(ctx, command); } return handleTxWriteCommand(ctx, command, command.getKey()); } @Override public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable { return handleTxWriteManyEntriesCommand(ctx, command, command.getMap(), (c, entries) -> new PutMapCommand(c).withMap(entries)); } @Override public Object visitLockControlCommand(TxInvocationContext ctx, LockControlCommand command) throws Throwable { if (ctx.isOriginLocal()) { TxInvocationContext<LocalTransaction> localTxCtx = (TxInvocationContext<LocalTransaction>) ctx; //In Pessimistic mode, the delta composite keys were sent to the wrong owner and never locked. Collection<Address> affectedNodes = dm.getCacheTopology().getWriteOwners(filterDeltaCompositeKeys(command.getKeys())); Collection<Address> recipients = isReplicated ? null : affectedNodes; localTxCtx.getCacheTransaction().locksAcquired(affectedNodes); log.tracef("Registered remote locks acquired %s", affectedNodes); RpcOptions rpcOptions = rpcManager.getRpcOptionsBuilder(ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS, DeliverOrder.NONE).build(); CompletableFuture<Map<Address, Response>> remoteInvocation = rpcManager.invokeRemotelyAsync(recipients, command, rpcOptions); return asyncValue(remoteInvocation.thenApply(responses -> { checkTxCommandResponses(responses, command, localTxCtx, localTxCtx.getCacheTransaction().getRemoteLocksAcquired()); return null; })); } return invokeNext(ctx, command); } @Override public Object visitWriteOnlyKeyCommand(InvocationContext ctx, WriteOnlyKeyCommand command) throws Throwable { return handleTxFunctionalCommand(ctx, command); } @Override public Object visitReadWriteKeyValueCommand(InvocationContext ctx, ReadWriteKeyValueCommand command) throws Throwable { return handleTxFunctionalCommand(ctx, command); } @Override public Object visitReadWriteKeyCommand(InvocationContext ctx, ReadWriteKeyCommand command) throws Throwable { return handleTxFunctionalCommand(ctx, command); } @Override public Object visitWriteOnlyManyEntriesCommand(InvocationContext ctx, WriteOnlyManyEntriesCommand command) throws Throwable { return handleTxWriteManyEntriesCommand(ctx, command, command.getEntries(), (c, entries) -> new WriteOnlyManyEntriesCommand(c).withEntries(entries)); } @Override public Object visitWriteOnlyKeyValueCommand(InvocationContext ctx, WriteOnlyKeyValueCommand command) throws Throwable { return handleTxFunctionalCommand(ctx, command); } @Override public Object visitWriteOnlyManyCommand(InvocationContext ctx, WriteOnlyManyCommand command) throws Throwable { return handleTxWriteManyCommand(ctx, command, command.getAffectedKeys(), (c, keys) -> new WriteOnlyManyCommand(c).withKeys(keys)); } @Override public Object visitReadWriteManyCommand(InvocationContext ctx, ReadWriteManyCommand command) throws Throwable { if (ctx.isOriginLocal()) { return handleFunctionalReadManyCommand(ctx, command, readWriteManyHelper); } else { return handleTxWriteManyCommand(ctx, command, command.getAffectedKeys(), readWriteManyHelper::copyForLocal); } } @Override public Object visitReadWriteManyEntriesCommand(InvocationContext ctx, ReadWriteManyEntriesCommand command) throws Throwable { if (ctx.isOriginLocal()) { return handleFunctionalReadManyCommand(ctx, command, readWriteManyEntriesHelper); } else { return handleTxWriteManyEntriesCommand(ctx, command, command.getEntries(), (c, entries) -> new ReadWriteManyEntriesCommand<>(c).withEntries(entries)); } } // ---- TX boundary commands @Override public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable { return handleSecondPhaseCommand(ctx, command); } @Override public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable { if (!ctx.isOriginLocal()) { return invokeNext(ctx, command); } return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> { if (!shouldInvokeRemoteTxCommand(ctx)) { return null; } TxInvocationContext<LocalTransaction> localTxCtx = (TxInvocationContext<LocalTransaction>) rCtx; Collection<Address> affectedNodes = dm.getCacheTopology().getWriteOwners(getAffectedKeysFromContext(localTxCtx)); Collection<Address> recipients = isReplicated ? null : affectedNodes; CompletableFuture<Object> remotePrepare = prepareOnAffectedNodes(localTxCtx, (PrepareCommand) rCommand, recipients); return asyncValue(remotePrepare.thenApply(o -> { localTxCtx.getCacheTransaction().locksAcquired(affectedNodes); return o; })); }); } protected CompletableFuture<Object> prepareOnAffectedNodes(TxInvocationContext<?> ctx, PrepareCommand command, Collection<Address> recipients) { try { // this method will return immediately if we're the only member (because exclude_self=true) CompletableFuture<Map<Address, Response>> remoteInvocation = rpcManager.invokeRemotelyAsync(recipients, command, createRpcOptions()); return remoteInvocation.handle((responses, t) -> { transactionRemotelyPrepared(ctx); CompletableFutures.rethrowException(t); checkTxCommandResponses(responses, command, (LocalTxInvocationContext) ctx, recipients); return null; }); } catch (Throwable t) { transactionRemotelyPrepared(ctx); throw t; } } @Override public Object visitRollbackCommand(TxInvocationContext ctx, RollbackCommand command) throws Throwable { return handleSecondPhaseCommand(ctx, command); } private Object handleSecondPhaseCommand(TxInvocationContext ctx, TransactionBoundaryCommand command) { if (shouldInvokeRemoteTxCommand(ctx)) { Collection<Address> recipients = getCommitNodes(ctx); CompletableFuture<Map<Address, Response>> remoteInvocation = rpcManager.invokeRemotelyAsync(recipients, command, createRpcOptions()); return asyncValue(remoteInvocation.thenApply(responses -> { checkTxCommandResponses(responses, command, ctx, recipients); return null; })); } return invokeNext(ctx, command); } private Collection<Address> getCommitNodes(TxInvocationContext ctx) { LocalTransaction localTx = (LocalTransaction) ctx.getCacheTransaction(); LocalizedCacheTopology cacheTopology = dm.getCacheTopology(); Collection<Address> affectedNodes = isReplicated ? null : cacheTopology.getWriteOwners(getAffectedKeysFromContext(ctx)); return localTx.getCommitNodes(affectedNodes, cacheTopology); } protected void checkTxCommandResponses(Map<Address, Response> responseMap, TransactionBoundaryCommand command, TxInvocationContext<LocalTransaction> context, Collection<Address> recipients) { OutdatedTopologyException outdatedTopologyException = null; for (Map.Entry<Address, Response> e : responseMap.entrySet()) { Address recipient = e.getKey(); Response response = e.getValue(); if (response == CacheNotFoundResponse.INSTANCE) { // No need to retry if the missing node wasn't a member when the command started. if (command.getTopologyId() == stateTransferManager.getCacheTopology().getTopologyId() && !rpcManager.getMembers().contains(recipient)) { if (trace) log.tracef("Ignoring response from node not targeted %s", recipient); } else { if (checkCacheNotFoundResponseInPartitionHandling(command, context, recipients)) { if (trace) log.tracef("Cache not running on node %s, or the node is missing. It will be handled by the PartitionHandlingManager", recipient); return; } else { if (trace) log.tracef("Cache not running on node %s, or the node is missing", recipient); //noinspection ThrowableInstanceNeverThrown outdatedTopologyException = new OutdatedTopologyException(format("Cache not running on node %s, or the node is missing", recipient)); } } } else if (response == UnsureResponse.INSTANCE) { if (trace) log.tracef("Node %s has a newer topology id", recipient); //noinspection ThrowableInstanceNeverThrown outdatedTopologyException = new OutdatedTopologyException(format("Node %s has a newer topology id", recipient)); } } if (outdatedTopologyException != null) { throw outdatedTopologyException; } } private boolean checkCacheNotFoundResponseInPartitionHandling(TransactionBoundaryCommand command, TxInvocationContext<LocalTransaction> context, Collection<Address> recipients) { final GlobalTransaction globalTransaction = command.getGlobalTransaction(); final Collection<Object> lockedKeys = context.getLockedKeys(); if (command instanceof RollbackCommand) { return partitionHandlingManager.addPartialRollbackTransaction(globalTransaction, recipients, lockedKeys); } else if (command instanceof PrepareCommand) { if (((PrepareCommand) command).isOnePhaseCommit()) { return partitionHandlingManager.addPartialCommit1PCTransaction(globalTransaction, recipients, lockedKeys, Arrays.asList(((PrepareCommand) command).getModifications())); } } else if (command instanceof CommitCommand) { EntryVersionsMap newVersion = null; if (command instanceof VersionedCommitCommand) { newVersion = ((VersionedCommitCommand) command).getUpdatedVersions(); } return partitionHandlingManager.addPartialCommit2PCTransaction(globalTransaction, recipients, lockedKeys, newVersion); } return false; } /** * If we are within one transaction we won't do any replication as replication would only be performed at commit * time. If the operation didn't originate locally we won't do any replication either. */ private Object handleTxWriteCommand(InvocationContext ctx, AbstractDataWriteCommand command, Object key) throws Throwable { try { if (!ctx.isOriginLocal() && !dm.getCacheTopology().isWriteOwner(command.getKey())) { return null; } CacheEntry entry = ctx.lookupEntry(command.getKey()); if (entry == null) { if (isLocalModeForced(command) || command.hasAnyFlag(FlagBitSets.SKIP_REMOTE_LOOKUP) || !needsPreviousValue(ctx, command)) { // in transactional mode, we always need the entry wrapped entryFactory.wrapExternalEntry(ctx, key, null, false, true); } else { // we need to retrieve the value locally regardless of load type; in transactional mode all operations // execute on origin // Also, operations that need value on backup [delta write] need to do the remote lookup even on non-origin Object result = asyncInvokeNext(ctx, command, remoteGet(ctx, command, command.getKey(), true)); return makeStage(result) .andFinally(ctx, command, (rCtx, rCommand, rv, t) -> updateMatcherForRetry((WriteCommand) rCommand)); } } // already wrapped, we can continue return invokeNextAndFinally(ctx, command, (rCtx, rCommand, rv, t) -> updateMatcherForRetry((WriteCommand) rCommand)); } catch (Throwable t) { updateMatcherForRetry(command); throw t; } } protected <C extends TopologyAffectedCommand & FlagAffectedCommand, K, V> Object handleTxWriteManyEntriesCommand(InvocationContext ctx, C command, Map<K, V> entries, BiFunction<C, Map<K, V>, C> copyCommand) { Map<K, V> filtered = new HashMap<>(entries.size()); Collection<CompletableFuture<?>> remoteGets = null; for (Map.Entry<K, V> e : entries.entrySet()) { K key = e.getKey(); if (ctx.isOriginLocal() || dm.getCacheTopology().isWriteOwner(key)) { if (ctx.lookupEntry(key) == null) { if (command.hasAnyFlag(FlagBitSets.CACHE_MODE_LOCAL) || command.hasAnyFlag( FlagBitSets.SKIP_REMOTE_LOOKUP) || !needsPreviousValue(ctx, command)) { entryFactory.wrapExternalEntry(ctx, key, null, false, true); } else { if (remoteGets == null) { remoteGets = new ArrayList<>(); } remoteGets.add(remoteGet(ctx, command, key, true)); } } filtered.put(key, e.getValue()); } } C narrowed = copyCommand.apply(command, filtered); if (remoteGets != null) { return asyncInvokeNext(ctx, narrowed, CompletableFuture.allOf(remoteGets.toArray(new CompletableFuture[remoteGets.size()]))); } else { return invokeNext(ctx, narrowed); } } protected <C extends VisitableCommand & FlagAffectedCommand, K> Object handleTxWriteManyCommand( InvocationContext ctx, C command, Collection<K> keys, BiFunction<C, List<K>, C> copyCommand) { List<K> filtered = new ArrayList<>(keys.size()); for (K key : keys) { if (ctx.isOriginLocal() || dm.getCacheTopology().isWriteOwner(key)) { if (ctx.lookupEntry(key) == null) { entryFactory.wrapExternalEntry(ctx, key, null, false, true); } filtered.add(key); } } return invokeNext(ctx, copyCommand.apply(command, filtered)); } public <C extends AbstractDataWriteCommand & FunctionalCommand> Object handleTxFunctionalCommand(InvocationContext ctx, C command) { Object key = command.getKey(); if (ctx.isOriginLocal()) { CacheEntry entry = ctx.lookupEntry(key); if (entry == null) { if (isLocalModeForced(command) || command.hasAnyFlag(FlagBitSets.SKIP_REMOTE_LOOKUP) || command.loadType() == VisitableCommand.LoadType.DONT_LOAD) { entryFactory.wrapExternalEntry(ctx, key, null, false, true); return invokeNext(ctx, command); } else { LocalizedCacheTopology cacheTopology = checkTopologyId(command); Collection<Address> owners = cacheTopology.getDistribution(key).readOwners(); List<Mutation> mutationsOnKey = getMutationsOnKey((TxInvocationContext) ctx, key); mutationsOnKey.add(command.toMutation(key)); TxReadOnlyKeyCommand remoteRead = new TxReadOnlyKeyCommand(key, mutationsOnKey); return asyncValue(rpcManager.invokeRemotelyAsync(owners, remoteRead, getStaggeredOptions(owners.size())).thenApply(responses -> { for (Response r : responses.values()) { if (r instanceof SuccessfulResponse) { SuccessfulResponse response = (SuccessfulResponse) r; Object responseValue = response.getResponseValue(); return unwrapFunctionalResultOnOrigin(ctx, command.getKey(), responseValue); } } throw handleMissingSuccessfulResponse(responses); })); } } // It's possible that this is not an owner, but the entry was loaded from L1 - let the command run return invokeNext(ctx, command); } else { if (!dm.getCacheTopology().isWriteOwner(key)) { return null; } CacheEntry entry = ctx.lookupEntry(key); if (entry == null) { return UnsureResponse.INSTANCE; } return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> wrapFunctionalResultOnNonOriginOnReturn(rv, entry)); } } private boolean needsPreviousValue(InvocationContext ctx, FlagAffectedCommand command) { switch (command.loadType()) { case DONT_LOAD: return false; case PRIMARY: // In transactional cache, the result is determined on origin return ctx.isOriginLocal(); case OWNER: return true; default: throw new IllegalStateException(); } } @Override public Object visitReadOnlyManyCommand(InvocationContext ctx, ReadOnlyManyCommand command) throws Throwable { return handleFunctionalReadManyCommand(ctx, command, txReadOnlyManyHelper); } @Override protected ReadOnlyKeyCommand remoteReadOnlyCommand(InvocationContext ctx, ReadOnlyKeyCommand command) { if (!ctx.isInTxScope()) { return command; } return new TxReadOnlyKeyCommand(command, getMutationsOnKey((TxInvocationContext) ctx, command.getKey())); } @Override protected <C extends FlagAffectedCommand & TopologyAffectedCommand> CompletableFuture<Void> remoteGet(InvocationContext ctx, C command, Object key, boolean isWrite) { CompletableFuture<Void> cf = super.remoteGet(ctx, command, key, isWrite); // If the remoteGet is executed on non-origin node, the mutations list already contains all modifications // and we are just trying to execute all of them from EntryWrappingIntercepot$EntryWrappingVisitor if (!ctx.isOriginLocal() || !ctx.isInTxScope()) { return cf; } List<Mutation> mutationsOnKey = getMutationsOnKey((TxInvocationContext) ctx, key); if (mutationsOnKey.isEmpty()) { return cf; } return cf.thenRun(() -> { entryFactory.wrapEntryForWriting(ctx, key, false, true); MVCCEntry cacheEntry = (MVCCEntry) ctx.lookupEntry(key); EntryView.ReadWriteEntryView readWriteEntryView = EntryViews.readWrite(cacheEntry); for (Mutation mutation : mutationsOnKey) { mutation.apply(readWriteEntryView); cacheEntry.updatePreviousValue(); } }); } @Override protected CompletableFuture<Void> remoteGetAll(InvocationContext ctx, GetAllCommand command, Map<Address, List<Object>> requestedKeys) { CompletableFuture<Void> cf = super.remoteGetAll(ctx, command, requestedKeys); if (!ctx.isInTxScope()) { return cf; } for (List<Object> keys : requestedKeys.values()) { List<List<Mutation>> mutations = getMutations(ctx, keys); if (mutations == null || mutations.isEmpty()) { continue; } cf = cf.thenRun(() -> { Iterator<Object> keysIterator = keys.iterator(); Iterator<List<Mutation>> mutationsIterator = mutations.iterator(); for (; keysIterator.hasNext() && mutationsIterator.hasNext(); ) { Object key = keysIterator.next(); entryFactory.wrapEntryForWriting(ctx, key, false, true); MVCCEntry cacheEntry = (MVCCEntry) ctx.lookupEntry(key); EntryView.ReadWriteEntryView readWriteEntryView = EntryViews.readWrite(cacheEntry); for (Mutation mutation : mutationsIterator.next()) { mutation.apply(readWriteEntryView); cacheEntry.updatePreviousValue(); } } assert !keysIterator.hasNext(); assert !mutationsIterator.hasNext(); }); } return cf; } protected RpcOptions createRpcOptions() { return rpcManager.getRpcOptionsBuilder(ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS, DeliverOrder.NONE).build(); } private static List<Mutation> getMutationsOnKey(TxInvocationContext ctx, Object key) { List<Mutation> mutations = new ArrayList<>(); // We don't use getAllModifications() because this goes remote and local mods should not affect it for (WriteCommand write : ctx.getCacheTransaction().getModifications()) { if (write.getAffectedKeys().contains(key)) { if (write instanceof FunctionalCommand) { mutations.add(((FunctionalCommand) write).toMutation(key)); } else { // Non-functional modification must have retrieved the value into context and we should not do any // remote reads! throw new IllegalStateException("Attempt to remote functional read after non-functional modification! " + "key=" + key + ", modification=" + write); } } } return mutations; } private static List<List<Mutation>> getMutations(InvocationContext ctx, List<Object> keys) { if (!ctx.isInTxScope()) { return null; } TxInvocationContext txCtx = (TxInvocationContext) ctx; List<List<Mutation>> mutations = new ArrayList<>(keys.size()); for (int i = keys.size(); i > 0; --i) mutations.add(Collections.emptyList()); for (WriteCommand write : txCtx.getCacheTransaction().getModifications()) { for (int i = 0; i < keys.size(); ++i) { Object key = keys.get(i); if (write.getAffectedKeys().contains(key)) { if (write instanceof FunctionalCommand) { List<Mutation> list = mutations.get(i); if (list.isEmpty()) { list = new ArrayList<>(); mutations.set(i, list); } list.add(((FunctionalCommand) write).toMutation(key)); } else { // Non-functional modification must have retrieved the value into context and we should not do any // remote reads! throw new IllegalStateException("Attempt to remote functional read after non-functional modification! " + "key=" + key + ", modification=" + write); } } } } return mutations; } private class TxReadOnlyManyHelper extends ReadOnlyManyHelper { @Override public ReplicableCommand copyForRemote(ReadOnlyManyCommand command, List<Object> keys, InvocationContext ctx) { List<List<Mutation>> mutations = getMutations(ctx, keys); if (mutations == null) { return new ReadOnlyManyCommand<>(command).withKeys(keys); } else { return new TxReadOnlyManyCommand(command, mutations).withKeys(keys); } } } private abstract class BaseFunctionalWriteHelper<C extends FunctionalCommand & WriteCommand> implements ReadManyCommandHelper<C> { @Override public Collection<?> keys(C command) { return command.getAffectedKeys(); } @Override public ReplicableCommand copyForRemote(C command, List<Object> keys, InvocationContext ctx) { List<List<Mutation>> mutations = getMutations(ctx, keys); // write command is always executed in transactional scope assert mutations != null; for (int i = 0; i < keys.size(); ++i) { List<Mutation> list = mutations.get(i); Mutation mutation = command.toMutation(keys.get(i)); if (list.isEmpty()) { mutations.set(i, Collections.singletonList(mutation)); } else { list.add(mutation); } } return new TxReadOnlyManyCommand(keys, mutations); } @Override public void applyLocalResult(MergingCompletableFuture allFuture, Object rv) { int pos = 0; for (Object value : ((List) rv)) { allFuture.results[pos++] = value; } } @Override public Object transformResult(Object[] results) { return Arrays.asList(results); } @Override public Object apply(InvocationContext rCtx, VisitableCommand rCommand, Object rv) throws Throwable { return wrapFunctionalManyResultOnNonOrigin(rCtx, ((WriteCommand) rCommand).getAffectedKeys(), ((List) rv).toArray()); } } private class ReadWriteManyHelper extends BaseFunctionalWriteHelper<ReadWriteManyCommand> { @Override public ReadWriteManyCommand copyForLocal(ReadWriteManyCommand command, List<Object> keys) { return new ReadWriteManyCommand(command).withKeys(keys); } } private class ReadWriteManyEntriesHelper extends BaseFunctionalWriteHelper<ReadWriteManyEntriesCommand> { @Override public ReadWriteManyEntriesCommand copyForLocal(ReadWriteManyEntriesCommand command, List<Object> keys) { return new ReadWriteManyEntriesCommand(command).withEntries(filterEntries(command.getEntries(), keys)); } private <K, V> Map<K, V> filterEntries(Map<K, V> originalEntries, List<K> keys) { Map<K, V> entries = new HashMap<>(keys.size()); for (K key : keys) { entries.put(key, originalEntries.get(key)); } return entries; } } }