This is an automated email from the ASF dual-hosted git repository.
ashapkin 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 40eca02a9b4 IGNITE-27411 Added tests to prove that lost TX problem is
gone (#7820)
40eca02a9b4 is described below
commit 40eca02a9b4c142bc8164333ad4fb58f59f1a647
Author: Anton Laletin <[email protected]>
AuthorDate: Wed Apr 8 17:11:10 2026 +0400
IGNITE-27411 Added tests to prove that lost TX problem is gone (#7820)
---
.../java/org/apache/ignite/lang/ErrorGroups.java | 3 +
modules/platforms/cpp/ignite/common/error_codes.h | 1 +
modules/platforms/cpp/ignite/odbc/common_types.cpp | 1 +
.../platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs | 3 +
.../ignite/internal/sql/api/ItSqlApiBaseTest.java | 192 +++++++++++++++++++++
.../engine/QueryTransactionWrapperSelfTest.java | 47 +++++
.../distributed/storage/InternalTableImplTest.java | 50 +++++-
.../ignite/internal/tx/impl/TxRecoveryEngine.java | 4 +-
.../apache/ignite/internal/tx/TxStateMetaTest.java | 11 ++
9 files changed, 306 insertions(+), 6 deletions(-)
diff --git a/modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java
b/modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java
index 6b85252d5f5..c576275646f 100755
--- a/modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java
+++ b/modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java
@@ -479,6 +479,9 @@ public class ErrorGroups {
/** Operation failed because the transaction is already finished due
to an error. */
public static final int TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR =
TX_ERR_GROUP.registerErrorCode((short) 19);
+
+ /** Operation failed because the transaction is aborted due to a
recovery. */
+ public static final int TX_ABORTED_DUE_TO_RECOVERY_ERR =
TX_ERR_GROUP.registerErrorCode((short) 20);
}
/** Replicator error group. */
diff --git a/modules/platforms/cpp/ignite/common/error_codes.h
b/modules/platforms/cpp/ignite/common/error_codes.h
index 8a09d2fe3c6..a852a9c6b83 100644
--- a/modules/platforms/cpp/ignite/common/error_codes.h
+++ b/modules/platforms/cpp/ignite/common/error_codes.h
@@ -141,6 +141,7 @@ enum class code : underlying_t {
TX_DELAYED_ACK = 0x70011,
TX_KILLED = 0x70012,
TX_ALREADY_FINISHED_WITH_EXCEPTION = 0x70013,
+ TX_ABORTED_DUE_TO_RECOVERY = 0x70014,
// Replicator group. Group code: 8
REPLICA_COMMON = 0x80001,
diff --git a/modules/platforms/cpp/ignite/odbc/common_types.cpp
b/modules/platforms/cpp/ignite/odbc/common_types.cpp
index 98c7acc4588..d2ca530211a 100644
--- a/modules/platforms/cpp/ignite/odbc/common_types.cpp
+++ b/modules/platforms/cpp/ignite/odbc/common_types.cpp
@@ -212,6 +212,7 @@ sql_state error_code_to_sql_state(error::code code) {
case error::code::TX_DELAYED_ACK:
case error::code::TX_KILLED:
case error::code::TX_ALREADY_FINISHED_WITH_EXCEPTION:
+ case error::code::TX_ABORTED_DUE_TO_RECOVERY:
return sql_state::S25000_INVALID_TRANSACTION_STATE;
// Replicator group. Group code: 8
diff --git a/modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs
b/modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs
index c979af85214..36db50e89bf 100644
--- a/modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs
@@ -376,6 +376,9 @@ namespace Apache.Ignite
/// <summary> TxAlreadyFinishedWithException error. </summary>
public const int TxAlreadyFinishedWithException = (GroupCode <<
16) | (19 & 0xFFFF);
+
+ /// <summary> TxAbortedDueToRecovery error. </summary>
+ public const int TxAbortedDueToRecovery = (GroupCode << 16) | (20
& 0xFFFF);
}
/// <summary> Replicator errors. </summary>
diff --git
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java
index 07cbfd22326..ae548e3f9ce 100644
---
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java
+++
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java
@@ -28,6 +28,8 @@ import static
org.apache.ignite.internal.sql.engine.util.SqlTestUtils.expectQuer
import static
org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause;
import static
org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCode;
import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
+import static org.apache.ignite.internal.util.ExceptionUtils.hasCause;
+import static org.apache.ignite.lang.ErrorGroups.Replicator.REPLICA_MISS_ERR;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
@@ -37,6 +39,7 @@ import static
org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.time.Instant;
import java.time.ZoneId;
@@ -51,9 +54,11 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.calcite.rel.core.JoinRelType;
import org.apache.ignite.Ignite;
+import org.apache.ignite.internal.app.IgniteImpl;
import org.apache.ignite.internal.catalog.commands.CatalogUtils;
import org.apache.ignite.internal.catalog.events.CatalogEvent;
import org.apache.ignite.internal.catalog.events.CreateTableEventParameters;
+import org.apache.ignite.internal.client.tx.ClientLazyTransaction;
import org.apache.ignite.internal.event.EventListener;
import org.apache.ignite.internal.sql.BaseSqlIntegrationTest;
import org.apache.ignite.internal.sql.ColumnMetadataImpl;
@@ -61,7 +66,11 @@ import
org.apache.ignite.internal.sql.ColumnMetadataImpl.ColumnOriginImpl;
import org.apache.ignite.internal.sql.engine.QueryCancelledException;
import org.apache.ignite.internal.sql.engine.exec.fsm.QueryInfo;
import org.apache.ignite.internal.testframework.IgniteTestUtils;
+import org.apache.ignite.internal.tx.InternalTransaction;
import org.apache.ignite.internal.tx.TxManager;
+import org.apache.ignite.internal.tx.TxState;
+import org.apache.ignite.internal.tx.TxStateMeta;
+import org.apache.ignite.internal.tx.message.TxMessageGroup;
import org.apache.ignite.internal.util.CompletableFutures;
import org.apache.ignite.lang.CancelHandle;
import org.apache.ignite.lang.CancellationToken;
@@ -83,7 +92,9 @@ import org.apache.ignite.sql.SqlRow;
import org.apache.ignite.sql.Statement;
import org.apache.ignite.sql.Statement.StatementBuilder;
import org.apache.ignite.tx.Transaction;
+import org.apache.ignite.tx.TransactionException;
import org.apache.ignite.tx.TransactionOptions;
+import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
@@ -740,6 +751,175 @@ public abstract class ItSqlApiBaseTest extends
BaseSqlIntegrationTest {
"Transaction is already finished due to an error");
}
+ @Test
+ public void
runtimeErrorReturnsSameTransactionErrorBeforeAndAfterRollbackCompletion()
throws Exception {
+ sql("CREATE TABLE tst(id INTEGER PRIMARY KEY, val INTEGER)");
+
+ IgniteSql sql = igniteSql();
+
+ Transaction tx = igniteTx().begin();
+
+ // Enlist enough operations to make rollback non-trivial.
+ for (int i = 0; i < 100; i++) {
+ execute(tx, sql, "INSERT INTO tst VALUES (?, ?)", i, i);
+ }
+
+ UUID txId = txId(tx);
+
+ assertThrowsSqlException(
+ Sql.RUNTIME_ERR,
+ "Division by zero",
+ () -> execute(tx, sql, "SELECT val / 0 FROM tst WHERE id = ?",
0)
+ );
+
+ IgniteException[] immediateExceptions = new IgniteException[5];
+ for (int i = 0; i < immediateExceptions.length; i++) {
+ immediateExceptions[i] = (IgniteException) assertThrowsWithCause(
+ () -> executeForRead(sql, tx, "SELECT * FROM tst WHERE id
= ?", 1),
+ IgniteException.class
+ );
+ }
+
+ if (tx instanceof InternalTransaction) {
+ assertNotNull(txId, "Expected transaction id for test transaction
implementation");
+
+ Awaitility.await()
+ .atMost(5, TimeUnit.SECONDS)
+ .until(() -> {
+ TxStateMeta meta = txManager().stateMeta(txId);
+
+ return meta != null &&
TxState.isFinalState(meta.txState());
+ });
+ }
+
+ IgniteException abortedStateException = (IgniteException)
assertThrowsWithCause(
+ () -> executeForRead(sql, tx, "SELECT * FROM tst WHERE id =
?", 1),
+ IgniteException.class
+ );
+
+ assertEquals(Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR,
abortedStateException.code());
+ assertTrue(abortedStateException.getMessage().contains("Transaction is
already finished due to an error"));
+
+ for (IgniteException immediateException : immediateExceptions) {
+ assertEquals(abortedStateException.code(),
immediateException.code());
+ assertTrue(immediateException.getMessage().contains("Transaction
is already finished due to an error"));
+ }
+ }
+
+ @Test
+ public void
secondRequestDuringRollbackReturnsFinishedWithExceptionAndPreservesOriginalCause()
{
+ sql("CREATE TABLE tst(id INTEGER PRIMARY KEY, val INTEGER)");
+ sql("INSERT INTO tst VALUES (0, 1)");
+
+ IgniteSql sql = igniteSql();
+
+ Transaction tx = igniteTx().begin();
+
+ List<IgniteImpl> clusterNodes = CLUSTER.runningNodes()
+ .map(node -> unwrapIgniteImpl(node))
+ .collect(toList());
+
+ CompletableFuture<Void> failingRequestStarted = new
CompletableFuture<>();
+ CompletableFuture<Void> finishRequestBlocked = new
CompletableFuture<>();
+ CompletableFuture<Void> releaseFinishRequest = new
CompletableFuture<>();
+
+ for (IgniteImpl clusterNode : clusterNodes) {
+ // Install predicates in cluster
+ clusterNode.dropMessages((recipientConsistentId, msg) -> {
+ if (!failingRequestStarted.isDone()) {
+ return false;
+ }
+
+ if (msg.groupType() != TxMessageGroup.GROUP_TYPE
+ || msg.messageType() !=
TxMessageGroup.TX_FINISH_REQUEST) {
+ return false;
+ }
+
+ finishRequestBlocked.complete(null);
+
+ return !releaseFinishRequest.isDone();
+ });
+ }
+
+ try {
+ CompletableFuture<IgniteException> failingRequestFut =
IgniteTestUtils.runAsync(() -> {
+ failingRequestStarted.complete(null);
+
+ IgniteException ex = assertInstanceOf(
+ IgniteException.class,
+ assertThrowsWithCause(
+ () -> execute(tx, sql, "SELECT val / 0 FROM
tst WHERE id = ?", 0),
+ IgniteException.class
+ )
+ );
+
+ assertTrue(hasCause(ex, "Division by zero", Throwable.class));
+ assertTrue(
+ ex.code() == Sql.RUNTIME_ERR || ex.code() ==
Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR,
+ "Unexpected code for a request that triggers rollback
[code=" + ex.code() + ']'
+ );
+
+ return ex;
+ });
+
+ Awaitility.await()
+ .atMost(5, TimeUnit.SECONDS)
+ .until(finishRequestBlocked::isDone);
+
+ IgniteException parallelRequestException = assertInstanceOf(
+ IgniteException.class,
+ assertThrowsWithCause(
+ () -> executeForRead(sql, tx, "SELECT * FROM tst
WHERE id = ?", 0),
+ IgniteException.class
+ )
+ );
+
+ assertEquals(Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR,
parallelRequestException.code());
+
assertTrue(parallelRequestException.getMessage().contains("Transaction is
already finished due to an error"));
+ assertTrue(
+ hasCause(parallelRequestException, "Division by zero",
Throwable.class),
+ "Expected original rollback cause in user-visible
exception chain"
+ );
+
+ releaseFinishRequest.complete(null);
+
+ IgniteException firstRequestException = await(failingRequestFut);
+
+ assertTrue(hasCause(firstRequestException, "Division by zero",
Throwable.class));
+ } finally {
+ clusterNodes.forEach(IgniteImpl::stopDroppingMessages);
+ }
+ }
+
+ @Test
+ public void rollbackWithExceptionCauseIsPropagatedToSubsequentSqlRequest()
{
+ sql("CREATE TABLE tst(id INTEGER PRIMARY KEY, val INTEGER)");
+ sql("INSERT INTO tst VALUES (?, ?)", 1, 1);
+
+ Transaction tx = igniteTx().begin();
+
+ assumeTrue(tx instanceof InternalTransaction, "InternalTransaction is
required");
+
+ InternalTransaction internalTx = (InternalTransaction) tx;
+ String rollbackCauseMessage = "rollback-cause-primary-replica-changed";
+ TransactionException rollbackCause = new
TransactionException(REPLICA_MISS_ERR, rollbackCauseMessage);
+
+ await(internalTx.rollbackWithExceptionAsync(rollbackCause));
+
+ IgniteException ex = assertInstanceOf(
+ IgniteException.class,
+ assertThrowsWithCause(
+ () -> executeForRead(igniteSql(), tx, "SELECT * FROM
tst WHERE id = ?", 1),
+ IgniteException.class
+ )
+ );
+
+ assertEquals(Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR,
ex.code());
+ assertTrue(ex.getMessage().contains("Transaction is already finished
due to an error"));
+ assertTrue(hasCause(ex, TransactionException.class));
+ assertTrue(hasCause(ex, rollbackCauseMessage, Throwable.class),
"Expected rollback cause message in user-visible exception chain");
+ }
+
@Test
public void testLockIsNotReleasedAfterTxRollback() {
IgniteSql sql = igniteSql();
@@ -1413,6 +1593,18 @@ public abstract class ItSqlApiBaseTest extends
BaseSqlIntegrationTest {
protected abstract ResultSet<SqlRow> executeForRead(IgniteSql sql,
@Nullable Transaction tx, Statement statement, Object... args);
+ private static @Nullable UUID txId(Transaction tx) {
+ if (tx instanceof InternalTransaction) {
+ return ((InternalTransaction) tx).id();
+ }
+
+ if (tx instanceof ClientLazyTransaction) {
+ return ((ClientLazyTransaction) tx).startedTx().txId();
+ }
+
+ return null;
+ }
+
protected void checkSqlError(
int code,
String msg,
diff --git
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java
index 9b99be7a335..e76f133727c 100644
---
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java
+++
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java
@@ -18,6 +18,7 @@
package org.apache.ignite.internal.sql.engine;
import static
org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException;
+import static
org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -34,6 +35,7 @@ import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.apache.ignite.internal.hlc.HybridTimestampTracker;
+import org.apache.ignite.internal.lang.IgniteInternalException;
import org.apache.ignite.internal.sql.engine.framework.NoOpTransaction;
import org.apache.ignite.internal.sql.engine.sql.IgniteSqlCommitTransaction;
import org.apache.ignite.internal.sql.engine.sql.IgniteSqlStartTransaction;
@@ -45,8 +47,11 @@ import
org.apache.ignite.internal.sql.engine.tx.QueryTransactionWrapperImpl;
import org.apache.ignite.internal.sql.engine.tx.ScriptTransactionContext;
import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
import org.apache.ignite.internal.tx.TxManager;
+import org.apache.ignite.internal.tx.TxStateMeta;
+import org.apache.ignite.internal.tx.TxStateMetaFinishing;
import org.apache.ignite.internal.tx.impl.TransactionInflights;
import org.apache.ignite.lang.ErrorGroups.Sql;
+import org.apache.ignite.tx.TransactionException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -231,6 +236,48 @@ public class QueryTransactionWrapperSelfTest extends
BaseIgniteAbstractTest {
assertEquals(1, inflights.size());
}
+ @Test
+ public void
testInflightTrackerUsesFinishedWithErrorClassificationForFinishingTx() {
+ NoOpTransaction tx = NoOpTransaction.readWrite("test-rw", false);
+ IgniteInternalException failure = new IgniteInternalException(321,
"boom");
+ TxStateMeta finishingMeta = new TxStateMetaFinishing(null, null,
false, null, failure, failure.code());
+
+ when(transactionInflights.track(tx.id())).thenReturn(false);
+ when(txManager.stateMeta(tx.id())).thenReturn(finishingMeta);
+
+ TransactionException ex = assertThrowsExactly(
+ TransactionException.class,
+ () -> new
InflightTransactionalOperationTracker(transactionInflights,
txManager).registerOperationStart(tx)
+ );
+
+ assertEquals(TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR, ex.code());
+ assertEquals(failure, ex.getCause());
+ }
+
+ @Test
+ public void
testExplicitSqlTransactionUsesFinishedWithErrorClassificationForFinishingTx() {
+ NoOpTransaction tx = NoOpTransaction.readWrite("test-rw", false);
+ IgniteInternalException failure = new IgniteInternalException(321,
"boom");
+ TxStateMeta finishingMeta = new TxStateMetaFinishing(null, null,
false, null, failure, failure.code());
+
+ when(txManager.stateMeta(tx.id())).thenReturn(finishingMeta);
+
+ QueryTransactionContext txCtx = new QueryTransactionContextImpl(
+ txManager,
+ observableTimeTracker,
+ tx,
+ new
InflightTransactionalOperationTracker(transactionInflights, txManager)
+ );
+
+ TransactionException ex = assertThrowsExactly(
+ TransactionException.class,
+ () -> txCtx.getOrStartSqlManaged(false, false)
+ );
+
+ assertEquals(TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR, ex.code());
+ assertEquals(failure, ex.getCause());
+ }
+
private void prepareTransactionsMocks() {
when(txManager.beginExplicit(any(), anyBoolean(), any())).thenAnswer(
inv -> {
diff --git
a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/storage/InternalTableImplTest.java
b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/storage/InternalTableImplTest.java
index e43bb7b60a6..c7f0918999c 100644
---
a/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/storage/InternalTableImplTest.java
+++
b/modules/table/src/test/java/org/apache/ignite/internal/table/distributed/storage/InternalTableImplTest.java
@@ -120,6 +120,7 @@ import
org.apache.ignite.internal.tx.PendingTxPartitionEnlistment;
import org.apache.ignite.internal.tx.TxManager;
import org.apache.ignite.internal.tx.TxState;
import org.apache.ignite.internal.tx.TxStateMeta;
+import org.apache.ignite.internal.tx.TxStateMetaFinishing;
import org.apache.ignite.internal.tx.impl.ReadWriteTransactionImpl;
import org.apache.ignite.internal.tx.impl.TransactionInflights;
import org.apache.ignite.internal.tx.impl.VolatileTxStateMetaStorage;
@@ -587,10 +588,10 @@ public class InternalTableImplTest extends
BaseIgniteAbstractTest {
@ParameterizedTest
@CsvSource({
- "0, 0", // GREATER | LESS (both exclusive)
- "1, 0", // GREATER_OR_EQUAL | LESS (lower inclusive,
upper exclusive)
- "0, 2", // GREATER | LESS_OR_EQUAL (lower exclusive,
upper inclusive)
- "1, 2" // GREATER_OR_EQUAL | LESS_OR_EQUAL (both
inclusive)
+ "0, 0", // GREATER | LESS (both exclusive)
+ "1, 0", // GREATER_OR_EQUAL | LESS (lower inclusive,
upper exclusive)
+ "0, 2", // GREATER | LESS_OR_EQUAL (lower exclusive,
upper inclusive)
+ "1, 2" // GREATER_OR_EQUAL | LESS_OR_EQUAL (both
inclusive)
})
void testRangeWithDifferentFlags(int lowerFlag, int upperFlag) {
InternalTableImpl internalTable = newInternalTable(TABLE_ID, 1);
@@ -950,6 +951,47 @@ public class InternalTableImplTest extends
BaseIgniteAbstractTest {
}
}
+ @Test
+ void testScanWhileFinishingAfterErrorThrowsFinishedWithErrCode() {
+ InternalTableImpl internalTable = newInternalTable(TABLE_ID, 1);
+
+ InternalTransaction tx = new ReadWriteTransactionImpl(
+ txManager,
+ mock(HybridTimestampTracker.class),
+ TestTransactionIds.newTransactionId(),
+ randomUUID(),
+ false,
+ 1,
+ null
+ );
+
+ UUID txId = tx.id();
+ IllegalStateException failure = new IllegalStateException("boom");
+
+ when(txManager.stateMeta(txId)).thenReturn(new
TxStateMetaFinishing(null, null, false, null, failure, null));
+ tx.rollbackWithExceptionAsync(new
TransactionException(TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR,
+ "Transaction is already finished")).join();
+
+ Publisher<BinaryRow> publisher = internalTable.scan(VALID_PARTITION,
tx, VALID_INDEX_ID, IndexScanCriteria.unbounded());
+
+ CompletableFuture<Void> completed = new CompletableFuture<>();
+
+ publisher.subscribe(new BlackholeSubscriber(completed));
+
+ try {
+ completed.get(10, TimeUnit.SECONDS);
+ fail("Expected TransactionException but scan completed
successfully");
+ } catch (Exception e) {
+ Throwable unwrapped = unwrapCause(e);
+ assertThat("Error should be TransactionException", unwrapped,
is(instanceOf(TransactionException.class)));
+
+ TransactionException txEx = (TransactionException) unwrapped;
+ assertThat("Error code should be
TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR",
+ txEx.code(), is(TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR));
+ assertThat("Cause should be the recorded exception",
txEx.getCause(), is(failure));
+ }
+ }
+
/**
* Tests for label propagation from OperationContext to TxStateMeta.
*/
diff --git
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxRecoveryEngine.java
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxRecoveryEngine.java
index 787a933b1ac..f21bafb12a0 100644
---
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxRecoveryEngine.java
+++
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TxRecoveryEngine.java
@@ -28,6 +28,7 @@ import static
org.apache.ignite.internal.tx.TxState.isFinalState;
import static
org.apache.ignite.internal.tx.TxStateMetaFinishing.castToFinishing;
import static
org.apache.ignite.internal.util.CompletableFutures.nullCompletedFuture;
import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
+import static
org.apache.ignite.lang.ErrorGroups.Transactions.TX_ABORTED_DUE_TO_RECOVERY_ERR;
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ROLLBACK_ERR;
import java.util.Map;
@@ -89,13 +90,12 @@ public class TxRecoveryEngine {
// If the transaction state is pending, then the transaction should be
rolled back,
// meaning that the state is changed to aborted and a corresponding
cleanup request
// is sent in a common durable manner to a partition that has
initiated recovery.
- // TODO https://issues.apache.org/jira/browse/IGNITE-27386 the reason
of rollback needs to be explained.
return txManager.finish(
HybridTimestampTracker.emptyTracker(),
// Tx recovery is executed on the commit partition.
commitPartitionId,
false,
- new TransactionInternalException(TX_ROLLBACK_ERR,
format("Transaction has been aborted"
+ new
TransactionInternalException(TX_ABORTED_DUE_TO_RECOVERY_ERR,
format("Transaction has been aborted"
+ " due to transaction recovery {}.",
formatTxInfo(txId, txManager))),
true,
false,
diff --git
a/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxStateMetaTest.java
b/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxStateMetaTest.java
index 370c5b2e35e..1cd13f6e3c2 100644
---
a/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxStateMetaTest.java
+++
b/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxStateMetaTest.java
@@ -28,6 +28,7 @@ import static
org.junit.jupiter.params.provider.Arguments.argumentSet;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Stream;
+import org.apache.ignite.internal.lang.IgniteInternalException;
import org.apache.ignite.internal.replicator.ZonePartitionId;
import org.apache.ignite.internal.replicator.message.ReplicaMessagesFactory;
import org.apache.ignite.internal.tx.message.TxMessagesFactory;
@@ -147,6 +148,16 @@ public class TxStateMetaTest {
assertEquals(321,
message.asTxStateMetaAbandoned().lastExceptionErrorCode());
}
+ @Test
+ public void
testFinishingMetaDerivesLastExceptionErrorCodeFromFinishReason() {
+ IgniteInternalException finishReason = new
IgniteInternalException(321, "boom");
+
+ TxStateMetaFinishing meta = PENDING_META.finishing(finishReason);
+
+ assertEquals(finishReason, meta.lastException());
+ assertEquals(321, meta.lastExceptionErrorCode());
+ }
+
private static ArgumentSet args(
String name,
@Nullable TxStateMeta meta,