protected void updateStateOnNodesLeaving(Collection<Address> leavers) {
    Set<GlobalTransaction> toKill = new HashSet<GlobalTransaction>();
    for (GlobalTransaction gt : remoteTransactions.keySet()) {
      if (leavers.contains(gt.getAddress())) toKill.add(gt);
    }

    if (toKill.isEmpty())
      log.tracef(
          "No global transactions pertain to originator(s) %s who have left the cluster.", leavers);
    else
      log.tracef(
          "%s global transactions pertain to leavers list %s and need to be killed",
          toKill.size(), leavers);

    for (GlobalTransaction gtx : toKill) {
      log.tracef("Killing remote transaction originating on leaver %s", gtx);
      RollbackCommand rc = new RollbackCommand(cacheName, gtx);
      rc.init(invoker, icc, TransactionTable.this);
      try {
        rc.perform(null);
        log.tracef("Rollback of transaction %s complete.", gtx);
      } catch (Throwable e) {
        log.unableToRollbackGlobalTx(gtx, e);
      }
    }

    log.trace("Completed cleaning transactions originating on leavers");
  }
    public CompletedTransactionStatus getTransactionStatus(GlobalTransaction gtx) {
      CompletedTransactionInfo completedTx = completedTransactions.get(gtx);
      if (completedTx != null) {
        return completedTx.successful
            ? CompletedTransactionStatus.COMMITTED
            : CompletedTransactionStatus.ABORTED;
      }

      // Transaction ids are allocated in sequence, so any transaction with a smaller id must have
      // been started
      // before a transaction that was already removed from the completed transactions map because
      // it was too old.
      // We assume that the transaction was either committed, or it was rolled back (e.g. because
      // the prepare
      // RPC timed out.
      // Note: We must check the id *after* verifying that the tx doesn't exist in the map.
      if (gtx.getId() > globalMaxPrunedTxId) return CompletedTransactionStatus.NOT_COMPLETED;
      Long nodeMaxPrunedTxId = nodeMaxPrunedTxIds.get(gtx.getAddress());
      if (nodeMaxPrunedTxId == null) {
        // We haven't removed any transaction for this node
        return CompletedTransactionStatus.NOT_COMPLETED;
      } else if (gtx.getId() > nodeMaxPrunedTxId) {
        // We haven't removed this particular transaction yet
        return CompletedTransactionStatus.NOT_COMPLETED;
      } else {
        // We already removed the status of this transaction from the completed transactions map
        return CompletedTransactionStatus.EXPIRED;
      }
    }
  private CompletableFuture<Void> visitSecondPhaseCommand(
      TxInvocationContext ctx,
      TransactionBoundaryCommand command,
      boolean commit,
      ExtendedStatistic duration,
      ExtendedStatistic counter)
      throws Throwable {
    GlobalTransaction globalTransaction = command.getGlobalTransaction();
    if (trace) {
      log.tracef(
          "Visit 2nd phase command %s. Is it local? %s. Transaction is %s",
          command, ctx.isOriginLocal(), globalTransaction.globalId());
    }
    long start = timeService.time();
    return ctx.onReturn(
        (rCtx, rCommand, rv, throwable) -> {
          if (throwable != null) {
            throw throwable;
          }

          long end = timeService.time();
          updateTime(duration, counter, start, end, globalTransaction, rCtx.isOriginLocal());
          cacheStatisticManager.setTransactionOutcome(
              commit, globalTransaction, rCtx.isOriginLocal());
          cacheStatisticManager.terminateTransaction(globalTransaction, true, true);
          return null;
        });
  }
    /** @see #markTransactionCompleted(GlobalTransaction, boolean) */
    public boolean isTransactionCompleted(GlobalTransaction gtx) {
      if (completedTransactions.containsKey(gtx)) return true;

      // Transaction ids are allocated in sequence, so any transaction with a smaller id must have
      // been started
      // before a transaction that was already removed from the completed transactions map because
      // it was too old.
      // We assume that the transaction was either committed, or it was rolled back (e.g. because
      // the prepare
      // RPC timed out.
      // Note: We must check the id *after* verifying that the tx doesn't exist in the map.
      if (gtx.getId() > globalMaxPrunedTxId) return false;
      Long nodeMaxPrunedTxId = nodeMaxPrunedTxIds.get(gtx.getAddress());
      return nodeMaxPrunedTxId != null && gtx.getId() <= nodeMaxPrunedTxId;
    }
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    EventImpl<?, ?> event = (EventImpl<?, ?>) o;

    if (originLocal != event.originLocal) return false;
    if (pre != event.pre) return false;
    if (transactionSuccessful != event.transactionSuccessful) return false;
    if (cache != null ? !cache.equals(event.cache) : event.cache != null) return false;
    if (key != null ? !key.equals(event.key) : event.key != null) return false;
    if (transaction != null ? !transaction.equals(event.transaction) : event.transaction != null)
      return false;
    if (type != event.type) return false;
    if (value != null ? !value.equals(event.value) : event.value != null) return false;
    if (!Util.safeEquals(consistentHashAtStart, event.consistentHashAtStart)) return false;
    if (!Util.safeEquals(consistentHashAtEnd, event.consistentHashAtEnd)) return false;
    if (!Util.safeEquals(unionConsistentHash, event.unionConsistentHash)) return false;
    if (newTopologyId != event.newTopologyId) return false;
    if (created != event.created) return false;
    if (oldValue != null ? !oldValue.equals(event.oldValue) : event.oldValue != null) return false;

    return true;
  }
  public void cleanupStaleTransactions(CacheTopology cacheTopology) {
    int topologyId = cacheTopology.getTopologyId();
    List<Address> members = cacheTopology.getMembers();

    // We only care about transactions originated before this topology update
    if (getMinTopologyId() >= topologyId) return;

    log.tracef(
        "Checking for transactions originated on leavers. Current members are %s, remote transactions: %d",
        members, remoteTransactions.size());
    Set<GlobalTransaction> toKill = new HashSet<GlobalTransaction>();
    for (Map.Entry<GlobalTransaction, RemoteTransaction> e : remoteTransactions.entrySet()) {
      GlobalTransaction gt = e.getKey();
      RemoteTransaction remoteTx = e.getValue();
      log.tracef("Checking transaction %s", gt);
      // The topology id check is needed for joiners
      if (remoteTx.getTopologyId() < topologyId && !members.contains(gt.getAddress())) {
        toKill.add(gt);
      }
    }

    if (toKill.isEmpty()) {
      log.tracef("No global transactions pertain to originator(s) who have left the cluster.");
    } else {
      log.tracef("%s global transactions pertain to leavers and need to be killed", toKill.size());
    }

    for (GlobalTransaction gtx : toKill) {
      log.tracef("Killing remote transaction originating on leaver %s", gtx);
      RollbackCommand rc = new RollbackCommand(cacheName, gtx);
      rc.init(invoker, icc, TransactionTable.this);
      try {
        rc.perform(null);
        log.tracef("Rollback of transaction %s complete.", gtx);
      } catch (Throwable e) {
        log.unableToRollbackGlobalTx(gtx, e);
      }
    }

    log.tracef(
        "Completed cleaning transactions originating on leavers. Remote transactions remaining: %d",
        remoteTransactions.size());
  }
  public void cleanupLeaverTransactions(List<Address> members) {
    // Can happen if the cache is non-transactional
    if (remoteTransactions == null) return;

    if (trace)
      log.tracef(
          "Checking for transactions originated on leavers. Current cache members are %s, remote transactions: %d",
          members, remoteTransactions.size());
    HashSet<Address> membersSet = new HashSet<>(members);
    List<GlobalTransaction> toKill = new ArrayList<>();
    for (Map.Entry<GlobalTransaction, RemoteTransaction> e : remoteTransactions.entrySet()) {
      GlobalTransaction gt = e.getKey();
      if (trace) log.tracef("Checking transaction %s", gt);
      if (!membersSet.contains(gt.getAddress())) {
        toKill.add(gt);
      }
    }

    if (toKill.isEmpty()) {
      if (trace)
        log.tracef("No remote transactions pertain to originator(s) who have left the cluster.");
    } else {
      log.debugf("The originating node left the cluster for %d remote transactions", toKill.size());
      for (GlobalTransaction gtx : toKill) {
        if (partitionHandlingManager.canRollbackTransactionAfterOriginatorLeave(gtx)) {
          log.debugf(
              "Rolling back transaction %s because originator %s left the cluster",
              gtx, gtx.getAddress());
          killTransaction(gtx);
        } else {
          log.debugf(
              "Keeping transaction %s after the originator %s left the cluster.",
              gtx, gtx.getAddress());
        }
      }

      if (trace)
        log.tracef(
            "Completed cleaning transactions originating on leavers. Remote transactions remaining: %d",
            remoteTransactions.size());
    }
  }
  @Override
  public CompletableFuture<Void> visitPrepareCommand(
      TxInvocationContext ctx, PrepareCommand command) throws Throwable {
    GlobalTransaction globalTransaction = command.getGlobalTransaction();
    if (trace) {
      log.tracef(
          "Visit Prepare command %s. Is it local?. Transaction is %s",
          command, ctx.isOriginLocal(), globalTransaction.globalId());
    }
    initStatsIfNecessary(ctx);
    cacheStatisticManager.onPrepareCommand(globalTransaction, ctx.isOriginLocal());
    if (command.hasModifications()) {
      cacheStatisticManager.markAsWriteTransaction(globalTransaction, ctx.isOriginLocal());
    }

    long start = timeService.time();
    return ctx.onReturn(
        (rCtx, rCommand, rv, throwable) -> {
          if (throwable != null) {
            processWriteException(rCtx, globalTransaction, throwable);
          } else {
            long end = timeService.time();
            updateTime(
                PREPARE_EXECUTION_TIME,
                NUM_PREPARE_COMMAND,
                start,
                end,
                globalTransaction,
                rCtx.isOriginLocal());
          }

          if (((PrepareCommand) rCommand).isOnePhaseCommit()) {
            boolean local = rCtx.isOriginLocal();
            boolean success = throwable == null;
            cacheStatisticManager.setTransactionOutcome(
                success, globalTransaction, rCtx.isOriginLocal());
            cacheStatisticManager.terminateTransaction(globalTransaction, local, !local);
          }
          return null;
        });
  }
 @Override
 public int hashCode() {
   int result = (pre ? 1 : 0);
   result = 31 * result + (cache != null ? cache.hashCode() : 0);
   result = 31 * result + (key != null ? key.hashCode() : 0);
   result = 31 * result + (transaction != null ? transaction.hashCode() : 0);
   result = 31 * result + (originLocal ? 1 : 0);
   result = 31 * result + (transactionSuccessful ? 1 : 0);
   result = 31 * result + (type != null ? type.hashCode() : 0);
   result = 31 * result + (value != null ? value.hashCode() : 0);
   result = 31 * result + (membersAtStart != null ? membersAtStart.hashCode() : 0);
   result = 31 * result + (membersAtEnd != null ? membersAtEnd.hashCode() : 0);
   result = 31 * result + (consistentHashAtStart != null ? consistentHashAtStart.hashCode() : 0);
   result = 31 * result + (consistentHashAtEnd != null ? consistentHashAtEnd.hashCode() : 0);
   result = 31 * result + ((int) newViewId);
   return result;
 }
 @Override
 public int hashCode() {
   int result = (pre ? 1 : 0);
   result = 31 * result + (cache != null ? cache.hashCode() : 0);
   result = 31 * result + (key != null ? key.hashCode() : 0);
   result = 31 * result + (transaction != null ? transaction.hashCode() : 0);
   result = 31 * result + (originLocal ? 1 : 0);
   result = 31 * result + (transactionSuccessful ? 1 : 0);
   result = 31 * result + (type != null ? type.hashCode() : 0);
   result = 31 * result + (value != null ? value.hashCode() : 0);
   result = 31 * result + (consistentHashAtStart != null ? consistentHashAtStart.hashCode() : 0);
   result = 31 * result + (consistentHashAtEnd != null ? consistentHashAtEnd.hashCode() : 0);
   result = 31 * result + (unionConsistentHash != null ? unionConsistentHash.hashCode() : 0);
   result = 31 * result + newTopologyId;
   result = 31 * result + (created ? 1 : 0);
   result = 31 * result + (oldValue != null ? oldValue.hashCode() : 0);
   return result;
 }