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

ptupitsyn 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 c02dc152a6c IGNITE-27091 Fix client tx rollback to ignore connection 
errors (#7671)
c02dc152a6c is described below

commit c02dc152a6ce4993c5bb871d19c3e104556e8bb7
Author: Pavel Tupitsyn <[email protected]>
AuthorDate: Mon Mar 9 10:26:30 2026 +0100

    IGNITE-27091 Fix client tx rollback to ignore connection errors (#7671)
    
    When the client disconnects, server rolls back the transaction 
automatically, so it does not make sense to throw 
`IgniteClientConnectionException` from `rollback` or `rollbackAsync`.
---
 .../internal/client/tx/ClientTransaction.java      | 28 +++++++++
 .../ignite/client/ClientTransactionsTest.java      | 69 ++++++++++++++++++++++
 .../RepeatedFinishClientTransactionTest.java       |  4 --
 .../app/client/ItThinClientTransactionsTest.java   | 22 +++++++
 4 files changed, 119 insertions(+), 4 deletions(-)

diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransaction.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransaction.java
index d4c2994767b..5e477efef14 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransaction.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransaction.java
@@ -23,6 +23,8 @@ import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.TX_
 import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static org.apache.ignite.internal.util.CompletableFutures.allOf;
 import static 
org.apache.ignite.internal.util.CompletableFutures.nullCompletedFuture;
+import static org.apache.ignite.internal.util.ExceptionUtils.hasCause;
+import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 import static org.apache.ignite.internal.util.ViewUtils.sync;
 import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
 import static 
org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_ERR;
@@ -38,6 +40,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+import org.apache.ignite.client.IgniteClientConnectionException;
 import org.apache.ignite.internal.client.ClientChannel;
 import org.apache.ignite.internal.client.PartitionMapping;
 import org.apache.ignite.internal.client.PayloadOutputChannel;
@@ -49,6 +52,7 @@ import org.apache.ignite.internal.replicator.TablePartitionId;
 import org.apache.ignite.internal.tostring.IgniteToStringExclude;
 import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.internal.util.ExceptionUtils;
+import org.apache.ignite.internal.util.ViewUtils;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.tx.Transaction;
 import org.apache.ignite.tx.TransactionException;
@@ -379,6 +383,30 @@ public class ClientTransaction implements Transaction {
     /** {@inheritDoc} */
     @Override
     public CompletableFuture<Void> rollbackAsync() {
+        try {
+            return rollbackAsyncInternal().handle((res, e) -> {
+                if (e != null) {
+                    if (hasCause(e, IgniteClientConnectionException.class)) {
+                        // Connection exception: tx rolled back on server.
+                        return null;
+                    }
+
+                    throw sneakyThrow(ViewUtils.ensurePublicException(e));
+                }
+
+                return null;
+            });
+        } catch (Throwable t) {
+            if (hasCause(t, IgniteClientConnectionException.class)) {
+                // Connection exception: tx rolled back on server.
+                return nullCompletedFuture();
+            }
+
+            throw sneakyThrow(ViewUtils.ensurePublicException(t));
+        }
+    }
+
+    private CompletableFuture<Void> rollbackAsyncInternal() {
         enlistPartitionLock.writeLock().lock();
 
         try {
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/ClientTransactionsTest.java
 
b/modules/client/src/test/java/org/apache/ignite/client/ClientTransactionsTest.java
new file mode 100644
index 00000000000..be6a6a6f6c3
--- /dev/null
+++ 
b/modules/client/src/test/java/org/apache/ignite/client/ClientTransactionsTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.client;
+
+import static org.apache.ignite.client.fakes.FakeIgniteTables.TABLE_ONE_COLUMN;
+import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willSucceedFast;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import org.apache.ignite.client.fakes.FakeIgniteTables;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.table.RecordView;
+import org.apache.ignite.table.mapper.Mapper;
+import org.apache.ignite.tx.Transaction;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests for client transactions.
+ */
+public class ClientTransactionsTest extends BaseIgniteAbstractTest {
+    private TestServer server;
+
+    @BeforeEach
+    public void setUp() {
+        server = TestServer.builder().build();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        server.close();
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testRollbackDoesNotThrowOnServerDisconnect(boolean async) {
+        try (var client = IgniteClient.builder().addresses("127.0.0.1:" + 
server.port()).build()) {
+            ((FakeIgniteTables) 
server.ignite().tables()).createTable(TABLE_ONE_COLUMN);
+            RecordView<String> recView = 
client.tables().table(TABLE_ONE_COLUMN).recordView(Mapper.of(String.class));
+            Transaction tx = client.transactions().begin();
+            recView.upsert(tx, "foo");
+
+            server.close();
+
+            if (async) {
+                assertThat(tx.rollbackAsync(), willSucceedFast());
+            } else {
+                assertDoesNotThrow(tx::rollback);
+            }
+        }
+    }
+}
diff --git 
a/modules/client/src/test/java/org/apache/ignite/internal/client/RepeatedFinishClientTransactionTest.java
 
b/modules/client/src/test/java/org/apache/ignite/internal/client/RepeatedFinishClientTransactionTest.java
index 781b076e655..6b660ce208d 100644
--- 
a/modules/client/src/test/java/org/apache/ignite/internal/client/RepeatedFinishClientTransactionTest.java
+++ 
b/modules/client/src/test/java/org/apache/ignite/internal/client/RepeatedFinishClientTransactionTest.java
@@ -84,9 +84,7 @@ public class RepeatedFinishClientTransactionTest extends 
BaseIgniteAbstractTest
         CompletableFuture<Void> rollbackFut = tx.rollbackAsync();
 
         assertNotSame(firstCommitFut, secondCommitFut);
-        assertSame(secondCommitFut, rollbackFut);
         assertSame(secondCommitFut, tx.commitAsync());
-        assertSame(rollbackFut, tx.rollbackAsync());
 
         assertFalse(firstCommitFut.isDone());
         assertFalse(secondCommitFut.isDone());
@@ -130,9 +128,7 @@ public class RepeatedFinishClientTransactionTest extends 
BaseIgniteAbstractTest
         CompletableFuture<Void> secondRollbackFut = tx.rollbackAsync();
 
         assertNotSame(firstRollbackFut, secondRollbackFut);
-        assertSame(secondRollbackFut, commitFut);
         assertSame(commitFut, tx.commitAsync());
-        assertSame(secondRollbackFut, tx.rollbackAsync());
 
         assertFalse(firstRollbackFut.isDone());
         assertFalse(secondRollbackFut.isDone());
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientTransactionsTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientTransactionsTest.java
index 7a86359cbdc..ae2633c88c8 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientTransactionsTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientTransactionsTest.java
@@ -1532,6 +1532,28 @@ public class ItThinClientTransactionsTest extends 
ItAbstractThinClientTest {
         assertThat(kvView.removeAllAsync(null, Arrays.asList(key0, key, 
key3)), willSucceedFast());
     }
 
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    public void testRollbackDoesNotThrowOnClientDisconnect(boolean async) {
+        try (IgniteClient client = 
IgniteClient.builder().addresses(getNodeAddress()).build()) {
+            KeyValueView<Integer, String> kvView = 
client.tables().table(TABLE_NAME).keyValueView(Integer.class, String.class);
+
+            Transaction tx = client.transactions().begin();
+            kvView.put(tx, 999, "test");
+
+            client.close();
+
+            if (async) {
+                assertThat(tx.rollbackAsync(), willSucceedFast());
+            } else {
+                assertDoesNotThrow(tx::rollback);
+            }
+        }
+
+        KeyValueView<Integer, String> kvView = 
client().tables().table(TABLE_NAME).keyValueView(Integer.class, String.class);
+        assertNull(kvView.get(null, 999), "Value should not be visible after 
rollback");
+    }
+
     @AfterEach
     protected void validateInflights() throws NoSuchFieldException {
         System.out.println("DBG: validateInflights");

Reply via email to