@Override
  public CompletableFuture<Void> visitReadOnlyKeyCommand(
      InvocationContext ctx, ReadOnlyKeyCommand command) throws Throwable {
    try {
      CacheEntry entry = entryFactory.wrapEntryForReading(ctx, command.getKey(), null);
      // Null entry is often considered to mean that entry is not available
      // locally, but if there's no need to get remote, the read-only
      // function needs to be executed, so force a non-null entry in
      // context with null content
      if (entry == null && cdl.localNodeIsOwner(command.getKey())) {
        entryFactory.wrapEntryForReading(ctx, command.getKey(), NullCacheEntry.getInstance());
      }

      return ctx.shortCircuit(ctx.forkInvocationSync(command));
    } finally {
      // needed because entries might be added in L1
      if (!ctx.isInTxScope()) commitContextEntries(ctx, command, null);
      else {
        CacheEntry entry = ctx.lookupEntry(command.getKey());
        if (entry != null) {
          entry.setSkipLookup(true);
        }
      }
    }
  }
 @Override
 public Object visitApplyDeltaCommand(InvocationContext ctx, ApplyDeltaCommand command)
     throws Throwable {
   if (cdl.localNodeIsOwner(command.getKey())) {
     entryFactory.wrapEntryForDelta(ctx, command.getKey(), command.getDelta());
     ctx.forkInvocationSync(command);
   }
   return null;
 }
  private Object invokeNextAndApplyChanges(
      InvocationContext ctx, FlagAffectedCommand command, Metadata metadata) throws Throwable {
    final Object result = ctx.forkInvocationSync(command);

    if (!ctx.isInTxScope()) {
      stateTransferLock.acquireSharedTopologyLock();
      try {
        // We only retry non-tx write commands
        if (command instanceof WriteCommand) {
          WriteCommand writeCommand = (WriteCommand) command;
          // Can't perform the check during preload or if the cache isn't clustered
          boolean isSync =
              (cacheConfiguration.clustering().cacheMode().isSynchronous()
                      && !command.hasFlag(Flag.FORCE_ASYNCHRONOUS))
                  || command.hasFlag(Flag.FORCE_SYNCHRONOUS);
          if (writeCommand.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 (isSync && 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
                writeCommand.setValueMatcher(writeCommand.getValueMatcher().matcherForRetry());
                throw new OutdatedTopologyException(
                    "Cache topology changed while the command was executing: expected "
                        + commandTopologyId
                        + ", got "
                        + currentTopologyId);
              }
            }
          }
        }

        commitContextEntries(ctx, command, metadata);
      } finally {
        stateTransferLock.releaseSharedTopologyLock();
      }
    }

    if (trace) log.tracef("The return value is %s", toStr(result));
    return result;
  }
 @Override
 public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command)
     throws Throwable {
   if (cdl.localNodeIsOwner(command.getKey())) {
     entryFactory.wrapEntryForWriting(
         ctx, command.getKey(), EntryFactory.Wrap.WRAP_ALL, false, false);
     ctx.forkInvocationSync(command);
   }
   return null;
 }
 @Override
 public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command)
     throws Throwable {
   if (cdl.localNodeIsOwner(command.getKey())) {
     boolean forceWrap = command.getValueMatcher().nonExistentEntryCanMatch();
     EntryFactory.Wrap wrap =
         forceWrap ? EntryFactory.Wrap.WRAP_ALL : EntryFactory.Wrap.WRAP_NON_NULL;
     entryFactory.wrapEntryForWriting(ctx, command.getKey(), wrap, false, false);
     ctx.forkInvocationSync(command);
   }
   return null;
 }
 @Override
 public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command)
     throws Throwable {
   if (cdl.localNodeIsOwner(command.getKey())) {
     // When retrying, we need to perform the command even if the previous value was deleted.
     boolean forceWrap = command.getValueMatcher().nonExistentEntryCanMatch();
     EntryFactory.Wrap wrap =
         forceWrap ? EntryFactory.Wrap.WRAP_ALL : EntryFactory.Wrap.WRAP_NON_NULL;
     entryFactory.wrapEntryForWriting(ctx, command.getKey(), wrap, false, false);
     ctx.forkInvocationSync(command);
   }
   return null;
 }
 @Override
 public Object visitInvalidateCommand(InvocationContext ctx, InvalidateCommand command)
     throws Throwable {
   if (command.getKeys() != null) {
     for (Object key : command.getKeys()) {
       if (cdl.localNodeIsOwner(key)) {
         entryFactory.wrapEntryForWriting(
             ctx, key, EntryFactory.Wrap.WRAP_NON_NULL, false, false);
         ctx.forkInvocationSync(command);
       }
     }
   }
   return null;
 }
 private CompletableFuture<Void> visitDataReadCommand(
     InvocationContext ctx, AbstractDataCommand command) throws Throwable {
   try {
     entryFactory.wrapEntryForReading(ctx, command.getKey(), null);
     return ctx.shortCircuit(ctx.forkInvocationSync(command));
   } finally {
     // needed because entries might be added in L1
     if (!ctx.isInTxScope()) commitContextEntries(ctx, command, null);
     else {
       CacheEntry entry = ctx.lookupEntry(command.getKey());
       if (entry != null) {
         entry.setSkipLookup(true);
       }
     }
   }
 }
 @Override
 public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command)
     throws Throwable {
   Map<Object, Object> newMap = new HashMap<>(4);
   for (Map.Entry<Object, Object> e : command.getMap().entrySet()) {
     Object key = e.getKey();
     if (cdl.localNodeIsOwner(key)) {
       entryFactory.wrapEntryForWriting(ctx, key, EntryFactory.Wrap.WRAP_ALL, true, false);
       newMap.put(key, e.getValue());
     }
   }
   if (newMap.size() > 0) {
     PutMapCommand clonedCommand =
         commandFactory.buildPutMapCommand(
             newMap, command.getMetadata(), command.getFlagsBitSet());
     ctx.forkInvocationSync(clonedCommand);
   }
   return null;
 }
 @Override
 public CompletableFuture<Void> visitReadOnlyManyCommand(
     InvocationContext ctx, ReadOnlyManyCommand command) throws Throwable {
   try {
     for (Object key : command.getKeys()) {
       entryFactory.wrapEntryForReading(ctx, key, null);
     }
     return ctx.shortCircuit(ctx.forkInvocationSync(command));
   } finally {
     if (ctx.isInTxScope()) {
       for (Object key : command.getKeys()) {
         CacheEntry entry = ctx.lookupEntry(key);
         if (entry != null) {
           entry.setSkipLookup(true);
         }
       }
     }
   }
 }