This is an automated email from the ASF dual-hosted git repository.

sk0x50 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new f5f6f77f415 IGNITE-27864 Fix TxIdMismatchException that happened due 
to absent WI cleanup after WI resolution (#7603)
f5f6f77f415 is described below

commit f5f6f77f41556613004013c4656d4039573778d4
Author: Denis Chudov <[email protected]>
AuthorDate: Fri Feb 20 19:39:15 2026 +0200

    IGNITE-27864 Fix TxIdMismatchException that happened due to absent WI 
cleanup after WI resolution (#7603)
---
 .../internal/testframework/IgniteTestUtils.java    |  16 ++-
 .../replicator/PartitionReplicaListener.java       |  57 ++++++--
 ...riteIntentResolutionWhenPrimaryExpiredTest.java | 150 ++++++++++++++++++++-
 .../internal/tx/impl/TransactionStateResolver.java |  15 +++
 .../ignite/internal/tx/impl/TxManagerImpl.java     |  12 +-
 5 files changed, 232 insertions(+), 18 deletions(-)

diff --git 
a/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/IgniteTestUtils.java
 
b/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/IgniteTestUtils.java
index f632b3fdc5c..2cc9da54449 100644
--- 
a/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/IgniteTestUtils.java
+++ 
b/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/IgniteTestUtils.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.internal.testframework;
 
-import static java.lang.Thread.sleep;
 import static java.nio.file.StandardOpenOption.CREATE;
 import static java.nio.file.StandardOpenOption.WRITE;
 import static java.util.function.Function.identity;
@@ -1099,6 +1098,21 @@ public final class IgniteTestUtils {
         return new UUID(str.hashCode(), new 
StringBuilder(str).reverse().toString().hashCode());
     }
 
+    /**
+     * Sleep for a while.
+     *
+     * @param millis Time to sleep in milliseconds.
+     */
+    public static void sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+
+            throw new RuntimeException(e);
+        }
+    }
+
     /**
      * Converts a result set to a list of rows.
      *
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/PartitionReplicaListener.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/PartitionReplicaListener.java
index e0d2b048f5c..a62bb2fee84 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/PartitionReplicaListener.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/PartitionReplicaListener.java
@@ -1684,11 +1684,11 @@ public class PartitionReplicaListener implements 
ReplicaTableProcessor {
 
                 return inBusyLockAsync(busyLock, () ->
                         resolveWriteIntentReadability(writeIntent, ts)
-                                .thenApply(writeIntentReadable ->
+                                .thenApply(wiResolutionResult ->
                                         inBusyLock(busyLock, () -> {
                                             metrics.onRead(true, true);
 
-                                            if (writeIntentReadable) {
+                                            if 
(wiResolutionResult.writeIntentReadable) {
                                                 return findAny(writeIntents, 
wi -> !wi.isEmpty()).map(ReadResult::binaryRow).orElse(null);
                                             } else {
                                                 for (ReadResult wi : 
writeIntents) {
@@ -3391,16 +3391,15 @@ public class PartitionReplicaListener implements 
ReplicaTableProcessor {
     ) {
         return inBusyLockAsync(busyLock, () ->
                 resolveWriteIntentReadability(readResult, timestamp)
-                        .thenApply(writeIntentReadable ->
+                        .thenApply(wiResolutionResult ->
                                 inBusyLock(busyLock, () -> {
-                                            if (writeIntentReadable) {
+                                            if 
(wiResolutionResult.writeIntentReadable) {
                                                 // Even though this readResult 
is still a write intent entry in the storage
                                                 // (therefore it contains 
txId), we already know it relates to a committed transaction
                                                 // and will be cleaned up by 
an asynchronous task
                                                 // started in 
scheduleTransactionRowAsyncCleanup().
                                                 // So it's safe to assume that 
that this is the latest committed entry.
-                                                HybridTimestamp 
commitTimestamp =
-                                                        
txManager.stateMeta(readResult.transactionId()).commitTimestamp();
+                                                HybridTimestamp 
commitTimestamp = wiResolutionResult.transactionMeta.commitTimestamp();
 
                                                 return new 
TimedBinaryRow(readResult.binaryRow(), commitTimestamp);
                                             }
@@ -3470,10 +3469,12 @@ public class PartitionReplicaListener implements 
ReplicaTableProcessor {
      *
      * @param writeIntent Write intent to resolve.
      * @param timestamp Timestamp.
-     * @return The future completes with {@code true} when the transaction is 
committed and commit time <= read time, {@code false}
-     *         otherwise (whe the transaction is either in progress, or 
aborted, or committed and commit time > read time).
+     * @return Write intent resolution result, see {@link 
WriteIntentResolutionResult}.
      */
-    private CompletableFuture<Boolean> 
resolveWriteIntentReadability(ReadResult writeIntent, @Nullable HybridTimestamp 
timestamp) {
+    private CompletableFuture<WriteIntentResolutionResult> 
resolveWriteIntentReadability(
+            ReadResult writeIntent,
+            @Nullable HybridTimestamp timestamp
+    ) {
         UUID txId = writeIntent.transactionId();
 
         HybridTimestamp now = clockService.current();
@@ -3482,19 +3483,35 @@ public class PartitionReplicaListener implements 
ReplicaTableProcessor {
                 ? null
                 : replicaMeta.getStartTime().longValue();
 
+        ZonePartitionId commitPartitionId = new 
ZonePartitionId(writeIntent.commitZoneId(), writeIntent.commitPartitionId());
+
         return transactionStateResolver.resolveTxState(
                         txId,
-                        new ZonePartitionId(writeIntent.commitZoneId(), 
writeIntent.commitPartitionId()),
+                        commitPartitionId,
                         timestamp,
                         currentConsistencyToken,
                         replicationGroupId
                 )
                 .thenApply(transactionMeta -> {
+                    boolean writeIntentReadable = canReadFromWriteIntent(txId, 
txManager, transactionMeta, timestamp);
+
                     if (isFinalState(transactionMeta.txState())) {
                         scheduleAsyncWriteIntentSwitch(txId, 
writeIntent.rowId(), transactionMeta);
+                    } else {
+                        LOG.info(
+                                "Received non-final transaction state after tx 
state resolution [txId={}, groupId={}, txMeta={}, "
+                                    + "timestamp={}, commitPartId={}, 
currentConsistencyToken={}, writeIntentReadable={}].",
+                                txId,
+                                replicationGroupId,
+                                transactionMeta,
+                                timestamp,
+                                commitPartitionId,
+                                currentConsistencyToken,
+                                writeIntentReadable
+                        );
                     }
 
-                    return canReadFromWriteIntent(txId, txManager, 
transactionMeta, timestamp);
+                    return new 
WriteIntentResolutionResult(writeIntentReadable, transactionMeta);
                 });
     }
 
@@ -3513,6 +3530,7 @@ public class PartitionReplicaListener implements 
ReplicaTableProcessor {
                 : format("Unexpected state defined by write intent resolution 
[{}, txMeta={}].",
                 formatTxInfo(txId, txManager, false), txMeta);
 
+        // TODO IGNITE-27494 double check UNKNOWN state works correctly here.
         if (txMeta.txState() == COMMITTED) {
             boolean readLatest = timestamp == null;
 
@@ -3944,4 +3962,21 @@ public class PartitionReplicaListener implements 
ReplicaTableProcessor {
     public void cleanupLocally(UUID txId, boolean commit, @Nullable 
HybridTimestamp commitTimestamp) {
         storageUpdateHandler.switchWriteIntents(txId, commit, commitTimestamp, 
null);
     }
+
+    private static class WriteIntentResolutionResult {
+        /**
+         * This value is assigned with awareness of read timestamp in case of 
WI resolution by read-only transaction. It is {@code true}
+         * when the transaction is committed and commit time <= read time, 
{@code false} otherwise (when the transaction
+         * is either in progress, or aborted, or committed and commit time > 
read time).
+         */
+        private final boolean writeIntentReadable;
+
+        /** Transaction meta. */
+        private final TransactionMeta transactionMeta;
+
+        public WriteIntentResolutionResult(boolean writeIntentReadable, 
TransactionMeta transactionMeta) {
+            this.writeIntentReadable = writeIntentReadable;
+            this.transactionMeta = transactionMeta;
+        }
+    }
 }
diff --git 
a/modules/transactions/src/integrationTest/java/org/apache/ignite/tx/distributed/ItTxAbortOnCoordinatorOnWriteIntentResolutionWhenPrimaryExpiredTest.java
 
b/modules/transactions/src/integrationTest/java/org/apache/ignite/tx/distributed/ItTxAbortOnCoordinatorOnWriteIntentResolutionWhenPrimaryExpiredTest.java
index a26094e092f..a05243ff932 100644
--- 
a/modules/transactions/src/integrationTest/java/org/apache/ignite/tx/distributed/ItTxAbortOnCoordinatorOnWriteIntentResolutionWhenPrimaryExpiredTest.java
+++ 
b/modules/transactions/src/integrationTest/java/org/apache/ignite/tx/distributed/ItTxAbortOnCoordinatorOnWriteIntentResolutionWhenPrimaryExpiredTest.java
@@ -17,9 +17,14 @@
 
 package org.apache.ignite.tx.distributed;
 
+import static java.util.stream.Collectors.toList;
+import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl;
 import static 
org.apache.ignite.internal.catalog.CatalogService.DEFAULT_STORAGE_PROFILE;
 import static 
org.apache.ignite.internal.sql.engine.util.SqlTestUtils.executeUpdate;
+import static org.apache.ignite.internal.table.NodeUtils.transferPrimary;
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.runInExecutor;
+import static org.apache.ignite.internal.tx.TxState.PENDING;
+import static org.apache.ignite.internal.tx.TxState.isFinalState;
 import static 
org.apache.ignite.internal.tx.test.ItTransactionTestUtils.findTupleToBeHostedOnNode;
 import static 
org.apache.ignite.internal.tx.test.ItTransactionTestUtils.partitionIdForTuple;
 import static org.apache.ignite.internal.tx.test.ItTransactionTestUtils.table;
@@ -30,11 +35,14 @@ import static 
org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermin
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.internal.ClusterPerTestIntegrationTest;
 import org.apache.ignite.internal.TestWrappers;
 import org.apache.ignite.internal.app.IgniteImpl;
@@ -52,13 +60,20 @@ import org.apache.ignite.internal.table.TableImpl;
 import 
org.apache.ignite.internal.table.distributed.TableSchemaAwareIndexStorage;
 import org.apache.ignite.internal.thread.IgniteThreadFactory;
 import org.apache.ignite.internal.thread.ThreadOperation;
+import org.apache.ignite.internal.tx.TxStateMeta;
+import org.apache.ignite.internal.tx.message.TxCleanupMessage;
+import org.apache.ignite.internal.tx.message.WriteIntentSwitchReplicaRequest;
+import 
org.apache.ignite.internal.tx.message.WriteIntentSwitchReplicaRequestBase;
 import org.apache.ignite.table.QualifiedName;
 import org.apache.ignite.table.RecordView;
 import org.apache.ignite.table.Tuple;
+import org.apache.ignite.tx.IgniteTransactions;
 import org.apache.ignite.tx.Transaction;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 /**
  * Test for transaction abort on coordinator when write-intent resolution 
happens after primary replica expiration.
@@ -103,8 +118,18 @@ public class 
ItTxAbortOnCoordinatorOnWriteIntentResolutionWhenPrimaryExpiredTest
         return new int[]{0, 1, 2};
     }
 
-    @Test
-    public void test() throws Exception {
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testCoordinatorAbortsTransaction(boolean withThinClient) 
throws Exception {
+        IgniteClient client = IgniteClient.builder()
+                
.addresses(getClientAddresses(runningNodes().collect(toList())).toArray(new 
String[0]))
+                .operationTimeout(15_000)
+                .build();
+
+        if (withThinClient) {
+            await().atMost(3, TimeUnit.SECONDS).until(() -> 
client.connections().size() == initialNodes());
+        }
+
         IgniteImpl firstPrimaryNode = anyNode();
         IgniteImpl coordinatorNode = findNode(n -> 
!n.name().equals(firstPrimaryNode.name()));
 
@@ -114,17 +139,32 @@ public class 
ItTxAbortOnCoordinatorOnWriteIntentResolutionWhenPrimaryExpiredTest
         RecordView<Tuple> view = 
coordinatorNode.tables().table(TABLE_NAME).recordView();
 
         Transaction txx = coordinatorNode.transactions().begin();
+        Tuple tuple0 = findTupleToBeHostedOnNode(coordinatorNode, TABLE_NAME, 
txx, INITIAL_TUPLE, NEXT_TUPLE, true);
         Tuple tuple = findTupleToBeHostedOnNode(firstPrimaryNode, TABLE_NAME, 
txx, INITIAL_TUPLE, NEXT_TUPLE, true);
         int partId = partitionIdForTuple(firstPrimaryNode, TABLE_NAME, tuple, 
txx);
         var groupId = new ZonePartitionId(zoneId(firstPrimaryNode, 
TABLE_NAME), partId);
         log.info("Test: groupId: " + groupId);
+        view.upsert(txx, tuple0);
         view.upsert(txx, tuple);
 
         txx.commit();
 
-        Transaction tx0 = coordinatorNode.transactions().begin();
-        log.info("Test: unfinished tx id: " + txId(tx0));
-        view.upsert(tx0, tuple);
+        // Unfinished transaction.
+        RecordView<Tuple> unfinishedTxView = withThinClient
+                ? client.tables().table(TABLE_NAME).recordView()
+                : coordinatorNode.tables().table(TABLE_NAME).recordView();
+        IgniteTransactions unfinishedTxTransactions = withThinClient
+                ? client.transactions()
+                : coordinatorNode.transactions();
+
+        Transaction tx0 = unfinishedTxTransactions.begin();
+
+        if (!withThinClient) {
+            log.info("Test: unfinished tx id: " + txId(tx0));
+        }
+
+        unfinishedTxView.upsert(tx0, tuple0);
+        unfinishedTxView.upsert(tx0, tuple);
         // Don't commit or rollback tx0.
 
         // Wait for replication of write intent.
@@ -164,6 +204,88 @@ public class 
ItTxAbortOnCoordinatorOnWriteIntentResolutionWhenPrimaryExpiredTest
         assertEquals(newTuple, actual);
     }
 
+    @Test
+    public void testWriteIntentResolutionUsesCorrectStateAndCommitTimestamp() 
throws Exception {
+        // Coordinator node will also be the commit partition primary node.
+        IgniteImpl coordinatorNode = anyNode();
+        IgniteImpl firstPrimaryNode = findNode(n -> 
!n.name().equals(coordinatorNode.name()));
+        IgniteImpl secondPrimaryNode = findNode(n -> 
!n.name().equals(coordinatorNode.name()) && 
!n.name().equals(firstPrimaryNode.name()));
+
+        log.info("Test: coordinatorNode: {}, coordinatorNode id: {}", 
coordinatorNode.name(), coordinatorNode.id());
+        log.info("Test: firstPrimaryNode: {}, firstPrimaryNode id: {}", 
firstPrimaryNode.name(), firstPrimaryNode.id());
+        log.info("Test: secondPrimaryNode: {}, secondPrimaryNode id: {}", 
secondPrimaryNode.name(), secondPrimaryNode.id());
+
+        RecordView<Tuple> view = 
coordinatorNode.tables().table(TABLE_NAME).recordView();
+
+        Transaction txx = coordinatorNode.transactions().begin();
+        Tuple tuple1 = findTupleToBeHostedOnNode(coordinatorNode, TABLE_NAME, 
txx, INITIAL_TUPLE, NEXT_TUPLE, true);
+        Tuple tuple2 = findTupleToBeHostedOnNode(firstPrimaryNode, TABLE_NAME, 
txx, INITIAL_TUPLE, NEXT_TUPLE, true);
+        int partId1 = partitionIdForTuple(coordinatorNode, TABLE_NAME, tuple1, 
txx);
+        var groupId1 = new ZonePartitionId(zoneId(firstPrimaryNode, 
TABLE_NAME), partId1);
+        int partId2 = partitionIdForTuple(coordinatorNode, TABLE_NAME, tuple2, 
txx);
+        var groupId2 = new ZonePartitionId(zoneId(firstPrimaryNode, 
TABLE_NAME), partId2);
+        log.info("Test: groupId1: " + groupId1);
+        log.info("Test: groupId2: " + groupId2);
+        view.upsert(txx, tuple1);
+        view.upsert(txx, tuple2);
+
+        txx.commit();
+
+        cluster.runningNodes().forEach(node -> {
+            unwrapIgniteImpl(node).dropMessages((dest, msg) -> {
+                boolean wiSwitch = msg instanceof TxCleanupMessage || msg 
instanceof WriteIntentSwitchReplicaRequestBase;
+                return wiSwitch && secondPrimaryNode.name().equals(dest);
+            });
+        });
+
+        coordinatorNode.dropMessages((dest, msg) -> msg instanceof 
TxCleanupMessage || msg instanceof WriteIntentSwitchReplicaRequest);
+        firstPrimaryNode.dropMessages((dest, msg) -> msg instanceof 
TxCleanupMessage || msg instanceof WriteIntentSwitchReplicaRequest);
+
+        Transaction tx0 = coordinatorNode.transactions().begin();
+        UUID tx0Id = txId(tx0);
+        log.info("Test: cleanup unfinished tx id: " + txId(tx0));
+        view.upsert(tx0, tuple1);
+        view.upsert(tx0, tuple2);
+
+        tx0.commitAsync();
+
+        await().atMost(5, TimeUnit.SECONDS)
+                .until(() -> txFinishedStateOnNode(coordinatorNode, tx0Id));
+
+        // Wait for replication of write intent.
+        Tuple keyTuple = Tuple.create().set("key", tuple2.longValue("key"));
+        await().atMost(5, TimeUnit.SECONDS)
+                .until(() -> checkWriteIntentInStorageOnAllNodes(partId2, 
keyTuple));
+
+        
transferPrimary(runningNodes().map(TestWrappers::unwrapIgniteImpl).collect(toList()),
 groupId2, secondPrimaryNode.name());
+
+        await().atMost(5, TimeUnit.SECONDS)
+                .until(() -> txFinishedStateOnNode(secondPrimaryNode, tx0Id));
+
+        Transaction tx = coordinatorNode.transactions().begin();
+        log.info("Test: new tx: " + txId(tx));
+        log.info("Test: upsert");
+
+        Tuple newTuple = Tuple.create().set("key", 
tuple2.longValue("key")).set("val", "v");
+
+        // Tx cleanup is blocked but tx state could be propagated if second 
tuple's primary node is commit partition's backup.
+        // Transfer it back to pending, imitating obsolete transaction state 
on secondPrimaryNode.
+        secondPrimaryNode.txManager().updateTxMeta(tx0Id, old -> null);
+        secondPrimaryNode.txManager().updateTxMeta(tx0Id, old -> 
TxStateMeta.builder(PENDING).build());
+
+        // If coordinator of tx0 doesn't abort it, tx will stumble into write 
intent and fail with TxIdMismatchException.
+        view.upsert(tx, newTuple);
+
+        coordinatorNode.stopDroppingMessages();
+        firstPrimaryNode.stopDroppingMessages();
+
+        tx.commit();
+
+        // Check that new value is written successfully.
+        Tuple actual = view.get(null, keyTuple);
+        assertEquals(newTuple, actual);
+    }
+
     private boolean checkWriteIntentInStorageOnAllNodes(int partId, Tuple key) 
{
         return cluster.runningNodes()
                 .map(TestWrappers::unwrapIgniteImpl)
@@ -195,4 +317,22 @@ public class 
ItTxAbortOnCoordinatorOnWriteIntentResolutionWhenPrimaryExpiredTest
             return false;
         });
     }
+
+    private static boolean txFinishedStateOnNode(IgniteImpl node, UUID txId) {
+        TxStateMeta meta = node.txManager().stateMeta(txId);
+
+        return meta != null && isFinalState(meta.txState());
+    }
+
+    private static List<String> getClientAddresses(List<Ignite> nodes) {
+        return getClientPorts(nodes).stream()
+                .map(port -> "127.0.0.1" + ":" + port)
+                .collect(toList());
+    }
+
+    private static List<Integer> getClientPorts(List<Ignite> nodes) {
+        return nodes.stream()
+                .map(ignite -> unwrapIgniteImpl(ignite).clientAddress().port())
+                .collect(toList());
+    }
 }
diff --git 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionStateResolver.java
 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionStateResolver.java
index 57470203bf4..847d3b6d250 100644
--- 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionStateResolver.java
+++ 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionStateResolver.java
@@ -32,6 +32,8 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import org.apache.ignite.internal.hlc.ClockService;
 import org.apache.ignite.internal.hlc.HybridTimestamp;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
 import org.apache.ignite.internal.network.ClusterNodeResolver;
 import org.apache.ignite.internal.network.InternalClusterNode;
 import org.apache.ignite.internal.network.MessagingService;
@@ -58,6 +60,8 @@ import org.jetbrains.annotations.Nullable;
  * Helper class that allows to resolve transaction state mainly for the 
purpose of write intent resolution.
  */
 public class TransactionStateResolver {
+    private static final IgniteLogger LOG = 
Loggers.forClass(TransactionStateResolver.class);
+
     /** Tx messages factory. */
     private static final TxMessagesFactory TX_MESSAGES_FACTORY = new 
TxMessagesFactory();
 
@@ -463,10 +467,21 @@ public class TransactionStateResolver {
 
                 if (tx != null && !tx.isReadOnly() && currentConsistencyToken 
!= null && groupId != null) {
                     return 
txManager.checkEnlistedPartitionsAndAbortIfNeeded(txStateMeta, tx, 
currentConsistencyToken, groupId);
+                } else {
+                    LOG.info("Failed to abort on coordinator a transaction 
that lost its primary replica's volatile state "
+                            + "[txId={}, internalTx={}, readOnly={}, 
senderCurrentConsistencyToken={}, senderGroupId={}].",
+                            txId,
+                            tx,
+                            (tx == null ? null : tx.isReadOnly()),
+                            currentConsistencyToken,
+                            groupId
+                    );
                 }
             }
         }
 
+        LOG.info("Transaction meta is absent on coordinator [txId={}].", txId);
+
         return completedFuture(txStateMeta);
     }
 
diff --git 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxManagerImpl.java
 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxManagerImpl.java
index 0e4dcb9e88c..69ffa216e6a 100644
--- 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxManagerImpl.java
+++ 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxManagerImpl.java
@@ -591,7 +591,8 @@ public class TxManagerImpl implements TxManager, 
NetworkMessageHandler, SystemVi
     ) {
         PendingTxPartitionEnlistment enlistment = 
tx.enlistedPartition(senderGroupId);
 
-        if (enlistment != null && enlistment.consistencyToken() != 
currentEnlistmentConsistencyToken) {
+        // Enlistment for thin client direct request may be absent on 
coordinator.
+        if (enlistment == null || enlistment.consistencyToken() != 
currentEnlistmentConsistencyToken) {
             // Remote partition already has different consistency token, so we 
can't commit this transaction anyway.
             // Even when graceful primary replica switch is done, we can get 
here only if the write intent that requires resolution
             // is not under lock.
@@ -606,6 +607,15 @@ public class TxManagerImpl implements TxManager, 
NetworkMessageHandler, SystemVi
                     });
         }
 
+        LOG.info("Skipped aborting on coordinator a transaction that lost its 
primary replica's volatile state "
+                        + "[txId={}, internalTx={}, enlistment={}, 
senderCurrentConsistencyToken={}, txMeta={}].",
+                tx.id(),
+                tx,
+                enlistment,
+                currentEnlistmentConsistencyToken,
+                txMeta
+        );
+
         return completedFuture(txMeta);
     }
 

Reply via email to