/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ignite.internal.processors.cache.transactions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.cache.processor.EntryProcessor;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.cluster.ClusterNode;
import org.apache.ignite.failure.FailureContext;
import org.apache.ignite.failure.FailureType;
import org.apache.ignite.internal.IgniteFutureTimeoutCheckedException;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.internal.cluster.ClusterTopologyCheckedException;
import org.apache.ignite.internal.pagemem.wal.record.RollbackRecord;
import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
import org.apache.ignite.internal.processors.cache.CacheEntryInfoCollection;
import org.apache.ignite.internal.processors.cache.CacheGroupContext;
import org.apache.ignite.internal.processors.cache.CacheObject;
import org.apache.ignite.internal.processors.cache.GridCacheContext;
import org.apache.ignite.internal.processors.cache.GridCacheEntryEx;
import org.apache.ignite.internal.processors.cache.GridCacheEntryInfo;
import org.apache.ignite.internal.processors.cache.GridCacheEntryRemovedException;
import org.apache.ignite.internal.processors.cache.GridCacheMessage;
import org.apache.ignite.internal.processors.cache.GridCacheReturnCompletableWrapper;
import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
import org.apache.ignite.internal.processors.cache.GridCacheUpdateTxResult;
import org.apache.ignite.internal.processors.cache.KeyCacheObject;
import org.apache.ignite.internal.processors.cache.distributed.GridCacheTxRecoveryFuture;
import org.apache.ignite.internal.processors.cache.distributed.GridCacheTxRecoveryRequest;
import org.apache.ignite.internal.processors.cache.distributed.GridCacheTxRecoveryResponse;
import org.apache.ignite.internal.processors.cache.distributed.GridDistributedTxRemoteAdapter;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtCacheAdapter;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtCacheEntry;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTopologyFuture;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxFinishFuture;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxFinishRequest;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxFinishResponse;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxLocal;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxOnePhaseCommitAckRequest;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxPrepareFuture;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxPrepareRequest;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxPrepareResponse;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridDhtTxRemote;
import org.apache.ignite.internal.processors.cache.distributed.dht.GridInvokeValue;
import org.apache.ignite.internal.processors.cache.distributed.dht.PartitionUpdateCountersMessage;
import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtInvalidPartitionException;
import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtLocalPartition;
import org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtPartitionTopology;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearCacheAdapter;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxFinishFuture;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxFinishRequest;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxFinishResponse;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxLocal;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxPrepareFutureAdapter;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxPrepareRequest;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxPrepareResponse;
import org.apache.ignite.internal.processors.cache.distributed.near.GridNearTxRemote;
import org.apache.ignite.internal.processors.cache.mvcc.MvccSnapshot;
import org.apache.ignite.internal.processors.cache.persistence.snapshot.IncrementalSnapshotAwareMessage;
import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
import org.apache.ignite.internal.processors.cache.version.GridCacheVersion;
import org.apache.ignite.internal.processors.query.EnlistOperation;
import org.apache.ignite.internal.processors.query.IgniteSQLException;
import org.apache.ignite.internal.processors.tracing.MTC;
import org.apache.ignite.internal.transactions.IgniteTxHeuristicCheckedException;
import org.apache.ignite.internal.transactions.IgniteTxOptimisticCheckedException;
import org.apache.ignite.internal.transactions.IgniteTxRollbackCheckedException;
import org.apache.ignite.internal.transactions.IgniteTxTimeoutCheckedException;
import org.apache.ignite.internal.util.future.GridCompoundFuture;
import org.apache.ignite.internal.util.future.GridFinishedFuture;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.X;
import org.apache.ignite.internal.util.typedef.internal.CU;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteFutureCancelledException;
import org.apache.ignite.lang.IgniteUuid;
import org.apache.ignite.plugin.extensions.communication.Message;
import org.apache.ignite.thread.IgniteThread;
import org.apache.ignite.transactions.TransactionState;
import org.jetbrains.annotations.Nullable;

import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_SYNC;
import static org.apache.ignite.internal.managers.communication.GridIoPolicy.SYSTEM_POOL;
import static org.apache.ignite.internal.managers.communication.GridIoPolicy.UTILITY_CACHE_POOL;
import static org.apache.ignite.internal.processors.cache.GridCacheIoManager.COMMON_MESSAGE_HANDLER_ID;
import static org.apache.ignite.internal.processors.cache.GridCacheOperation.NOOP;
import static org.apache.ignite.internal.processors.cache.GridCacheOperation.TRANSFORM;
import static org.apache.ignite.internal.processors.cache.GridCacheUtils.isNearEnabled;
import static org.apache.ignite.internal.processors.cache.distributed.dht.topology.GridDhtPartitionState.RENTING;
import static org.apache.ignite.internal.processors.cache.transactions.IgniteInternalTx.FinalizationStatus.USER_FINISH;
import static org.apache.ignite.internal.processors.security.SecurityUtils.securitySubjectId;
import static org.apache.ignite.internal.processors.tracing.MTC.TraceSurroundings;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_NEAR_FINISH_REQ;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_NEAR_FINISH_RESP;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_NEAR_PREPARE_REQ;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_NEAR_PREPARE_RESP;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_PROCESS_DHT_FINISH_REQ;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_PROCESS_DHT_FINISH_RESP;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_PROCESS_DHT_ONE_PHASE_COMMIT_ACK_REQ;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_PROCESS_DHT_PREPARE_REQ;
import static org.apache.ignite.internal.processors.tracing.SpanType.TX_PROCESS_DHT_PREPARE_RESP;
import static org.apache.ignite.transactions.TransactionConcurrency.OPTIMISTIC;
import static org.apache.ignite.transactions.TransactionConcurrency.PESSIMISTIC;
import static org.apache.ignite.transactions.TransactionState.PREPARED;
import static org.apache.ignite.transactions.TransactionState.ROLLED_BACK;
import static org.apache.ignite.transactions.TransactionState.ROLLING_BACK;

/**
 * Isolated logic to process cache messages.
 */
public class IgniteTxHandler {
    /** Logger. */
    private final IgniteLogger log;

    /** */
    private final IgniteLogger txPrepareMsgLog;

    /** */
    private final IgniteLogger txFinishMsgLog;

    /** */
    private final IgniteLogger txRecoveryMsgLog;

    /** Shared cache context. */
    private final GridCacheSharedContext<?, ?> ctx;

    /**
     * @param nearNodeId Sender node ID.
     * @param req Request.
     */
    private void processNearTxPrepareRequest(UUID nearNodeId, GridNearTxPrepareRequest req) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_NEAR_PREPARE_REQ, MTC.span()))) {
            if (txPrepareMsgLog.isDebugEnabled()) {
                txPrepareMsgLog.debug("Received near prepare request [txId=" + req.version() +
                    ", node=" + nearNodeId + ']');
            }

            ClusterNode nearNode = ctx.node(nearNodeId);

            if (nearNode == null) {
                if (txPrepareMsgLog.isDebugEnabled()) {
                    txPrepareMsgLog.debug("Received near prepare from node that left grid (will ignore) [" +
                        "txId=" + req.version() +
                        ", node=" + nearNodeId + ']');
                }

                return;
            }

            processNearTxPrepareRequest0(nearNode, req);
        }
    }

    /**
     * @param nearNode Sender node.
     * @param req Request.
     */
    private void processNearTxPrepareRequest0(ClusterNode nearNode, GridNearTxPrepareRequest req) {
        IgniteInternalFuture<GridNearTxPrepareResponse> fut;

        if (req.firstClientRequest() && req.allowWaitTopologyFuture()) {
            for (;;) {
                if (waitForExchangeFuture(nearNode, req))
                    return;

                fut = prepareNearTx(nearNode, req);

                if (fut != null)
                    break;
            }
        }
        else
            fut = prepareNearTx(nearNode, req);

        assert req.txState() != null || fut == null || fut.error() != null ||
            (ctx.tm().tx(req.version()) == null && ctx.tm().nearTx(req.version()) == null);
    }

    /**
     * @param ctx Shared cache context.
     */
    public IgniteTxHandler(GridCacheSharedContext ctx) {
        this.ctx = ctx;

        log = ctx.logger(IgniteTxHandler.class);

        txRecoveryMsgLog = ctx.logger(CU.TX_MSG_RECOVERY_LOG_CATEGORY);
        txPrepareMsgLog = ctx.logger(CU.TX_MSG_PREPARE_LOG_CATEGORY);
        txFinishMsgLog = ctx.logger(CU.TX_MSG_FINISH_LOG_CATEGORY);

        ctx.io().addCacheHandler(GridNearTxPrepareRequest.class, (UUID nodeId, GridCacheMessage msg) ->
            processNearTxPrepareRequest(nodeId, (GridNearTxPrepareRequest)msg));

        ctx.io().addCacheHandler(GridNearTxPrepareResponse.class, (UUID nodeId, GridCacheMessage msg) ->
            processNearTxPrepareResponse(nodeId, (GridNearTxPrepareResponse)msg));

        ctx.io().addCacheHandler(GridNearTxFinishRequest.class, (UUID nodeId, GridCacheMessage msg) ->
            processNearTxFinishRequest(nodeId, (GridNearTxFinishRequest)msg));

        ctx.io().addCacheHandler(GridNearTxFinishResponse.class, (UUID nodeId, GridCacheMessage msg) ->
            processNearTxFinishResponse(nodeId, (GridNearTxFinishResponse)msg));

        ctx.io().addCacheHandler(GridDhtTxPrepareRequest.class, (UUID nodeId, GridCacheMessage msg) ->
            processDhtTxPrepareRequest(nodeId, (GridDhtTxPrepareRequest)msg));

        ctx.io().addCacheHandler(GridDhtTxPrepareResponse.class, (UUID nodeId, GridCacheMessage msg) ->
            processDhtTxPrepareResponse(nodeId, (GridDhtTxPrepareResponse)msg));

        ctx.io().addCacheHandler(GridDhtTxFinishRequest.class, (UUID nodeId, GridCacheMessage msg) ->
            processDhtTxFinishRequest(nodeId, (GridDhtTxFinishRequest)msg));

        ctx.io().addCacheHandler(GridDhtTxOnePhaseCommitAckRequest.class, (UUID nodeId, GridCacheMessage msg) ->
            processDhtTxOnePhaseCommitAckRequest(nodeId, (GridDhtTxOnePhaseCommitAckRequest)msg));

        ctx.io().addCacheHandler(GridDhtTxFinishResponse.class, (UUID nodeId, GridCacheMessage msg) ->
            processDhtTxFinishResponse(nodeId, (GridDhtTxFinishResponse)msg));

        ctx.io().addCacheHandler(GridCacheTxRecoveryRequest.class, this::processCheckPreparedTxRequest);

        ctx.io().addCacheHandler(GridCacheTxRecoveryResponse.class, this::processCheckPreparedTxResponse);

        ctx.io().addCacheHandler(IncrementalSnapshotAwareMessage.class, this::processIncrementalSnapshotAwareMessage);
    }

    /** */
    private void processIncrementalSnapshotAwareMessage(UUID nodeId, IncrementalSnapshotAwareMessage msg) {
        // Skip if node has joined after incremental snapshot started.
        if (ctx.localNode().order() <= msg.snapshotTopologyVersion()) {
            ctx.snapshotMgr().handleIncrementalSnapshotId(msg.id(), msg.snapshotTopologyVersion());

            setIncrementalSnapshotIdIfRequired(msg);
        }

        GridCacheMessage cacheMsg = msg.payload();

        ctx.io()
            .cacheHandler(COMMON_MESSAGE_HANDLER_ID, cacheMsg.getClass())
            .apply(nodeId, cacheMsg);
    }

    /**
     * Set received incremental snapshot ID to transaction if specified.
     *
     * @param msg Finish message signed with incremental snapshot ID.
     */
    private void setIncrementalSnapshotIdIfRequired(IncrementalSnapshotAwareMessage msg) {
        if (msg.txInrementalSnapshotId() != null) {
            IgniteInternalTx tx = findTransactionByMessage(msg.payload());

            if (tx != null)
                tx.incrementalSnapshotId(msg.txInrementalSnapshotId());
        }
    }

    /**
     * Finds transaction related to the specified message.
     *
     * @param msg Transaction message.
     * @return Transaction, or {@code null} if not found.
     */
    private @Nullable IgniteInternalTx findTransactionByMessage(GridCacheMessage msg) {
        if (msg instanceof GridNearTxFinishRequest) {
            GridNearTxFinishRequest req = (GridNearTxFinishRequest)msg;

            GridCacheVersion dhtVer = ctx.tm().mappedVersion(req.version());

            if (dhtVer != null)
                return ctx.tm().tx(dhtVer);
        }
        else if (msg instanceof GridDhtTxFinishRequest) {
            GridDhtTxFinishRequest req = (GridDhtTxFinishRequest)msg;

            return ctx.tm().tx(req.version());
        }
        else if (msg instanceof GridDhtTxPrepareResponse) {
            GridDhtTxPrepareResponse res = (GridDhtTxPrepareResponse)msg;

            GridDhtTxPrepareFuture fut =
                (GridDhtTxPrepareFuture)ctx.mvcc().versionedFuture(res.version(), res.futureId());

            if (fut != null)
                return fut.tx();
        }
        else if (msg instanceof GridNearTxPrepareResponse) {
            GridNearTxPrepareResponse res = (GridNearTxPrepareResponse)msg;

            GridNearTxPrepareFutureAdapter fut =
                (GridNearTxPrepareFutureAdapter)ctx.mvcc().versionedFuture(res.version(), res.futureId());

            if (fut != null)
                return fut.tx();
        }

        assert false : "Unexpected message is wrapped into IncrementalSnapshotAwareMessage: " + msg;

        return null;
    }

    /**
     * Prepares local colocated tx.
     *
     * @param locTx Local transaction.
     * @param req Near prepare request.
     * @return Prepare future.
     */
    public IgniteInternalFuture<GridNearTxPrepareResponse> prepareColocatedTx(
        final GridNearTxLocal locTx,
        final GridNearTxPrepareRequest req
    ) {
        req.txState(locTx.txState());

        IgniteInternalFuture<GridNearTxPrepareResponse> fut = locTx.prepareAsyncLocal(req);

        return fut.chain(() -> {
            try {
                return fut.get();
            }
            catch (Exception e) {
                locTx.setRollbackOnly(); // Just in case.

                if (!X.hasCause(e, IgniteTxOptimisticCheckedException.class) &&
                    !X.hasCause(e, IgniteFutureCancelledException.class))
                    U.error(log, "Failed to prepare DHT transaction: " + locTx, e);

                return new GridNearTxPrepareResponse(
                    req.partition(),
                    req.version(),
                    req.futureId(),
                    req.miniId(),
                    req.version(),
                    req.version(),
                    null,
                    e,
                    null,
                    req.onePhaseCommit(),
                    req.deployInfo() != null);
            }
        });
    }

    /**
     * @param entries Entries.
     * @return First entry.
     * @throws IgniteCheckedException If failed.
     */
    private IgniteTxEntry unmarshal(@Nullable Collection<IgniteTxEntry> entries) throws IgniteCheckedException {
        if (entries == null)
            return null;

        IgniteTxEntry firstEntry = null;

        for (IgniteTxEntry e : entries) {
            e.unmarshal(ctx, false, ctx.deploy().globalLoader());

            if (firstEntry == null)
                firstEntry = e;
        }

        return firstEntry;
    }

    /**
     * @param originTx Transaction for copy.
     * @param req Request.
     * @return Prepare future.
     */
    public IgniteInternalFuture<GridNearTxPrepareResponse> prepareNearTxLocal(
        final GridNearTxLocal originTx,
        final GridNearTxPrepareRequest req) {
        // Make sure not to provide Near entries to DHT cache.
        req.cloneEntries();

        return prepareNearTx(originTx, ctx.localNode(), req);
    }

    /**
     * @param nearNode Node that initiated transaction.
     * @param req Near prepare request.
     * @return Prepare future or {@code null} if need retry operation.
     */
    @Nullable private IgniteInternalFuture<GridNearTxPrepareResponse> prepareNearTx(
        final ClusterNode nearNode,
        final GridNearTxPrepareRequest req
    ) {
        return prepareNearTx(null, nearNode, req);
    }

    /**
     * @param originTx Transaction for copy.
     * @param nearNode Node that initiated transaction.
     * @param req Near prepare request.
     * @return Prepare future or {@code null} if need retry operation.
     */
    @Nullable private IgniteInternalFuture<GridNearTxPrepareResponse> prepareNearTx(
        final GridNearTxLocal originTx,
        final ClusterNode nearNode,
        final GridNearTxPrepareRequest req
    ) {
        IgniteTxEntry firstEntry;

        try {
            IgniteTxEntry firstWrite = unmarshal(req.writes());
            IgniteTxEntry firstRead = unmarshal(req.reads());

            firstEntry = firstWrite != null ? firstWrite : firstRead;
        }
        catch (IgniteCheckedException e) {
            return new GridFinishedFuture<>(e);
        }

        GridDhtTxLocal tx = null;

        GridCacheVersion mappedVer = ctx.tm().mappedVersion(req.version());

        if (mappedVer != null) {
            tx = ctx.tm().tx(mappedVer);

            if (tx == null)
                U.warn(log, "Missing local transaction for mapped near version [nearVer=" + req.version()
                    + ", mappedVer=" + mappedVer + ']');
            else {
                if (req.concurrency() == PESSIMISTIC)
                    tx.nearFutureId(req.futureId());
            }
        }
        else {
            GridDhtPartitionTopology top = null;

            if (req.firstClientRequest()) {
                assert firstEntry != null : req;

                assert req.concurrency() == OPTIMISTIC : req;
                assert nearNode.isClient() : nearNode;

                top = firstEntry.context().topology();

                top.readLock();

                if (req.allowWaitTopologyFuture()) {
                    GridDhtTopologyFuture topFut = top.topologyVersionFuture();

                    if (!topFut.isDone()) {
                        top.readUnlock();

                        return null;
                    }
                }
            }

            try {
                if (top != null ) {
                    boolean retry = false;

                    GridDhtTopologyFuture topFut = top.topologyVersionFuture();

                    if (!req.allowWaitTopologyFuture() && !topFut.isDone()) {
                        retry = true;

                        if (txPrepareMsgLog.isDebugEnabled()) {
                            txPrepareMsgLog.debug("Topology change is in progress, need remap transaction [" +
                                "txId=" + req.version() +
                                ", node=" + nearNode.id() +
                                ", reqTopVer=" + req.topologyVersion() +
                                ", locTopVer=" + top.readyTopologyVersion() +
                                ", req=" + req + ']');
                        }
                    }

                    if (!retry && needRemap(req.topologyVersion(), req)) {
                        retry = true;

                        if (txPrepareMsgLog.isDebugEnabled()) {
                            txPrepareMsgLog.debug("Topology version mismatch for near prepare, need remap transaction [" +
                                "txId=" + req.version() +
                                ", node=" + nearNode.id() +
                                ", reqTopVer=" + req.topologyVersion() +
                                ", locTopVer=" + top.readyTopologyVersion() +
                                ", req=" + req + ']');
                        }
                    }

                    if (retry) {
                        GridNearTxPrepareResponse res = new GridNearTxPrepareResponse(
                            req.partition(),
                            req.version(),
                            req.futureId(),
                            req.miniId(),
                            req.version(),
                            req.version(),
                            null,
                            null,
                            top.lastTopologyChangeVersion(),
                            req.onePhaseCommit(),
                            req.deployInfo() != null);

                        try {
                            ctx.io().send(nearNode, res, req.policy());

                            if (txPrepareMsgLog.isDebugEnabled()) {
                                txPrepareMsgLog.debug("Sent remap response for near prepare [txId=" + req.version() +
                                    ", node=" + nearNode.id() + ']');
                            }
                        }
                        catch (ClusterTopologyCheckedException ignored) {
                            if (txPrepareMsgLog.isDebugEnabled()) {
                                txPrepareMsgLog.debug("Failed to send remap response for near prepare, node failed [" +
                                    "txId=" + req.version() +
                                    ", node=" + nearNode.id() + ']');
                            }
                        }
                        catch (IgniteCheckedException e) {
                            U.error(txPrepareMsgLog, "Failed to send remap response for near prepare " +
                                "[txId=" + req.version() +
                                ", node=" + nearNode.id() +
                                ", req=" + req + ']', e);
                        }

                        return new GridFinishedFuture<>(res);
                    }

                    assert topFut.isDone();
                }

                tx = new GridDhtTxLocal(
                    ctx,
                    req.topologyVersion(),
                    nearNode.id(),
                    req.version(),
                    req.futureId(),
                    req.miniId(),
                    req.threadId(),
                    req.implicitSingle(),
                    req.implicitSingle(),
                    req.system(),
                    req.explicitLock(),
                    req.policy(),
                    req.concurrency(),
                    req.isolation(),
                    req.timeout(),
                    req.isInvalidate(),
                    true,
                    req.onePhaseCommit(),
                    req.txSize(),
                    req.transactionNodes(),
                    securitySubjectId(ctx),
                    req.taskNameHash(),
                    req.txLabel(),
                    originTx
                );

                tx = ctx.tm().onCreated(null, tx);

                if (tx != null)
                    tx.topologyVersion(req.topologyVersion());
                else
                    U.warn(log, "Failed to create local transaction (was transaction rolled back?) [xid=" +
                        req.version() + ", req=" + req + ']');
            }
            finally {
                if (tx != null)
                    req.txState(tx.txState());

                if (top != null)
                    top.readUnlock();
            }
        }

        if (tx != null) {
            req.txState(tx.txState());

            if (req.explicitLock())
                tx.explicitLock(true);

            tx.transactionNodes(req.transactionNodes());

            if (req.near())
                tx.nearOnOriginatingNode(true);

            if (req.onePhaseCommit()) {
                assert req.last() : req;

                tx.onePhaseCommit(true);
            }

            if (req.needReturnValue())
                tx.needReturnValue(true);

            IgniteInternalFuture<GridNearTxPrepareResponse> fut = tx.prepareAsync(req);

            if (tx.isRollbackOnly() && !tx.commitOnPrepare()) {
                if (tx.state() != TransactionState.ROLLED_BACK && tx.state() != TransactionState.ROLLING_BACK)
                    tx.rollbackDhtLocalAsync();
            }

            final GridDhtTxLocal tx0 = tx;

            fut.listen(() -> {
                try {
                    fut.get();
                }
                catch (IgniteCheckedException e) {
                    tx0.setRollbackOnly(); // Just in case.

                    if (!X.hasCause(e, IgniteTxOptimisticCheckedException.class) &&
                        !X.hasCause(e, IgniteFutureCancelledException.class) && !ctx.kernalContext().isStopping())
                        U.error(log, "Failed to prepare DHT transaction: " + tx0, e);
                }
            });

            return fut;
        }
        else
            return new GridFinishedFuture<>((GridNearTxPrepareResponse)null);
    }

    /**
     * @param node Sender node.
     * @param req Request.
     * @return {@code True} if update will be retried from future listener or topology version future is timed out.
     */
    private boolean waitForExchangeFuture(final ClusterNode node, final GridNearTxPrepareRequest req) {
        assert req.firstClientRequest() : req;

        GridDhtTopologyFuture topFut = ctx.exchange().lastTopologyFuture();

        if (!topFut.isDone()) {
            Thread curThread = Thread.currentThread();

            if (curThread instanceof IgniteThread) {
                final IgniteThread thread = (IgniteThread)curThread;

                if (thread.cachePoolThread()) {
                    ctx.time().waitAsync(topFut, req.timeout(), (e, timedOut) -> {
                            if (e != null || timedOut) {
                                sendResponseOnTimeoutOrError(e, topFut, node, req);

                                return;
                            }
                            ctx.kernalContext().closure().runLocalWithThreadPolicy(thread, () -> {
                                try {
                                    processNearTxPrepareRequest0(node, req);
                                }
                                finally {
                                    ctx.io().onMessageProcessed(req);
                                }
                            });
                        }
                    );

                    return true;
                }
            }

            try {
                if (req.timeout() > 0)
                    topFut.get(req.timeout());
                else
                    topFut.get();
            }
            catch (IgniteFutureTimeoutCheckedException e) {
                sendResponseOnTimeoutOrError(null, topFut, node, req);

                return true;
            }
            catch (IgniteCheckedException e) {
                U.error(log, "Topology future failed: " + e, e);
            }
        }

        return false;
    }

    /**
     * @param e Exception or null if timed out.
     * @param topFut Topology future.
     * @param node Node.
     * @param req Prepare request.
     */
    private void sendResponseOnTimeoutOrError(@Nullable IgniteCheckedException e,
        GridDhtTopologyFuture topFut,
        ClusterNode node,
        GridNearTxPrepareRequest req) {
        if (e == null)
            e = new IgniteTxTimeoutCheckedException("Failed to wait topology version for near prepare " +
                "[txId=" + req.version() +
                ", topVer=" + topFut.initialVersion() +
                ", node=" + node.id() +
                ", req=" + req + ']');

        GridNearTxPrepareResponse res = new GridNearTxPrepareResponse(
            req.partition(),
            req.version(),
            req.futureId(),
            req.miniId(),
            req.version(),
            req.version(),
            null,
            e,
            null,
            req.onePhaseCommit(),
            req.deployInfo() != null);

        try {
            ctx.io().send(node.id(), res, req.policy());
        }
        catch (IgniteCheckedException e0) {
            U.error(txPrepareMsgLog, "Failed to send wait topology version response for near prepare " +
                "[txId=" + req.version() +
                ", topVer=" + topFut.initialVersion() +
                ", node=" + node.id() +
                ", req=" + req + ']', e0);
        }
    }

    /**
     * @param rmtVer Topology version that the cache request was mapped to by the remote node.
     * @param req Request.
     * @return {@code True} if cache affinity changed and request should be remapped.
     */
    private boolean needRemap(AffinityTopologyVersion rmtVer, GridNearTxPrepareRequest req) {
        // TODO IGNITE-6754 check mvcc crd for mvcc enabled txs.

        for (IgniteTxEntry e : F.concat(false, req.reads(), req.writes())) {
            if (!e.context().affinity().isCompatibleWithCurrentTopologyVersion(rmtVer))
                return true;
        }

        return false;
    }

    /**
     * @param nodeId Node ID.
     * @param res Response.
     */
    private void processNearTxPrepareResponse(UUID nodeId, GridNearTxPrepareResponse res) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_NEAR_PREPARE_RESP, MTC.span()))) {
            if (txPrepareMsgLog.isDebugEnabled())
                txPrepareMsgLog.debug("Received near prepare response [txId=" + res.version() + ", node=" +
                    nodeId + ']');

            GridNearTxPrepareFutureAdapter fut = (GridNearTxPrepareFutureAdapter)ctx.mvcc()
                .versionedFuture(res.version(), res.futureId());

            if (fut == null) {
                U.warn(log, "Failed to find future for near prepare response [txId=" + res.version() +
                    ", node=" + nodeId +
                    ", res=" + res + ']');

                return;
            }

            IgniteInternalTx tx = fut.tx();

            assert tx != null;

            res.txState(tx.txState());

            fut.onResult(nodeId, res);
        }
    }

    /**
     * @param nodeId Node ID.
     * @param res Response.
     */
    private void processNearTxFinishResponse(UUID nodeId, GridNearTxFinishResponse res) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_NEAR_FINISH_RESP, MTC.span()))) {
            if (txFinishMsgLog.isDebugEnabled())
                txFinishMsgLog.debug("Received near finish response [txId=" + res.xid() + ", node=" + nodeId + ']');

            GridNearTxFinishFuture fut = (GridNearTxFinishFuture)ctx.mvcc().future(res.futureId());

            if (fut == null) {
                if (txFinishMsgLog.isDebugEnabled()) {
                    txFinishMsgLog.debug("Failed to find future for near finish response [txId=" + res.xid() +
                        ", node=" + nodeId +
                        ", res=" + res + ']');
                }

                return;
            }

            fut.onResult(nodeId, res);
        }
    }

    /**
     * @param nodeId Node ID.
     * @param res Response.
     */
    private void processDhtTxPrepareResponse(UUID nodeId, GridDhtTxPrepareResponse res) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_PROCESS_DHT_PREPARE_RESP, MTC.span()))) {
            GridDhtTxPrepareFuture fut =
                (GridDhtTxPrepareFuture)ctx.mvcc().versionedFuture(res.version(), res.futureId());

            if (fut == null) {
                if (txPrepareMsgLog.isDebugEnabled()) {
                    txPrepareMsgLog.debug("Failed to find future for dht prepare response [txId=null" +
                        ", dhtTxId=" + res.version() +
                        ", node=" + nodeId +
                        ", res=" + res + ']');
                }

                return;
            }
            else if (txPrepareMsgLog.isDebugEnabled())
                txPrepareMsgLog.debug("Received dht prepare response [txId=" + fut.tx().nearXidVersion() +
                    ", node=" + nodeId + ']');

            IgniteInternalTx tx = fut.tx();

            assert tx != null;

            res.txState(tx.txState());

            fut.onResult(nodeId, res);
        }
    }

    /**
     * @param nodeId Node ID.
     * @param res Response.
     */
    private void processDhtTxFinishResponse(UUID nodeId, GridDhtTxFinishResponse res) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_PROCESS_DHT_FINISH_RESP, MTC.span()))) {
            assert nodeId != null;
            assert res != null;

            if (res.checkCommitted()) {
                GridNearTxFinishFuture fut = (GridNearTxFinishFuture)ctx.mvcc().future(res.futureId());

                if (fut == null) {
                    if (txFinishMsgLog.isDebugEnabled()) {
                        txFinishMsgLog.debug("Failed to find future for dht finish check committed response [txId=null" +
                            ", dhtTxId=" + res.xid() +
                            ", node=" + nodeId +
                            ", res=" + res + ']');
                    }

                    return;
                }
                else if (txFinishMsgLog.isDebugEnabled()) {
                    txFinishMsgLog.debug("Received dht finish check committed response [txId=" + fut.tx().nearXidVersion() +
                        ", dhtTxId=" + res.xid() +
                        ", node=" + nodeId + ']');
                }

                fut.onResult(nodeId, res);
            }
            else {
                GridDhtTxFinishFuture fut = (GridDhtTxFinishFuture)ctx.mvcc().future(res.futureId());

                if (fut == null) {
                    if (txFinishMsgLog.isDebugEnabled()) {
                        txFinishMsgLog.debug("Failed to find future for dht finish response [txId=null" +
                            ", dhtTxId=" + res.xid() +
                            ", node=" + nodeId +
                            ", res=" + res);
                    }

                    return;
                }
                else if (txFinishMsgLog.isDebugEnabled()) {
                    txFinishMsgLog.debug("Received dht finish response [txId=" + fut.tx().nearXidVersion() +
                        ", dhtTxId=" + res.xid() +
                        ", node=" + nodeId + ']');
                }

                fut.onResult(nodeId, res);
            }
        }
    }

    /**
     * @param nodeId Node ID.
     * @param req Request.
     */
    private void processNearTxFinishRequest(UUID nodeId, GridNearTxFinishRequest req) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_NEAR_FINISH_REQ, MTC.span()))) {
            if (txFinishMsgLog.isDebugEnabled())
                txFinishMsgLog.debug("Received near finish request [txId=" + req.version() + ", node=" + nodeId +
                    ']');

            IgniteInternalFuture<IgniteInternalTx> fut = finish(nodeId, null, req);

            assert req.txState() != null || fut == null || fut.error() != null ||
                (ctx.tm().tx(req.version()) == null && ctx.tm().nearTx(req.version()) == null) :
                "[req=" + req + ", fut=" + fut + "]";
        }
    }

    /**
     * @param nodeId Node ID.
     * @param locTx Local transaction.
     * @param req Request.
     * @return Future.
     */
    @Nullable public IgniteInternalFuture<IgniteInternalTx> finish(UUID nodeId,
        @Nullable GridNearTxLocal locTx,
        GridNearTxFinishRequest req
    ) {
        assert nodeId != null;
        assert req != null;

        if (locTx != null)
            req.txState(locTx.txState());

        // Always add near version to rollback history to prevent races with rollbacks.
        if (!req.commit())
            ctx.tm().addRolledbackTx(null, req.version());

        // Transaction on local cache only.
        if (locTx != null && !locTx.nearLocallyMapped() && !locTx.colocatedLocallyMapped())
            return new GridFinishedFuture<>(locTx);

        if (log.isDebugEnabled())
            log.debug("Processing near tx finish request [nodeId=" + nodeId + ", req=" + req + "]");

        IgniteInternalFuture<IgniteInternalTx> colocatedFinishFut = null;

        if (locTx != null && locTx.colocatedLocallyMapped())
            colocatedFinishFut = finishColocatedLocal(req.commit(), locTx);

        IgniteInternalFuture<IgniteInternalTx> nearFinishFut = null;

        if (locTx == null || locTx.nearLocallyMapped())
            nearFinishFut = finishDhtLocal(nodeId, locTx, req);

        if (colocatedFinishFut != null && nearFinishFut != null) {
            GridCompoundFuture<IgniteInternalTx, IgniteInternalTx> res = new GridCompoundFuture<>();

            res.add(colocatedFinishFut);
            res.add(nearFinishFut);

            res.markInitialized();

            return res;
        }

        if (colocatedFinishFut != null)
            return colocatedFinishFut;

        return nearFinishFut;
    }

    /**
     * @param nodeId Node ID initiated commit.
     * @param locTx Optional local transaction.
     * @param req Finish request.
     * @return Finish future.
     */
    private IgniteInternalFuture<IgniteInternalTx> finishDhtLocal(UUID nodeId,
        @Nullable GridNearTxLocal locTx,
        GridNearTxFinishRequest req
    ) {
        GridCacheVersion dhtVer = ctx.tm().mappedVersion(req.version());

        GridDhtTxLocal tx = null;

        if (dhtVer == null) {
            if (log.isDebugEnabled())
                log.debug("Received transaction finish request for unknown near version (was lock explicit?): " + req);
        }
        else
            tx = ctx.tm().tx(dhtVer);

        if (tx != null) {
            tx.mvccSnapshot(req.mvccSnapshot());

            req.txState(tx.txState());
        }

        if (tx == null && locTx != null && !req.commit()) {
            U.warn(log, "DHT local tx not found for near local tx rollback " +
                "[req=" + req + ", dhtVer=" + dhtVer + ", tx=" + locTx + ']');

            return null;
        }

        if (tx == null && !req.explicitLock()) {
            assert locTx == null : "DHT local tx should never be lost for near local tx: " + locTx;

            U.warn(txFinishMsgLog, "Received finish request for completed transaction (the message may be too late) [" +
                "txId=" + req.version() +
                ", dhtTxId=" + dhtVer +
                ", node=" + nodeId +
                ", commit=" + req.commit() + ']');

            // Always send finish response.
            GridCacheMessage res = new GridNearTxFinishResponse(
                req.partition(),
                req.version(),
                req.threadId(),
                req.futureId(),
                req.miniId(),
                new IgniteTxRollbackCheckedException("Transaction has been already completed or not started yet."));

            try {
                ctx.io().send(nodeId, res, req.policy());

                if (txFinishMsgLog.isDebugEnabled()) {
                    txFinishMsgLog.debug("Sent near finish response for completed tx [txId=" + req.version() +
                        ", dhtTxId=" + dhtVer +
                        ", node=" + nodeId + ']');
                }
            }
            catch (Throwable e) {
                // Double-check.
                if (ctx.discovery().node(nodeId) == null) {
                    if (txFinishMsgLog.isDebugEnabled()) {
                        txFinishMsgLog.debug("Failed to send near finish response for completed tx, node failed [" +
                            "txId=" + req.version() +
                            ", dhtTxId=" + dhtVer +
                            ", node=" + nodeId + ']');
                    }
                }
                else {
                    U.error(txFinishMsgLog, "Failed to send near finish response for completed tx, node failed [" +
                        "txId=" + req.version() +
                        ", dhtTxId=" + dhtVer +
                        ", node=" + nodeId +
                        ", req=" + req +
                        ", res=" + res + ']', e);
                }

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

            return null;
        }

        try {
            assert tx != null : "Transaction is null for near finish request [nodeId=" +
                nodeId + ", req=" + req + "]";
            assert req.syncMode() != null : req;

            tx.syncMode(req.syncMode());
            tx.nearFinishFutureId(req.futureId());
            tx.nearFinishMiniId(req.miniId());
            tx.storeEnabled(req.storeEnabled());

            if (locTx != null)
                tx.incrementalSnapshotId(locTx.incrementalSnapshotId());

            if (!tx.markFinalizing(USER_FINISH)) {
                if (log.isDebugEnabled())
                    log.debug("Will not finish transaction (it is handled by another thread) [commit=" + req.commit() + ", tx=" + tx + ']');

                return null;
            }

            if (req.commit()) {
                IgniteInternalFuture<IgniteInternalTx> commitFut = tx.commitDhtLocalAsync();

                // Only for error logging.
                commitFut.listen(CU.errorLogger(log));

                return commitFut;
            }
            else {
                IgniteInternalFuture<IgniteInternalTx> rollbackFut = tx.rollbackDhtLocalAsync();

                // Only for error logging.
                rollbackFut.listen(CU.errorLogger(log));

                return rollbackFut;
            }
        }
        catch (Throwable e) {
            try {
                if (tx != null) {
                    tx.commitError(e);

                    tx.systemInvalidate(true);

                    try {
                        IgniteInternalFuture<IgniteInternalTx> res = tx.rollbackDhtLocalAsync();

                        // Only for error logging.
                        res.listen(CU.errorLogger(log));

                        return res;
                    }
                    catch (Throwable e1) {
                        e.addSuppressed(e1);
                    }

                    tx.logTxFinishErrorSafe(log, req.commit(), e);
                }

                if (e instanceof Error)
                    throw (Error)e;

                return new GridFinishedFuture<>(e);
            }
            finally {
                ctx.kernalContext().failure().process(new FailureContext(FailureType.CRITICAL_ERROR, e));
            }
        }
    }

    /**
     * @param commit Commit flag (rollback if {@code false}).
     * @param tx Transaction to commit.
     * @return Future.
     */
    public IgniteInternalFuture<IgniteInternalTx> finishColocatedLocal(boolean commit, GridNearTxLocal tx) {
        try {
            if (commit) {
                if (!tx.markFinalizing(USER_FINISH)) {
                    if (log.isDebugEnabled())
                        log.debug("Will not finish transaction (it is handled by another thread): " + tx);

                    return null;
                }

                return tx.commitAsyncLocal();
            }
            else
                return tx.rollbackAsyncLocal();
        }
        catch (Throwable e) {
            try {
                if (tx != null) {
                    try {
                        return tx.rollbackNearTxLocalAsync();
                    }
                    catch (Throwable e1) {
                        e.addSuppressed(e1);
                    }

                    tx.logTxFinishErrorSafe(log, commit, e);
                }

                if (e instanceof Error)
                    throw e;

                return new GridFinishedFuture<>(e);
            }
            finally {
                ctx.kernalContext().failure().process(new FailureContext(FailureType.CRITICAL_ERROR, e));
            }
        }
    }

    /**
     * @param nodeId Sender node ID.
     * @param req Request.
     */
    private void processDhtTxPrepareRequest(final UUID nodeId, final GridDhtTxPrepareRequest req) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_PROCESS_DHT_PREPARE_REQ, MTC.span()))) {
            if (txPrepareMsgLog.isDebugEnabled()) {
                txPrepareMsgLog.debug("Received dht prepare request [txId=" + req.nearXidVersion() +
                    ", dhtTxId=" + req.version() +
                    ", node=" + nodeId + ']');
            }

            assert nodeId != null;
            assert req != null;

            assert req.transactionNodes() != null;

            GridDhtTxRemote dhtTx = null;
            GridNearTxRemote nearTx = null;

            GridDhtTxPrepareResponse res;

            try {
                res = new GridDhtTxPrepareResponse(
                    req.partition(),
                    req.version(),
                    req.futureId(),
                    req.miniId(),
                    req.deployInfo() != null);

                // Start near transaction first.
                nearTx = !F.isEmpty(req.nearWrites()) ? startNearRemoteTx(ctx.deploy().globalLoader(), nodeId, req) : null;
                dhtTx = startRemoteTx(nodeId, req, res);

                // Set evicted keys from near transaction.
                if (nearTx != null)
                    res.nearEvicted(nearTx.evicted());

                List<IgniteTxKey> writesCacheMissed = req.nearWritesCacheMissed();

                if (writesCacheMissed != null) {
                    Collection<IgniteTxKey> evicted0 = res.nearEvicted();

                    if (evicted0 != null)
                        writesCacheMissed.addAll(evicted0);

                    res.nearEvicted(writesCacheMissed);
                }

                if (dhtTx != null)
                    req.txState(dhtTx.txState());
                else if (nearTx != null)
                    req.txState(nearTx.txState());

                if (dhtTx != null && !F.isEmpty(dhtTx.invalidPartitions()))
                    res.invalidPartitionsByCacheId(dhtTx.invalidPartitions());

                if (req.onePhaseCommit()) {
                    assert req.last();

                    if (dhtTx != null) {
                        dhtTx.onePhaseCommit(true);
                        dhtTx.needReturnValue(req.needReturnValue());
                        dhtTx.incrementalSnapshotId(ctx.snapshotMgr().incrementalSnapshotId());

                        finish(dhtTx, req);
                    }

                    if (nearTx != null) {
                        nearTx.onePhaseCommit(true);

                        finish(nearTx, req);
                    }
                }
            }
            catch (IgniteCheckedException e) {
                if (e instanceof IgniteTxRollbackCheckedException)
                    U.error(log, "Transaction was rolled back before prepare completed: " + req, e);
                else if (e instanceof IgniteTxOptimisticCheckedException) {
                    if (log.isDebugEnabled())
                        log.debug("Optimistic failure for remote transaction (will rollback): " + req);
                }
                else
                    U.error(log, "Failed to process prepare request: " + req, e);

                if (nearTx != null)
                    try {
                        nearTx.rollbackRemoteTx();
                    }
                    catch (Throwable e1) {
                        e.addSuppressed(e1);
                    }

                res = new GridDhtTxPrepareResponse(
                    req.partition(),
                    req.version(),
                    req.futureId(),
                    req.miniId(),
                    e,
                    req.deployInfo() != null);
            }

            if (req.onePhaseCommit()) {
                IgniteInternalFuture<IgniteInternalTx> completeFut = completeFuture(dhtTx, nearTx);

                if (completeFut != null) {
                    final GridDhtTxPrepareResponse res0 = res;
                    final GridDhtTxRemote dhtTx0 = dhtTx;
                    final GridNearTxRemote nearTx0 = nearTx;

                    completeFut.listen(() ->
                        sendReply(nodeId, req, res0, dhtTx0, nearTx0));
                }
                else
                    sendReply(nodeId, req, res, dhtTx, nearTx);
            }
            else
                sendReply(nodeId, req, res, dhtTx, nearTx);

            assert req.txState() != null || res.error() != null || (dhtTx == null && nearTx == null) :
                req + " tx=" + dhtTx + " nearTx=" + nearTx;
        }
    }

    /**
     * @param dhtTx Dht tx.
     * @param nearTx Near tx.
     */
    private IgniteInternalFuture<IgniteInternalTx> completeFuture(GridDhtTxRemote dhtTx, GridNearTxRemote nearTx) {
        IgniteInternalFuture<IgniteInternalTx> dhtFin =
            dhtTx == null ? null : dhtTx.done() ? null : dhtTx.finishFuture();

        final IgniteInternalFuture<IgniteInternalTx> nearFin =
            nearTx == null ? null : nearTx.done() ? null : nearTx.finishFuture();

        if (dhtFin != null && nearFin != null) {
            GridCompoundFuture<IgniteInternalTx, IgniteInternalTx> fut = new GridCompoundFuture<>();

            fut.add(dhtFin);
            fut.add(nearFin);

            fut.markInitialized();

            return fut;
        }
        else
            return dhtFin != null ? dhtFin : nearFin;
    }

    /**
     * @param nodeId Node ID.
     * @param req Request.
     */
    private void processDhtTxOnePhaseCommitAckRequest(final UUID nodeId,
        final GridDhtTxOnePhaseCommitAckRequest req) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_PROCESS_DHT_ONE_PHASE_COMMIT_ACK_REQ, MTC.span()))) {
            assert nodeId != null;
            assert req != null;

            if (log.isDebugEnabled())
                log.debug("Processing dht tx one phase commit ack request [nodeId=" + nodeId + ", req=" + req + ']');

            for (GridCacheVersion ver : req.versions())
                ctx.tm().removeTxReturn(ver);
        }
    }

    /**
     * @param nodeId Node ID.
     * @param req Request.
     */
    private void processDhtTxFinishRequest(final UUID nodeId, final GridDhtTxFinishRequest req) {
        try (TraceSurroundings ignored =
                 MTC.support(ctx.kernalContext().tracing().create(TX_PROCESS_DHT_FINISH_REQ, MTC.span()))) {
            assert nodeId != null;
            assert req != null;

            if (req.checkCommitted()) {
                boolean committed = req.waitRemoteTransactions() || !ctx.tm().addRolledbackTx(null, req.version());

                if (!committed || req.syncMode() != FULL_SYNC)
                    sendReply(nodeId, req, committed, null);
                else {
                    IgniteInternalFuture<?> fut = ctx.tm().remoteTxFinishFuture(req.version());

                    fut.listen(() -> sendReply(nodeId, req, true, null));
                }

                return;
            }

            // Always add version to rollback history to prevent races with rollbacks.
            if (!req.commit())
                ctx.tm().addRolledbackTx(null, req.version());

            GridDhtTxRemote dhtTx = ctx.tm().tx(req.version());
            GridNearTxRemote nearTx = ctx.tm().nearTx(req.version());

            IgniteInternalTx anyTx = U.<IgniteInternalTx>firstNotNull(dhtTx, nearTx);

            final GridCacheVersion nearTxId = anyTx != null ? anyTx.nearXidVersion() : null;

            if (txFinishMsgLog.isDebugEnabled())
                txFinishMsgLog.debug("Received dht finish request [txId=" + nearTxId + ", dhtTxId=" + req.version() +
                    ", node=" + nodeId + ']');

            if (anyTx == null && req.commit())
                ctx.tm().addCommittedTx(null, req.version(), null);

            if (dhtTx != null)
                finish(dhtTx, req);
            else {
                try {
                    applyPartitionsUpdatesCounters(req.updateCounters(), !req.commit(), false);
                }
                catch (IgniteCheckedException e) {
                    throw new IgniteException(e);
                }
            }

            if (nearTx != null)
                finish(nearTx, req);

            if (req.replyRequired()) {
                IgniteInternalFuture<IgniteInternalTx> completeFut = completeFuture(dhtTx, nearTx);

                if (completeFut != null) {
                    completeFut.listen(() ->
                        sendReply(nodeId, req, true, nearTxId));
                }
                else
                    sendReply(nodeId, req, true, nearTxId);
            }
            else
                sendReply(nodeId, req, true, null);

            assert req.txState() != null || (dhtTx == null && nearTx == null) : req + " tx=" + dhtTx + " nearTx=" + nearTx;
        }
    }

    /**
     * @param tx Transaction.
     * @param req Request.
     */
    protected void finish(IgniteTxRemoteEx tx, GridDhtTxFinishRequest req) {
        assert tx != null;

        req.txState(tx.txState());

        try {
            if (req.commit() || req.isSystemInvalidate()) {
                tx.commitVersion(req.commitVersion());
                if (req.isInvalidate())
                    tx.invalidate(true);
                if (req.isSystemInvalidate())
                    tx.systemInvalidate(true);
                tx.mvccSnapshot(req.mvccSnapshot());

                // Complete remote candidates.
                tx.doneRemote(req.baseVersion(), null, null, null);

                tx.commitRemoteTx();
            }
            else {
                if (tx.dht() && req.updateCounters() != null)
                    tx.txCounters(true).updateCounters(req.updateCounters());

                tx.doneRemote(req.baseVersion(), null, null, null);
                tx.mvccSnapshot(req.mvccSnapshot());
                tx.rollbackRemoteTx();
            }
        }
        catch (IgniteTxHeuristicCheckedException ignore) {
            // Already uncommitted.
        }
        catch (Throwable e) {
            // Mark transaction for invalidate.
            tx.invalidate(true);
            tx.systemInvalidate(true);

            try {
                tx.commitRemoteTx();
            }
            catch (IgniteCheckedException ex) {
                U.error(log, "Failed to invalidate transaction: " + tx, ex);
            }

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

    /**
     * Finish for one-phase distributed tx.
     *
     * @param tx Transaction.
     * @param req Request.
     */
    protected void finish(
        GridDistributedTxRemoteAdapter tx,
        GridDhtTxPrepareRequest req) throws IgniteTxHeuristicCheckedException {
        assert tx != null : "No transaction for one-phase commit prepare request: " + req;

        try {
            tx.commitVersion(req.writeVersion());
            tx.invalidate(req.isInvalidate());
            tx.mvccSnapshot(req.mvccSnapshot());

            // Complete remote candidates.
            tx.doneRemote(req.version(), null, null, null);

            tx.commitRemoteTx();
        }
        catch (IgniteTxHeuristicCheckedException e) {
            // Just rethrow this exception. Transaction was already uncommitted.
            throw e;
        }
        catch (Throwable e) {
            try {
                // Mark transaction for invalidate.
                tx.invalidate(true);
                tx.systemInvalidate(true);

                try {
                    tx.rollbackRemoteTx();
                }
                catch (Throwable e1) {
                    e.addSuppressed(e1);
                }

                tx.logTxFinishErrorSafe(log, true, e);

                if (e instanceof Error)
                    throw (Error)e;
            }
            finally {
                ctx.kernalContext().failure().process(new FailureContext(FailureType.CRITICAL_ERROR, e));
            }
        }
    }

    /**
     * @param nodeId Node id.
     * @param req Request.
     * @param res Response.
     * @param dhtTx Dht tx.
     * @param nearTx Near tx.
     */
    private void sendReply(UUID nodeId,
        GridDhtTxPrepareRequest req,
        GridDhtTxPrepareResponse res,
        GridDhtTxRemote dhtTx,
        GridNearTxRemote nearTx) {
        try {
            IgniteTxAdapter tx = dhtTx != null ? dhtTx : nearTx;

            // Reply back to sender.
            ctx.tm().sendTransactionMessage(nodeId, res, tx, req.policy());

            if (txPrepareMsgLog.isDebugEnabled()) {
                txPrepareMsgLog.debug("Sent dht prepare response [txId=" + req.nearXidVersion() +
                    ", dhtTxId=" + req.version() +
                    ", node=" + nodeId + ']');
            }
        }
        catch (IgniteCheckedException e) {
            if (e instanceof ClusterTopologyCheckedException) {
                if (txPrepareMsgLog.isDebugEnabled()) {
                    txPrepareMsgLog.debug("Failed to send dht prepare response, node left [txId=" + req.nearXidVersion() +
                        ", dhtTxId=" + req.version() +
                        ", node=" + nodeId + ']');
                }
            }
            else {
                U.warn(log, "Failed to send tx response to remote node (will rollback transaction) [" +
                    "txId=" + req.nearXidVersion() +
                    ", dhtTxId=" + req.version() +
                    ", node=" + nodeId +
                    ", err=" + e.getMessage() + ']');
            }

            if (nearTx != null)
                try {
                    nearTx.rollbackRemoteTx();
                }
                catch (Throwable e1) {
                    e.addSuppressed(e1);
                }

            if (dhtTx != null)
                try {
                    dhtTx.rollbackRemoteTx();
                }
                catch (Throwable e1) {
                    e.addSuppressed(e1);
                }
        }
    }

    /**
     * Sends tx finish response to remote node, if response is requested.
     *
     * @param nodeId Node id that originated finish request.
     * @param req Request.
     * @param committed {@code True} if transaction committed on this node.
     * @param nearTxId Near tx version.
     */
    private void sendReply(UUID nodeId, GridDhtTxFinishRequest req, boolean committed, GridCacheVersion nearTxId) {
        if (req.replyRequired() || req.checkCommitted()) {
            GridDhtTxFinishResponse res = new GridDhtTxFinishResponse(
                req.partition(),
                req.version(),
                req.futureId(),
                req.miniId());

            if (req.checkCommitted()) {
                res.checkCommitted(true);

                if (committed) {
                    if (req.needReturnValue()) {
                        try {
                            GridCacheReturnCompletableWrapper wrapper = ctx.tm().getCommittedTxReturn(req.version());

                            if (wrapper != null)
                                res.returnValue(wrapper.fut().get());
                            else
                                assert !ctx.discovery().alive(nodeId) : nodeId;
                        }
                        catch (IgniteCheckedException ignored) {
                            if (txFinishMsgLog.isDebugEnabled()) {
                                txFinishMsgLog.debug("Failed to gain entry processor return value. [txId=" + nearTxId +
                                    ", dhtTxId=" + req.version() +
                                    ", node=" + nodeId + ']');
                            }
                        }
                    }
                }
                else {
                    ClusterTopologyCheckedException cause =
                        new ClusterTopologyCheckedException("Primary node left grid.");

                    res.checkCommittedError(new IgniteTxRollbackCheckedException("Failed to commit transaction " +
                        "(transaction has been rolled back on backup node): " + req.version(), cause));
                }
            }

            try {
                ctx.io().send(nodeId, res, req.policy());

                if (txFinishMsgLog.isDebugEnabled()) {
                    txFinishMsgLog.debug("Sent dht tx finish response [txId=" + nearTxId +
                        ", dhtTxId=" + req.version() +
                        ", node=" + nodeId +
                        ", checkCommitted=" + req.checkCommitted() + ']');
                }
            }
            catch (Throwable e) {
                // Double-check.
                if (ctx.discovery().node(nodeId) == null) {
                    if (txFinishMsgLog.isDebugEnabled()) {
                        txFinishMsgLog.debug("Node left while send dht tx finish response [txId=" + nearTxId +
                            ", dhtTxId=" + req.version() +
                            ", node=" + nodeId + ']');
                    }
                }
                else {
                    U.error(log, "Failed to send finish response to node [txId=" + nearTxId +
                        ", dhtTxId=" + req.version() +
                        ", nodeId=" + nodeId +
                        ", res=" + res + ']', e);
                }

                if (e instanceof Error)
                    throw (Error)e;
            }
        }
        else {
            if (txFinishMsgLog.isDebugEnabled()) {
                txFinishMsgLog.debug("Skip send dht tx finish response [txId=" + nearTxId +
                    ", dhtTxId=" + req.version() +
                    ", node=" + nodeId + ']');
            }
        }
    }

    /**
     * @param nodeId Node ID.
     * @param req Request.
     * @param res Response.
     * @return Remote transaction.
     * @throws IgniteCheckedException If failed.
     */
    @Nullable GridDhtTxRemote startRemoteTx(
        UUID nodeId,
        GridDhtTxPrepareRequest req,
        GridDhtTxPrepareResponse res
    ) throws IgniteCheckedException {
        if (req.queryUpdate() || !F.isEmpty(req.writes())) {
            GridDhtTxRemote tx = ctx.tm().tx(req.version());

            if (tx == null) {
                boolean single = req.last() && req.writes().size() == 1;

                tx = new GridDhtTxRemote(
                    ctx,
                    req.nearNodeId(),
                    nodeId,
                    req.topologyVersion(),
                    req.version(),
                    null,
                    req.system(),
                    req.policy(),
                    req.concurrency(),
                    req.isolation(),
                    req.isInvalidate(),
                    req.timeout(),
                    req.writes() != null ? Math.max(req.writes().size(), req.txSize()) : req.txSize(),
                    req.nearXidVersion(),
                    req.transactionNodes(),
                    securitySubjectId(ctx),
                    req.taskNameHash(),
                    single,
                    req.storeWriteThrough(),
                    req.txLabel());

                tx.onePhaseCommit(req.onePhaseCommit());
                tx.writeVersion(req.writeVersion());

                tx = ctx.tm().onCreated(null, tx);

                if (tx == null || !ctx.tm().onStarted(tx)) {
                    if (log.isDebugEnabled())
                        log.debug("Attempt to start a completed transaction (will ignore): " + tx);

                    applyPartitionsUpdatesCounters(req.updateCounters(), true, false);

                    return null;
                }

                if (ctx.discovery().node(nodeId) == null) {
                    tx.state(ROLLING_BACK);

                    tx.state(ROLLED_BACK);

                    ctx.tm().uncommitTx(tx);

                    applyPartitionsUpdatesCounters(req.updateCounters(), true, false);

                    return null;
                }

                ctx.versions().onReceived(nodeId, req.writeVersion());
            }
            else {
                tx.writeVersion(req.writeVersion());
                tx.transactionNodes(req.transactionNodes());
            }

            TxCounters txCounters = null;

            if (req.updateCounters() != null) {
                txCounters = tx.txCounters(true);

                txCounters.updateCounters(req.updateCounters());
            }

            Set<GridDhtLocalPartition> reservedParts = new HashSet<>();

            try {
                if (!tx.isSystemInvalidate()) {
                    int idx = 0;

                    for (IgniteTxEntry entry : req.writes()) {
                        GridCacheContext cacheCtx = entry.context();

                        int part = cacheCtx.affinity().partition(entry.key());

                        try {
                            GridDhtLocalPartition locPart = cacheCtx.topology().localPartition(part,
                                req.topologyVersion(),
                                false);

                            // Avoid enlisting to invalid partition.
                            boolean reserved = locPart != null && reservedParts.contains(locPart);

                            if (!reserved) {
                                if ((reserved = locPart != null && locPart.reserve()))
                                    reservedParts.add(locPart);
                            }

                            if (reserved) {
                                tx.addWrite(entry, ctx.deploy().globalLoader());

                                if (txCounters != null && entry.op() != NOOP && !(entry.op() == TRANSFORM && entry.noop())) {
                                    Long cntr = txCounters.generateNextCounter(entry.cacheId(), part);

                                    if (cntr != null) // Counter is null if entry is no-op.
                                        entry.updateCounter(cntr);
                                }

                                if (isNearEnabled(cacheCtx) && req.invalidateNearEntry(idx))
                                    invalidateNearEntry(cacheCtx, entry.key(), req.version());

                                if (req.needPreloadKey(idx)) {
                                    GridCacheEntryEx cached = entry.cached();

                                    if (cached == null)
                                        cached = cacheCtx.cache().entryEx(entry.key(), req.topologyVersion());

                                    GridCacheEntryInfo info = cached.info();

                                    if (info != null && !info.isNew() && !info.isDeleted())
                                        res.addPreloadEntry(info);
                                }

                                if (cacheCtx.readThroughConfigured() &&
                                    !entry.skipStore() &&
                                    entry.op() == TRANSFORM &&
                                    entry.oldValueOnPrimary() &&
                                    !entry.hasValue()) {
                                    while (true) {
                                        try {
                                            GridCacheEntryEx cached = entry.cached();

                                            if (cached == null) {
                                                cached = cacheCtx.cache().entryEx(entry.key(), req.topologyVersion());

                                                entry.cached(cached);
                                            }

                                            CacheObject val = cached.innerGet(
                                                /*ver*/null,
                                                tx,
                                                /*readThrough*/false,
                                                /*updateMetrics*/false,
                                                /*evt*/false,
                                                /*transformClo*/null,
                                                tx.resolveTaskName(),
                                                /*expiryPlc*/null,
                                                /*keepBinary*/true);

                                            if (val == null)
                                                val = cacheCtx.toCacheObject(cacheCtx.store().load(null, entry.key()));

                                            if (val != null)
                                                entry.readValue(val);

                                            break;
                                        }
                                        catch (GridCacheEntryRemovedException ignored) {
                                            if (log.isDebugEnabled())
                                                log.debug("Got entry removed exception, will retry: " + entry.txKey());

                                            entry.cached(cacheCtx.cache().entryEx(entry.key(), req.topologyVersion()));
                                        }
                                    }
                                }
                            }
                            else
                                tx.addInvalidPartition(cacheCtx.cacheId(), part);
                        }
                        catch (GridDhtInvalidPartitionException e) {
                            tx.addInvalidPartition(cacheCtx.cacheId(), part);
                        }

                        idx++;
                    }
                }

                // Prepare prior to reordering, so the pending locks added
                // in prepare phase will get properly ordered as well.
                tx.prepareRemoteTx();
            }
            finally {
                reservedParts.forEach(GridDhtLocalPartition::release);
            }

            if (req.last()) {
                assert !F.isEmpty(req.transactionNodes()) :
                    "Received last prepare request with empty transaction nodes: " + req;

                tx.state(PREPARED);
            }

            res.invalidPartitionsByCacheId(tx.invalidPartitions());

            if (!req.queryUpdate() && tx.empty() && req.last()) {
                tx.skipCompletedVersions(req.skipCompletedVersion());

                tx.rollbackRemoteTx();

                return null;
            }

            return tx;
        }

        return null;
    }

    /**
     * Writes updated values on the backup node.
     *
     * @param tx Transaction.
     * @param ctx Cache context.
     * @param op Operation.
     * @param keys Keys.
     * @param vals Values sent from the primary node.
     * @param snapshot Mvcc snapshot.
     * @param batchNum Batch number.
     * @param futId Future id.
     * @throws IgniteCheckedException If failed.
     */
    public void mvccEnlistBatch(GridDhtTxRemote tx, GridCacheContext ctx, EnlistOperation op, List<KeyCacheObject> keys,
        List<Message> vals, MvccSnapshot snapshot, IgniteUuid futId, int batchNum) throws IgniteCheckedException {
        assert keys != null && (vals == null || vals.size() == keys.size());
        assert tx != null;

        GridDhtCacheAdapter dht = ctx.dht();

        tx.addActiveCache(ctx, false);

        for (int i = 0; i < keys.size(); i++) {
            KeyCacheObject key = keys.get(i);

            assert key != null;

            int part = ctx.affinity().partition(key);

            try {
                GridDhtLocalPartition locPart = ctx.topology().localPartition(part, tx.topologyVersion(), false);

                if (locPart != null && locPart.reserve()) {
                    try {
                        // Skip renting partitions.
                        if (locPart.state() == RENTING) {
                            tx.addInvalidPartition(ctx.cacheId(), part);

                            continue;
                        }

                        CacheObject val = null;
                        EntryProcessor entryProc = null;
                        Object[] invokeArgs = null;

                        boolean needOldVal = tx.txState().useMvccCaching(ctx.cacheId());

                        Message val0 = vals != null ? vals.get(i) : null;

                        CacheEntryInfoCollection entries =
                            val0 instanceof CacheEntryInfoCollection ? (CacheEntryInfoCollection)val0 : null;

                        if (entries == null && !op.isDeleteOrLock() && !op.isInvoke())
                            val = (val0 instanceof CacheObject) ? (CacheObject)val0 : null;

                        if (entries == null && op.isInvoke()) {
                            assert val0 instanceof GridInvokeValue;

                            GridInvokeValue invokeVal = (GridInvokeValue)val0;

                            entryProc = invokeVal.entryProcessor();
                            invokeArgs = invokeVal.invokeArgs();
                        }

                        assert entries != null || entryProc != null || !op.isInvoke() : "entryProc=" + entryProc + ", op=" + op;

                        GridDhtCacheEntry entry = dht.entryExx(key, tx.topologyVersion());

                        GridCacheUpdateTxResult updRes;

                        while (true) {
                            ctx.shared().database().checkpointReadLock();

                            try {
                                if (entries == null) {
                                    switch (op) {
                                        case DELETE:
                                            updRes = entry.mvccRemove(
                                                tx,
                                                ctx.localNodeId(),
                                                tx.topologyVersion(),
                                                snapshot,
                                                false,
                                                needOldVal,
                                                null,
                                                false);

                                            break;

                                        case INSERT:
                                        case TRANSFORM:
                                        case UPSERT:
                                        case UPDATE:
                                            updRes = entry.mvccSet(
                                                tx,
                                                ctx.localNodeId(),
                                                val,
                                                entryProc,
                                                invokeArgs,
                                                0,
                                                tx.topologyVersion(),
                                                snapshot,
                                                op.cacheOperation(),
                                                false,
                                                false,
                                                needOldVal,
                                                null,
                                                false,
                                                false);

                                            break;

                                        default:
                                            throw new IgniteSQLException("Cannot acquire lock for operation [op= "
                                                + op + "]" + "Operation is unsupported at the moment ",
                                                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
                                    }
                                }
                                else {
                                    updRes = entry.mvccUpdateRowsWithPreloadInfo(tx,
                                        ctx.localNodeId(),
                                        tx.topologyVersion(),
                                        entries.infos(),
                                        op.cacheOperation(),
                                        snapshot,
                                        futId,
                                        batchNum);
                                }

                                break;
                            }
                            catch (GridCacheEntryRemovedException ignore) {
                                entry = dht.entryExx(key);
                            }
                            finally {
                                ctx.shared().database().checkpointReadUnlock();
                            }
                        }

                        if (!updRes.filtered())
                            ctx.shared().mvccCaching().addEnlisted(key, updRes.newValue(), 0, 0, tx.xidVersion(),
                                updRes.oldValue(), tx.local(), tx.topologyVersion(), snapshot, ctx.cacheId(), tx, futId, batchNum);

                        assert updRes.updateFuture() == null : "Entry should not be locked on the backup";
                    }

                    finally {
                        locPart.release();
                    }
                }
                else
                    tx.addInvalidPartition(ctx.cacheId(), part);
            }
            catch (GridDhtInvalidPartitionException e) {
                tx.addInvalidPartition(ctx.cacheId(), e.partition());
            }
        }
    }

    /**
     * @param cacheCtx Context.
     * @param key Key
     * @param ver Version.
     * @throws IgniteCheckedException If invalidate failed.
     */
    private void invalidateNearEntry(GridCacheContext cacheCtx, KeyCacheObject key, GridCacheVersion ver)
        throws IgniteCheckedException {
        GridNearCacheAdapter near = cacheCtx.isNear() ? cacheCtx.near() : cacheCtx.dht().near();

        GridCacheEntryEx nearEntry = near.peekEx(key);

        if (nearEntry != null)
            nearEntry.invalidate(ver);
    }

    /**
     * Called while processing dht tx prepare request.
     *
     * @param ldr Loader.
     * @param nodeId Sender node ID.
     * @param req Request.
     * @return Remote transaction.
     * @throws IgniteCheckedException If failed.
     */
    @Nullable private GridNearTxRemote startNearRemoteTx(ClassLoader ldr, UUID nodeId,
        GridDhtTxPrepareRequest req) throws IgniteCheckedException {

        if (!F.isEmpty(req.nearWrites())) {
            GridNearTxRemote tx = ctx.tm().nearTx(req.version());

            if (tx == null) {
                tx = new GridNearTxRemote(
                    ctx,
                    req.topologyVersion(),
                    ldr,
                    nodeId,
                    req.nearNodeId(),
                    req.version(),
                    null,
                    req.system(),
                    req.policy(),
                    req.concurrency(),
                    req.isolation(),
                    req.isInvalidate(),
                    req.timeout(),
                    req.nearWrites(),
                    req.txSize(),
                    securitySubjectId(ctx),
                    req.taskNameHash(),
                    req.txLabel()
                );

                tx.writeVersion(req.writeVersion());

                if (!tx.empty()) {
                    tx = ctx.tm().onCreated(null, tx);

                    if (tx == null || !ctx.tm().onStarted(tx))
                        throw new IgniteTxRollbackCheckedException("Attempt to start a completed transaction: " + tx);
                }
            }
            else
                tx.addEntries(ldr, req.nearWrites());

            tx.ownedVersions(req.owned());

            // Prepare prior to reordering, so the pending locks added
            // in prepare phase will get properly ordered as well.
            tx.prepareRemoteTx();

            if (req.last())
                tx.state(PREPARED);

            return tx;
        }

        return null;
    }

    /**
     * @param nodeId Node ID.
     * @param req Request.
     */
    private void processCheckPreparedTxRequest(final UUID nodeId,
        final GridCacheTxRecoveryRequest req) {
        if (txRecoveryMsgLog.isDebugEnabled()) {
            txRecoveryMsgLog.debug("Received tx recovery request [txId=" + req.nearXidVersion() +
                ", node=" + nodeId + ']');
        }

        IgniteInternalFuture<Boolean> fut = req.nearTxCheck() ? ctx.tm().txCommitted(req.nearXidVersion()) :
            ctx.tm().txsPreparedOrCommitted(req.nearXidVersion(), req.transactions());

        if (fut == null || fut.isDone()) {
            boolean prepared;

            try {
                prepared = fut == null || fut.get();
            }
            catch (IgniteCheckedException e) {
                U.error(log, "Check prepared transaction future failed [req=" + req + ']', e);

                prepared = false;
            }

            sendCheckPreparedResponse(nodeId, req, prepared);
        }
        else {
            fut.listen(() -> {
                boolean prepared;

                try {
                    prepared = fut.get();
                }
                catch (IgniteCheckedException e) {
                    U.error(log, "Check prepared transaction future failed [req=" + req + ']', e);

                    prepared = false;
                }

                sendCheckPreparedResponse(nodeId, req, prepared);
            });
        }
    }

    /**
     * @param nodeId Node ID.
     * @param req Request.
     * @param prepared {@code True} if all transaction prepared or committed.
     */
    private void sendCheckPreparedResponse(UUID nodeId,
        GridCacheTxRecoveryRequest req,
        boolean prepared) {
        GridCacheTxRecoveryResponse res = new GridCacheTxRecoveryResponse(req.version(),
            req.futureId(),
            req.miniId(),
            prepared,
            req.deployInfo() != null);

        try {
            ctx.io().send(nodeId, res, req.system() ? UTILITY_CACHE_POOL : SYSTEM_POOL);

            if (txRecoveryMsgLog.isDebugEnabled()) {
                txRecoveryMsgLog.debug("Sent tx recovery response [txId=" + req.nearXidVersion() +
                    ", node=" + nodeId + ", res=" + res + ']');
            }
        }
        catch (ClusterTopologyCheckedException ignored) {
            if (txRecoveryMsgLog.isDebugEnabled())
                txRecoveryMsgLog.debug("Failed to send tx recovery response, node failed [" +
                    ", txId=" + req.nearXidVersion() +
                    ", node=" + nodeId +
                    ", res=" + res + ']');
        }
        catch (IgniteCheckedException e) {
            U.error(txRecoveryMsgLog, "Failed to send tx recovery response [txId=" + req.nearXidVersion() +
                ", node=" + nodeId +
                ", req=" + req +
                ", res=" + res + ']', e);
        }
    }

    /**
     * @param nodeId Node ID.
     * @param res Response.
     */
    protected void processCheckPreparedTxResponse(UUID nodeId, GridCacheTxRecoveryResponse res) {
        if (txRecoveryMsgLog.isInfoEnabled()) {
            txRecoveryMsgLog.info("Received tx recovery response [txId=" + res.version() +
                ", node=" + nodeId +
                ", res=" + res + ']');
        }

        GridCacheTxRecoveryFuture fut = (GridCacheTxRecoveryFuture)ctx.mvcc().future(res.futureId());

        if (fut == null) {
            if (txRecoveryMsgLog.isInfoEnabled()) {
                txRecoveryMsgLog.info("Failed to find future for tx recovery response [txId=" + res.version() +
                    ", node=" + nodeId + ", res=" + res + ']');
            }

            return;
        }
        else
            res.txState(fut.tx().txState());

        fut.onResult(nodeId, res);
    }

    /**
     * Applies partition counter updates for transactions.
     * <p>
     * Called after entries are written to WAL on commit or during rollback to close gaps in update counter sequence.
     *
     * @param counters Counters.
     */
    public void applyPartitionsUpdatesCounters(Iterable<PartitionUpdateCountersMessage> counters)
        throws IgniteCheckedException {
        applyPartitionsUpdatesCounters(counters, false, false);
    }

    /**
     * Applies partition counter updates for transactions.
     * <p>
     * Called after entries are written to WAL on commit or during rollback to close gaps in update counter sequence.
     * <p>
     * On rollback counters should be applied on the primary only after backup nodes, otherwise if the primary fail
     * before sending rollback requests to backups remote transactions can be committed by recovery protocol and
     * partition consistency will not be restored when primary returns to the grid because RollbackRecord was written
     * (actual for persistent mode only).
     *
     * @param counters Counter values to be updated.
     * @param rollback {@code True} if applied during rollbacks.
     * @param rollbackOnPrimary {@code True} if rollback happens on primary node. Passed to CQ engine.
     */
    public void applyPartitionsUpdatesCounters(
        Iterable<PartitionUpdateCountersMessage> counters,
        boolean rollback,
        boolean rollbackOnPrimary
    ) throws IgniteCheckedException {
        if (counters == null)
            return;

        WALPointer ptr = null;

        try {
            for (PartitionUpdateCountersMessage counter : counters) {
                GridCacheContext ctx0 = ctx.cacheContext(counter.cacheId());

                GridDhtPartitionTopology top = ctx0.topology();

                AffinityTopologyVersion topVer = top.readyTopologyVersion();

                assert top != null;

                for (int i = 0; i < counter.size(); i++) {
                    boolean invalid = false;

                    try {
                        GridDhtLocalPartition part = top.localPartition(counter.partition(i));

                        if (part != null && part.reserve()) {
                            try {
                                if (part.state() != RENTING) { // Check is actual only for backup node.
                                    long start = counter.initialCounter(i);
                                    long delta = counter.updatesCount(i);

                                    boolean updated = part.updateCounter(start, delta);

                                    // Need to log rolled back range for logical recovery.
                                    if (updated && rollback) {
                                        CacheGroupContext grpCtx = part.group();

                                        if (grpCtx.persistenceEnabled() && grpCtx.walEnabled() && !grpCtx.mvccEnabled()) {
                                            RollbackRecord rec =
                                                new RollbackRecord(grpCtx.groupId(), part.id(), start, delta);

                                            ptr = ctx.wal().log(rec);
                                        }

                                        for (int cntr = 1; cntr <= delta; cntr++) {
                                            ctx0.continuousQueries().skipUpdateCounter(null, part.id(), start + cntr,
                                                topVer, rollbackOnPrimary);
                                        }
                                    }
                                }
                                else
                                    invalid = true;
                            }
                            finally {
                                part.release();
                            }
                        }
                        else
                            invalid = true;
                    }
                    catch (GridDhtInvalidPartitionException e) {
                        invalid = true;
                    }

                    if (log.isDebugEnabled() && invalid) {
                        log.debug("Received partition update counters message for invalid partition, ignoring: " +
                            "[cacheId=" + counter.cacheId() + ", part=" + counter.partition(i) + ']');
                    }
                }
            }
        }
        finally {
            if (ptr != null)
                ctx.wal().flush(ptr, false);
        }
    }

    /**
     * @param tx Transaction.
     * @param node Backup node.
     * @return Partition counters for the given backup node.
     */
    @Nullable public List<PartitionUpdateCountersMessage> filterUpdateCountersForBackupNode(
        IgniteInternalTx tx, ClusterNode node) {
        TxCounters txCntrs = tx.txCounters(false);
        Collection<PartitionUpdateCountersMessage> updCntrs;

        if (txCntrs == null || F.isEmpty(updCntrs = txCntrs.updateCounters()))
            return null;

        List<PartitionUpdateCountersMessage> res = new ArrayList<>(updCntrs.size());

        AffinityTopologyVersion top = tx.topologyVersionSnapshot();

        for (PartitionUpdateCountersMessage partCntrs : updCntrs) {
            GridDhtPartitionTopology partTop = ctx.cacheContext(partCntrs.cacheId()).topology();

            PartitionUpdateCountersMessage resCntrs = new PartitionUpdateCountersMessage(partCntrs.cacheId(), partCntrs.size());

            for (int i = 0; i < partCntrs.size(); i++) {
                int part = partCntrs.partition(i);

                if (partTop.nodes(part, top).indexOf(node) > 0)
                    resCntrs.add(part, partCntrs.initialCounter(i), partCntrs.updatesCount(i));
            }

            if (resCntrs.size() > 0)
                res.add(resCntrs);
        }

        return res;
    }
}
