private Object handleTopologyAffectedCommand(
      InvocationContext ctx, VisitableCommand command, Address origin, boolean sync)
      throws Throwable {
    log.tracef("handleTopologyAffectedCommand for command %s", command);

    if (isLocalOnly(ctx, command)) {
      return invokeNextInterceptor(ctx, command);
    }
    updateTopologyId((TopologyAffectedCommand) command);

    // TODO we may need to skip local invocation for read/write/tx commands if the command is too
    // old and none of its keys are local
    Object localResult = invokeNextInterceptor(ctx, command);

    boolean isNonTransactionalWrite = !ctx.isInTxScope() && command instanceof WriteCommand;
    boolean isTransactionalAndNotRolledBack = false;
    if (ctx.isInTxScope()) {
      isTransactionalAndNotRolledBack =
          !((TxInvocationContext) ctx).getCacheTransaction().isMarkedForRollback();
    }

    if (isNonTransactionalWrite || isTransactionalAndNotRolledBack) {
      stateTransferManager.forwardCommandIfNeeded(
          ((TopologyAffectedCommand) command), getAffectedKeys(ctx, command), origin, sync);
    }

    return localResult;
  }
 private void updateTopologyId(TopologyAffectedCommand command) throws InterruptedException {
   // 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();
     if (cacheTopology != null) {
       command.setTopologyId(cacheTopology.getTopologyId());
     }
   }
 }
  /**
   * For non-tx write commands, we retry the command locally if the topology changed, instead of
   * forwarding to the new owners like we do for tx commands. But we only retry on the originator,
   * and only if the command doesn't have the {@code CACHE_MODE_LOCAL} flag.
   */
  private Object handleNonTxWriteCommand(InvocationContext ctx, WriteCommand command)
      throws Throwable {
    log.tracef("handleNonTxWriteCommand for command %s", command);

    if (isLocalOnly(ctx, command)) {
      return invokeNextInterceptor(ctx, command);
    }

    updateTopologyId(command);

    // Only catch OutdatedTopologyExceptions on the originator
    if (!ctx.isOriginLocal()) {
      return invokeNextInterceptor(ctx, command);
    }

    int commandTopologyId = command.getTopologyId();
    Object localResult;
    try {
      localResult = invokeNextInterceptor(ctx, command);
      return localResult;
    } catch (CacheException e) {
      Throwable ce = e;
      while (ce instanceof RemoteException) {
        ce = ce.getCause();
      }
      if (!(ce instanceof OutdatedTopologyException)) throw e;

      log.tracef("Retrying command because of topology change: %s", command);
      // 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(stateTransferManager.getCacheTopology().getTopologyId(), commandTopologyId + 1);
      command.setTopologyId(newTopologyId);
      stateTransferLock.waitForTransactionData(
          newTopologyId, transactionDataTimeout, TimeUnit.MILLISECONDS);
      localResult = handleNonTxWriteCommand(ctx, command);
    }

    // We retry the command every time the topology changes, either in
    // NonTxConcurrentDistributionInterceptor or in
    // EntryWrappingInterceptor. So we don't need to forward the command again here (without holding
    // a lock).
    // stateTransferManager.forwardCommandIfNeeded(command, command.getAffectedKeys(),
    // ctx.getOrigin(), false);
    return localResult;
  }