/** @throws Exception If failed. */
  public void testNotifier() throws Exception {
    String nodeVer = IgniteProperties.get("ignite.version");

    GridUpdateNotifier ntf =
        new GridUpdateNotifier(
            null, nodeVer, TEST_GATEWAY, Collections.<PluginProvider>emptyList(), false);

    ntf.checkForNewVersion(log);

    String ver = ntf.latestVersion();

    // Wait 60 sec for response.
    for (int i = 0; ver == null && i < 600; i++) {
      Thread.sleep(100);

      ver = ntf.latestVersion();
    }

    info("Latest version: " + ver);

    assertNotNull("Ignite latest version has not been detected.", ver);

    byte nodeMaintenance = IgniteProductVersion.fromString(nodeVer).maintenance();

    byte lastMaintenance = IgniteProductVersion.fromString(ver).maintenance();

    assertTrue(
        "Wrong latest version.",
        (nodeMaintenance == 0 && lastMaintenance == 0)
            || (nodeMaintenance > 0 && lastMaintenance > 0));

    ntf.reportStatus(log);
  }
  /** @return {@code True} if need to send finish request for one phase commit transaction. */
  private boolean needFinishOnePhase() {
    if (tx.mappings().empty()) return false;

    boolean finish = tx.txState().hasNearCache(cctx);

    if (finish) {
      GridDistributedTxMapping mapping = tx.mappings().singleMapping();

      if (FINISH_NEAR_ONE_PHASE_SINCE.compareTo(mapping.node().version()) > 0) finish = false;
    }

    return finish;
  }
  /** @throws Exception If failed. */
  public void testFromString() throws Exception {
    IgniteProductVersion ver = IgniteProductVersion.fromString("1.2.3");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("", ver.stage());
    assertEquals(0, ver.revisionTimestamp());
    assertArrayEquals(new byte[20], ver.revisionHash());

    ver = IgniteProductVersion.fromString("1.2.3-0-DEV");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals(0, ver.revisionTimestamp());
    assertArrayEquals(new byte[20], ver.revisionHash());

    ver = IgniteProductVersion.fromString("1.2.3.b1-4-DEV");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("b1", ver.stage());
    assertEquals(4, ver.revisionTimestamp());
    assertArrayEquals(new byte[20], ver.revisionHash());

    ver = IgniteProductVersion.fromString("1.2.3.final-4-DEV");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("final", ver.stage());
    assertEquals(4, ver.revisionTimestamp());
    assertArrayEquals(new byte[20], ver.revisionHash());

    ver = IgniteProductVersion.fromString("1.2.3");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("", ver.stage());
    assertEquals(0, ver.revisionTimestamp());
    assertArrayEquals(new byte[20], ver.revisionHash());

    ver = IgniteProductVersion.fromString("1.2.3-4");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("", ver.stage());
    assertEquals(4, ver.revisionTimestamp());
    assertArrayEquals(new byte[20], ver.revisionHash());

    ver = IgniteProductVersion.fromString("1.2.3-4-18e5a7ec9e3202126a69bc231a6b965bc1d73dee");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("", ver.stage());
    assertEquals(4, ver.revisionTimestamp());
    assertArrayEquals(
        new byte[] {
          24, -27, -89, -20, -98, 50, 2, 18, 106, 105, -68, 35, 26, 107, -106, 91, -63, -41, 61, -18
        },
        ver.revisionHash());

    ver = IgniteProductVersion.fromString("1.2.3.b1-4-18e5a7ec9e3202126a69bc231a6b965bc1d73dee");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("b1", ver.stage());
    assertEquals(4, ver.revisionTimestamp());
    assertArrayEquals(
        new byte[] {
          24, -27, -89, -20, -98, 50, 2, 18, 106, 105, -68, 35, 26, 107, -106, 91, -63, -41, 61, -18
        },
        ver.revisionHash());

    ver = IgniteProductVersion.fromString("1.2.3-rc1-4-18e5a7ec9e3202126a69bc231a6b965bc1d73dee");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("rc1", ver.stage());
    assertEquals(4, ver.revisionTimestamp());
    assertArrayEquals(
        new byte[] {
          24, -27, -89, -20, -98, 50, 2, 18, 106, 105, -68, 35, 26, 107, -106, 91, -63, -41, 61, -18
        },
        ver.revisionHash());

    ver =
        IgniteProductVersion.fromString(
            "1.2.3-SNAPSHOT-4-18e5a7ec9e3202126a69bc231a6b965bc1d73dee");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("SNAPSHOT", ver.stage());
    assertEquals(4, ver.revisionTimestamp());
    assertArrayEquals(
        new byte[] {
          24, -27, -89, -20, -98, 50, 2, 18, 106, 105, -68, 35, 26, 107, -106, 91, -63, -41, 61, -18
        },
        ver.revisionHash());

    ver =
        IgniteProductVersion.fromString(
            "1.2.3.b1-SNAPSHOT-4-18e5a7ec9e3202126a69bc231a6b965bc1d73dee");

    assertEquals(1, ver.major());
    assertEquals(2, ver.minor());
    assertEquals(3, ver.maintenance());
    assertEquals("b1-SNAPSHOT", ver.stage());
    assertEquals(4, ver.revisionTimestamp());
    assertArrayEquals(
        new byte[] {
          24, -27, -89, -20, -98, 50, 2, 18, 106, 105, -68, 35, 26, 107, -106, 91, -63, -41, 61, -18
        },
        ver.revisionHash());

    IgniteProductVersion.fromString(VER_STR + '-' + BUILD_TSTAMP + '-' + REV_HASH_STR);
  }
public final class GridNearTxFinishFuture<K, V> extends GridCompoundIdentityFuture<IgniteInternalTx>
    implements GridCacheFuture<IgniteInternalTx> {
  /** */
  public static final IgniteProductVersion FINISH_NEAR_ONE_PHASE_SINCE =
      IgniteProductVersion.fromString("1.4.0");

  /** */
  public static final IgniteProductVersion WAIT_REMOTE_TXS_SINCE =
      IgniteProductVersion.fromString("1.5.1");

  /** */
  private static final long serialVersionUID = 0L;

  /** Logger reference. */
  private static final AtomicReference<IgniteLogger> logRef = new AtomicReference<>();

  /** Logger. */
  private static IgniteLogger log;

  /** Context. */
  private GridCacheSharedContext<K, V> cctx;

  /** Future ID. */
  private final IgniteUuid futId;

  /** Transaction. */
  @GridToStringInclude private GridNearTxLocal tx;

  /** Commit flag. */
  private boolean commit;

  /** Node mappings. */
  private IgniteTxMappings mappings;

  /** Trackable flag. */
  private boolean trackable = true;

  /** */
  private boolean finishOnePhaseCalled;

  /**
   * @param cctx Context.
   * @param tx Transaction.
   * @param commit Commit flag.
   */
  public GridNearTxFinishFuture(
      GridCacheSharedContext<K, V> cctx, GridNearTxLocal tx, boolean commit) {
    super(F.<IgniteInternalTx>identityReducer(tx));

    this.cctx = cctx;
    this.tx = tx;
    this.commit = commit;

    ignoreInterrupts(true);

    mappings = tx.mappings();

    futId = IgniteUuid.randomUuid();

    if (log == null) log = U.logger(cctx.kernalContext(), logRef, GridNearTxFinishFuture.class);
  }

  /** {@inheritDoc} */
  @Override
  public IgniteUuid futureId() {
    return futId;
  }

  /** {@inheritDoc} */
  @SuppressWarnings("unchecked")
  @Override
  public boolean onNodeLeft(UUID nodeId) {
    boolean found = false;

    for (IgniteInternalFuture<?> fut : futures())
      if (isMini(fut)) {
        MinFuture f = (MinFuture) fut;

        if (f.onNodeLeft(nodeId)) {
          // Remove previous mapping.
          mappings.remove(nodeId);

          found = true;
        }
      }

    return found;
  }

  /** {@inheritDoc} */
  @Override
  public boolean trackable() {
    return trackable;
  }

  /** Marks this future as not trackable. */
  @Override
  public void markNotTrackable() {
    trackable = false;
  }

  /**
   * @param nodeId Sender.
   * @param res Result.
   */
  @SuppressWarnings("ForLoopReplaceableByForEach")
  public void onResult(UUID nodeId, GridNearTxFinishResponse res) {
    if (!isDone()) {
      FinishMiniFuture finishFut = null;

      synchronized (futs) {
        for (int i = 0; i < futs.size(); i++) {
          IgniteInternalFuture<IgniteInternalTx> fut = futs.get(i);

          if (fut.getClass() == FinishMiniFuture.class) {
            FinishMiniFuture f = (FinishMiniFuture) fut;

            if (f.futureId().equals(res.miniId())) {
              assert f.node().id().equals(nodeId);

              finishFut = f;

              break;
            }
          }
        }
      }

      if (finishFut != null) finishFut.onNearFinishResponse(res);
    }
  }

  /**
   * @param nodeId Sender.
   * @param res Result.
   */
  public void onResult(UUID nodeId, GridDhtTxFinishResponse res) {
    if (!isDone())
      for (IgniteInternalFuture<IgniteInternalTx> fut : futures()) {
        if (fut.getClass() == CheckBackupMiniFuture.class) {
          CheckBackupMiniFuture f = (CheckBackupMiniFuture) fut;

          if (f.futureId().equals(res.miniId())) {
            assert f.node().id().equals(nodeId);

            f.onDhtFinishResponse(res);
          }
        } else if (fut.getClass() == CheckRemoteTxMiniFuture.class) {
          CheckRemoteTxMiniFuture f = (CheckRemoteTxMiniFuture) fut;

          if (f.futureId().equals(res.miniId())) f.onDhtFinishResponse(nodeId);
        }
      }
  }

  /** {@inheritDoc} */
  @Override
  public boolean onDone(IgniteInternalTx tx0, Throwable err) {
    if (isDone()) return false;

    synchronized (this) {
      if (isDone()) return false;

      if (err != null) {
        tx.commitError(err);

        boolean marked = tx.setRollbackOnly();

        if (err instanceof IgniteTxRollbackCheckedException) {
          if (marked) {
            try {
              tx.rollback();
            } catch (IgniteCheckedException ex) {
              U.error(log, "Failed to automatically rollback transaction: " + tx, ex);
            }
          }
        } else if (tx.implicit()
            && tx.isSystemInvalidate()) { // Finish implicit transaction on heuristic error.
          try {
            tx.close();
          } catch (IgniteCheckedException ex) {
            U.error(log, "Failed to invalidate transaction: " + tx, ex);
          }
        }
      }

      if (initialized() || err != null) {
        if (tx.needCheckBackup()) {
          assert tx.onePhaseCommit();

          if (err != null)
            err = new TransactionRollbackException("Failed to commit transaction.", err);

          try {
            tx.finish(err == null);
          } catch (IgniteCheckedException e) {
            if (err != null) err.addSuppressed(e);
            else err = e;
          }
        }

        if (tx.onePhaseCommit()) {
          boolean commit = this.commit && err == null;

          finishOnePhase(commit);

          tx.tmFinish(commit);
        }

        if (super.onDone(tx0, err)) {
          if (error() instanceof IgniteTxHeuristicCheckedException) {
            AffinityTopologyVersion topVer = tx.topologyVersion();

            for (IgniteTxEntry e : tx.writeMap().values()) {
              GridCacheContext cacheCtx = e.context();

              try {
                if (e.op() != NOOP && !cacheCtx.affinity().localNode(e.key(), topVer)) {
                  GridCacheEntryEx entry = cacheCtx.cache().peekEx(e.key());

                  if (entry != null) entry.invalidate(null, tx.xidVersion());
                }
              } catch (Throwable t) {
                U.error(log, "Failed to invalidate entry.", t);

                if (t instanceof Error) throw (Error) t;
              }
            }
          }

          // Don't forget to clean up.
          cctx.mvcc().removeFuture(futId);

          return true;
        }
      }
    }

    return false;
  }

  /**
   * @param fut Future.
   * @return {@code True} if mini-future.
   */
  private boolean isMini(IgniteInternalFuture<?> fut) {
    return fut.getClass() == FinishMiniFuture.class
        || fut.getClass() == CheckBackupMiniFuture.class
        || fut.getClass() == CheckRemoteTxMiniFuture.class;
  }

  /** Completeness callback. */
  private void onComplete() {
    onDone(tx);
  }

  /** @return Synchronous flag. */
  private boolean isSync() {
    return tx.explicitLock() || (commit ? tx.syncCommit() : tx.syncRollback());
  }

  /** Initializes future. */
  @SuppressWarnings("ForLoopReplaceableByForEach")
  void finish() {
    if (tx.onNeedCheckBackup()) {
      assert tx.onePhaseCommit();

      checkBackup();

      // If checkBackup is set, it means that primary node has crashed and we will not need to send
      // finish request to it, so we can mark future as initialized.
      markInitialized();

      return;
    }

    try {
      if (tx.finish(commit) || (!commit && tx.state() == UNKNOWN)) {
        if ((tx.onePhaseCommit() && needFinishOnePhase())
            || (!tx.onePhaseCommit() && mappings != null)) {
          if (mappings.single()) {
            GridDistributedTxMapping mapping = mappings.singleMapping();

            if (mapping != null) finish(mapping);
          } else finish(mappings.mappings());
        }

        markInitialized();

        if (!isSync() && !isDone()) {
          boolean complete = true;

          synchronized (futs) {
            // Avoid collection copy and iterator creation.
            for (int i = 0; i < futs.size(); i++) {
              IgniteInternalFuture<IgniteInternalTx> f = futs.get(i);

              if (isMini(f) && !f.isDone()) {
                complete = false;

                break;
              }
            }
          }

          if (complete) onComplete();
        }
      } else onDone(new IgniteCheckedException("Failed to commit transaction: " + CU.txString(tx)));
    } catch (Error | RuntimeException e) {
      onDone(e);

      throw e;
    } catch (IgniteCheckedException e) {
      onDone(e);
    }
  }

  /** */
  private void checkBackup() {
    GridDistributedTxMapping mapping = mappings.singleMapping();

    if (mapping != null) {
      UUID nodeId = mapping.node().id();

      Collection<UUID> backups = tx.transactionNodes().get(nodeId);

      if (!F.isEmpty(backups)) {
        assert backups.size() == 1;

        UUID backupId = F.first(backups);

        ClusterNode backup = cctx.discovery().node(backupId);

        // Nothing to do if backup has left the grid.
        if (backup == null) {
          readyNearMappingFromBackup(mapping);

          ClusterTopologyCheckedException cause =
              new ClusterTopologyCheckedException("Backup node left grid: " + backupId);

          cause.retryReadyFuture(cctx.nextAffinityReadyFuture(tx.topologyVersion()));

          onDone(
              new IgniteTxRollbackCheckedException(
                  "Failed to commit transaction " + "(backup has left grid): " + tx.xidVersion(),
                  cause));
        } else {
          final CheckBackupMiniFuture mini = new CheckBackupMiniFuture(backup, mapping);

          add(mini);

          if (backup.isLocal()) {
            boolean committed = !cctx.tm().addRolledbackTx(tx);

            readyNearMappingFromBackup(mapping);

            if (committed) {
              if (tx.syncCommit()) {
                GridCacheVersion nearXidVer = tx.nearXidVersion();

                assert nearXidVer != null : tx;

                IgniteInternalFuture<?> fut = cctx.tm().remoteTxFinishFuture(nearXidVer);

                fut.listen(
                    new CI1<IgniteInternalFuture<?>>() {
                      @Override
                      public void apply(IgniteInternalFuture<?> fut) {
                        mini.onDone(tx);
                      }
                    });

                return;
              }

              mini.onDone(tx);
            } else {
              ClusterTopologyCheckedException cause =
                  new ClusterTopologyCheckedException("Primary node left grid: " + nodeId);

              cause.retryReadyFuture(cctx.nextAffinityReadyFuture(tx.topologyVersion()));

              mini.onDone(
                  new IgniteTxRollbackCheckedException(
                      "Failed to commit transaction "
                          + "(transaction has been rolled back on backup node): "
                          + tx.xidVersion(),
                      cause));
            }
          } else {
            GridDhtTxFinishRequest finishReq = checkCommittedRequest(mini.futureId());

            // Preserve old behavior, otherwise response is not sent.
            if (WAIT_REMOTE_TXS_SINCE.compareTo(backup.version()) > 0) finishReq.syncCommit(true);

            try {
              if (FINISH_NEAR_ONE_PHASE_SINCE.compareTo(backup.version()) <= 0)
                cctx.io().send(backup, finishReq, tx.ioPolicy());
              else {
                mini.onDone(
                    new IgniteTxHeuristicCheckedException(
                        "Failed to check for tx commit on "
                            + "the backup node (node has an old Ignite version) [rmtNodeId="
                            + backup.id()
                            + ", ver="
                            + backup.version()
                            + ']'));
              }
            } catch (ClusterTopologyCheckedException e) {
              mini.onNodeLeft(backupId);
            } catch (IgniteCheckedException e) {
              mini.onDone(e);
            }
          }
        }
      } else readyNearMappingFromBackup(mapping);
    }
  }

  /** @return {@code True} if need to send finish request for one phase commit transaction. */
  private boolean needFinishOnePhase() {
    if (tx.mappings().empty()) return false;

    boolean finish = tx.txState().hasNearCache(cctx);

    if (finish) {
      GridDistributedTxMapping mapping = tx.mappings().singleMapping();

      if (FINISH_NEAR_ONE_PHASE_SINCE.compareTo(mapping.node().version()) > 0) finish = false;
    }

    return finish;
  }

  /** @param commit Commit flag. */
  private void finishOnePhase(boolean commit) {
    assert Thread.holdsLock(this);

    if (finishOnePhaseCalled) return;

    finishOnePhaseCalled = true;

    GridDistributedTxMapping locMapping = mappings.localMapping();

    if (locMapping != null) {
      // No need to send messages as transaction was already committed on remote node.
      // Finish local mapping only as we need send commit message to backups.
      IgniteInternalFuture<IgniteInternalTx> fut =
          cctx.tm().txHandler().finishColocatedLocal(commit, tx);

      // Add new future.
      if (fut != null) add(fut);
    }
  }

  /** @param mapping Mapping to finish. */
  private void readyNearMappingFromBackup(GridDistributedTxMapping mapping) {
    if (mapping.near()) {
      GridCacheVersion xidVer = tx.xidVersion();

      mapping.dhtVersion(xidVer, xidVer);

      tx.readyNearLocks(
          mapping,
          Collections.<GridCacheVersion>emptyList(),
          Collections.<GridCacheVersion>emptyList(),
          Collections.<GridCacheVersion>emptyList());
    }
  }

  /** @param mappings Mappings. */
  private void finish(Iterable<GridDistributedTxMapping> mappings) {
    // Create mini futures.
    for (GridDistributedTxMapping m : mappings) finish(m);
  }

  /** @param m Mapping. */
  private void finish(GridDistributedTxMapping m) {
    ClusterNode n = m.node();

    assert !m.empty();

    GridNearTxFinishRequest req =
        new GridNearTxFinishRequest(
            futId,
            tx.xidVersion(),
            tx.threadId(),
            commit,
            tx.isInvalidate(),
            tx.system(),
            tx.ioPolicy(),
            tx.syncCommit(),
            tx.syncRollback(),
            m.explicitLock(),
            tx.storeEnabled(),
            tx.topologyVersion(),
            null,
            null,
            null,
            tx.size(),
            tx.subjectId(),
            tx.taskNameHash(),
            tx.activeCachesDeploymentEnabled());

    // If this is the primary node for the keys.
    if (n.isLocal()) {
      req.miniId(IgniteUuid.randomUuid());

      IgniteInternalFuture<IgniteInternalTx> fut = cctx.tm().txHandler().finish(n.id(), tx, req);

      // Add new future.
      if (fut != null) add(fut);
    } else {
      FinishMiniFuture fut = new FinishMiniFuture(m);

      req.miniId(fut.futureId());

      add(fut); // Append new future.

      if (tx.pessimistic()) cctx.tm().beforeFinishRemote(n.id(), tx.threadId());

      try {
        cctx.io().send(n, req, tx.ioPolicy());

        // If we don't wait for result, then mark future as done.
        if (!isSync() && !m.explicitLock()) fut.onDone();
      } catch (ClusterTopologyCheckedException e) {
        // Remove previous mapping.
        mappings.remove(m.node().id());

        fut.onNodeLeft(n.id());
      } catch (IgniteCheckedException e) {
        // Fail the whole thing.
        fut.onDone(e);
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  public String toString() {
    Collection<String> futs =
        F.viewReadOnly(
            futures(),
            new C1<IgniteInternalFuture<?>, String>() {
              @SuppressWarnings("unchecked")
              @Override
              public String apply(IgniteInternalFuture<?> f) {
                if (f.getClass() == FinishMiniFuture.class) {
                  FinishMiniFuture fut = (FinishMiniFuture) f;

                  ClusterNode node = fut.node();

                  if (node != null) {
                    return "FinishFuture[node="
                        + node.id()
                        + ", loc="
                        + node.isLocal()
                        + ", done="
                        + fut.isDone()
                        + ']';
                  } else {
                    return "FinishFuture[node=null, done=" + fut.isDone() + ']';
                  }
                } else if (f.getClass() == CheckBackupMiniFuture.class) {
                  CheckBackupMiniFuture fut = (CheckBackupMiniFuture) f;

                  ClusterNode node = fut.node();

                  if (node != null) {
                    return "CheckBackupFuture[node="
                        + node.id()
                        + ", loc="
                        + node.isLocal()
                        + ", done="
                        + f.isDone()
                        + "]";
                  } else {
                    return "CheckBackupFuture[node=null, done=" + f.isDone() + "]";
                  }
                } else if (f.getClass() == CheckRemoteTxMiniFuture.class) {
                  CheckRemoteTxMiniFuture fut = (CheckRemoteTxMiniFuture) f;

                  return "CheckRemoteTxMiniFuture[nodes="
                      + fut.nodes()
                      + ", done="
                      + f.isDone()
                      + "]";
                } else return "[loc=true, done=" + f.isDone() + "]";
              }
            });

    return S.toString(
        GridNearTxFinishFuture.class, this, "innerFuts", futs, "super", super.toString());
  }

  /**
   * @param miniId Mini future ID.
   * @return Finish request.
   */
  private GridDhtTxFinishRequest checkCommittedRequest(IgniteUuid miniId) {
    GridDhtTxFinishRequest finishReq =
        new GridDhtTxFinishRequest(
            cctx.localNodeId(),
            futureId(),
            miniId,
            tx.topologyVersion(),
            tx.xidVersion(),
            tx.commitVersion(),
            tx.threadId(),
            tx.isolation(),
            true,
            false,
            tx.system(),
            tx.ioPolicy(),
            false,
            tx.syncCommit(),
            tx.syncRollback(),
            null,
            null,
            null,
            null,
            0,
            null,
            0,
            tx.activeCachesDeploymentEnabled());

    finishReq.checkCommitted(true);

    return finishReq;
  }

  /** */
  private abstract class MinFuture extends GridFutureAdapter<IgniteInternalTx> {
    /** */
    private final IgniteUuid futId = IgniteUuid.randomUuid();

    /**
     * @param nodeId Node ID.
     * @return {@code True} if future processed node failure.
     */
    abstract boolean onNodeLeft(UUID nodeId);

    /** @return Future ID. */
    final IgniteUuid futureId() {
      return futId;
    }
  }

  /**
   * Mini-future for get operations. Mini-futures are only waiting on a single node as opposed to
   * multiple nodes.
   */
  private class FinishMiniFuture extends MinFuture {
    /** */
    private static final long serialVersionUID = 0L;

    /** Keys. */
    @GridToStringInclude private GridDistributedTxMapping m;

    /** @param m Mapping. */
    FinishMiniFuture(GridDistributedTxMapping m) {
      this.m = m;
    }

    /** @return Node ID. */
    ClusterNode node() {
      return m.node();
    }

    /** @return Keys. */
    public GridDistributedTxMapping mapping() {
      return m;
    }

    /** @param nodeId Failed node ID. */
    boolean onNodeLeft(UUID nodeId) {
      if (nodeId.equals(m.node().id())) {
        if (log.isDebugEnabled())
          log.debug("Remote node left grid while sending or waiting for reply: " + this);

        if (isSync()) {
          Map<UUID, Collection<UUID>> txNodes = tx.transactionNodes();

          if (txNodes != null) {
            Collection<UUID> backups = txNodes.get(nodeId);

            if (!F.isEmpty(backups)) {
              final CheckRemoteTxMiniFuture mini =
                  new CheckRemoteTxMiniFuture(new HashSet<>(backups));

              add(mini);

              GridDhtTxFinishRequest req = checkCommittedRequest(mini.futureId());

              req.waitRemoteTransactions(true);

              for (UUID backupId : backups) {
                ClusterNode backup = cctx.discovery().node(backupId);

                if (backup != null && WAIT_REMOTE_TXS_SINCE.compareTo(backup.version()) <= 0) {
                  if (backup.isLocal()) {
                    IgniteInternalFuture<?> fut =
                        cctx.tm().remoteTxFinishFuture(tx.nearXidVersion());

                    fut.listen(
                        new CI1<IgniteInternalFuture<?>>() {
                          @Override
                          public void apply(IgniteInternalFuture<?> fut) {
                            mini.onDhtFinishResponse(cctx.localNodeId());
                          }
                        });
                  } else {
                    try {
                      cctx.io().send(backup, req, tx.ioPolicy());
                    } catch (ClusterTopologyCheckedException e) {
                      mini.onNodeLeft(backupId);
                    } catch (IgniteCheckedException e) {
                      mini.onDone(e);
                    }
                  }
                } else mini.onDhtFinishResponse(backupId);
              }
            }
          }
        }

        onDone(tx);

        return true;
      }

      return false;
    }

    /** @param res Result callback. */
    void onNearFinishResponse(GridNearTxFinishResponse res) {
      if (res.error() != null) onDone(res.error());
      else onDone(tx);
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
      return S.toString(
          FinishMiniFuture.class,
          this,
          "done",
          isDone(),
          "cancelled",
          isCancelled(),
          "err",
          error());
    }
  }

  /** */
  private class CheckBackupMiniFuture extends MinFuture {
    /** Keys. */
    @GridToStringInclude private GridDistributedTxMapping m;

    /** Backup node to check. */
    private ClusterNode backup;

    /**
     * @param backup Backup to check.
     * @param m Mapping associated with the backup.
     */
    CheckBackupMiniFuture(ClusterNode backup, GridDistributedTxMapping m) {
      this.backup = backup;
      this.m = m;
    }

    /** @return Node ID. */
    public ClusterNode node() {
      return backup;
    }

    /** {@inheritDoc} */
    @Override
    boolean onNodeLeft(UUID nodeId) {
      if (nodeId.equals(backup.id())) {
        readyNearMappingFromBackup(m);

        onDone(new ClusterTopologyCheckedException("Remote node left grid: " + nodeId));

        return true;
      }

      return false;
    }

    /** @param res Response. */
    void onDhtFinishResponse(GridDhtTxFinishResponse res) {
      readyNearMappingFromBackup(m);

      Throwable err = res.checkCommittedError();

      if (err != null) {
        if (err instanceof IgniteCheckedException) {
          ClusterTopologyCheckedException cause =
              ((IgniteCheckedException) err).getCause(ClusterTopologyCheckedException.class);

          if (cause != null)
            cause.retryReadyFuture(cctx.nextAffinityReadyFuture(tx.topologyVersion()));
        }

        onDone(err);
      } else onDone(tx);
    }
  }

  /** */
  private class CheckRemoteTxMiniFuture extends MinFuture {
    /** */
    private Set<UUID> nodes;

    /** @param nodes Backup nodes. */
    public CheckRemoteTxMiniFuture(Set<UUID> nodes) {
      this.nodes = nodes;
    }

    /** @return Backup nodes. */
    Set<UUID> nodes() {
      synchronized (this) {
        return new HashSet<>(nodes);
      }
    }

    /** {@inheritDoc} */
    @Override
    boolean onNodeLeft(UUID nodeId) {
      return onResponse(nodeId);
    }

    /** @param nodeId Node ID. */
    void onDhtFinishResponse(UUID nodeId) {
      onResponse(nodeId);
    }

    /**
     * @param nodeId Node ID.
     * @return {@code True} if processed node response.
     */
    private boolean onResponse(UUID nodeId) {
      boolean done;

      boolean ret;

      synchronized (this) {
        ret = nodes.remove(nodeId);

        done = nodes.isEmpty();
      }

      if (done) onDone(tx);

      return ret;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
      return S.toString(CheckRemoteTxMiniFuture.class, this);
    }
  }
}
  private void checkBackup() {
    GridDistributedTxMapping mapping = mappings.singleMapping();

    if (mapping != null) {
      UUID nodeId = mapping.node().id();

      Collection<UUID> backups = tx.transactionNodes().get(nodeId);

      if (!F.isEmpty(backups)) {
        assert backups.size() == 1;

        UUID backupId = F.first(backups);

        ClusterNode backup = cctx.discovery().node(backupId);

        // Nothing to do if backup has left the grid.
        if (backup == null) {
          readyNearMappingFromBackup(mapping);

          ClusterTopologyCheckedException cause =
              new ClusterTopologyCheckedException("Backup node left grid: " + backupId);

          cause.retryReadyFuture(cctx.nextAffinityReadyFuture(tx.topologyVersion()));

          onDone(
              new IgniteTxRollbackCheckedException(
                  "Failed to commit transaction " + "(backup has left grid): " + tx.xidVersion(),
                  cause));
        } else {
          final CheckBackupMiniFuture mini = new CheckBackupMiniFuture(backup, mapping);

          add(mini);

          if (backup.isLocal()) {
            boolean committed = !cctx.tm().addRolledbackTx(tx);

            readyNearMappingFromBackup(mapping);

            if (committed) {
              if (tx.syncCommit()) {
                GridCacheVersion nearXidVer = tx.nearXidVersion();

                assert nearXidVer != null : tx;

                IgniteInternalFuture<?> fut = cctx.tm().remoteTxFinishFuture(nearXidVer);

                fut.listen(
                    new CI1<IgniteInternalFuture<?>>() {
                      @Override
                      public void apply(IgniteInternalFuture<?> fut) {
                        mini.onDone(tx);
                      }
                    });

                return;
              }

              mini.onDone(tx);
            } else {
              ClusterTopologyCheckedException cause =
                  new ClusterTopologyCheckedException("Primary node left grid: " + nodeId);

              cause.retryReadyFuture(cctx.nextAffinityReadyFuture(tx.topologyVersion()));

              mini.onDone(
                  new IgniteTxRollbackCheckedException(
                      "Failed to commit transaction "
                          + "(transaction has been rolled back on backup node): "
                          + tx.xidVersion(),
                      cause));
            }
          } else {
            GridDhtTxFinishRequest finishReq = checkCommittedRequest(mini.futureId());

            // Preserve old behavior, otherwise response is not sent.
            if (WAIT_REMOTE_TXS_SINCE.compareTo(backup.version()) > 0) finishReq.syncCommit(true);

            try {
              if (FINISH_NEAR_ONE_PHASE_SINCE.compareTo(backup.version()) <= 0)
                cctx.io().send(backup, finishReq, tx.ioPolicy());
              else {
                mini.onDone(
                    new IgniteTxHeuristicCheckedException(
                        "Failed to check for tx commit on "
                            + "the backup node (node has an old Ignite version) [rmtNodeId="
                            + backup.id()
                            + ", ver="
                            + backup.version()
                            + ']'));
              }
            } catch (ClusterTopologyCheckedException e) {
              mini.onNodeLeft(backupId);
            } catch (IgniteCheckedException e) {
              mini.onDone(e);
            }
          }
        }
      } else readyNearMappingFromBackup(mapping);
    }
  }