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");