Skip to content

Commit 40eca02

Browse files
authored
IGNITE-27411 Added tests to prove that lost TX problem is gone (#7820)
1 parent eb4d96c commit 40eca02

9 files changed

Lines changed: 306 additions & 6 deletions

File tree

modules/api/src/main/java/org/apache/ignite/lang/ErrorGroups.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,9 @@ public static class Transactions {
479479

480480
/** Operation failed because the transaction is already finished due to an error. */
481481
public static final int TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR = TX_ERR_GROUP.registerErrorCode((short) 19);
482+
483+
/** Operation failed because the transaction is aborted due to a recovery. */
484+
public static final int TX_ABORTED_DUE_TO_RECOVERY_ERR = TX_ERR_GROUP.registerErrorCode((short) 20);
482485
}
483486

484487
/** Replicator error group. */

modules/platforms/cpp/ignite/common/error_codes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ enum class code : underlying_t {
141141
TX_DELAYED_ACK = 0x70011,
142142
TX_KILLED = 0x70012,
143143
TX_ALREADY_FINISHED_WITH_EXCEPTION = 0x70013,
144+
TX_ABORTED_DUE_TO_RECOVERY = 0x70014,
144145

145146
// Replicator group. Group code: 8
146147
REPLICA_COMMON = 0x80001,

modules/platforms/cpp/ignite/odbc/common_types.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ sql_state error_code_to_sql_state(error::code code) {
212212
case error::code::TX_DELAYED_ACK:
213213
case error::code::TX_KILLED:
214214
case error::code::TX_ALREADY_FINISHED_WITH_EXCEPTION:
215+
case error::code::TX_ABORTED_DUE_TO_RECOVERY:
215216
return sql_state::S25000_INVALID_TRANSACTION_STATE;
216217

217218
// Replicator group. Group code: 8

modules/platforms/dotnet/Apache.Ignite/ErrorCodes.g.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,9 @@ public static class Transactions
376376

377377
/// <summary> TxAlreadyFinishedWithException error. </summary>
378378
public const int TxAlreadyFinishedWithException = (GroupCode << 16) | (19 & 0xFFFF);
379+
380+
/// <summary> TxAbortedDueToRecovery error. </summary>
381+
public const int TxAbortedDueToRecovery = (GroupCode << 16) | (20 & 0xFFFF);
379382
}
380383

381384
/// <summary> Replicator errors. </summary>

modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/api/ItSqlApiBaseTest.java

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause;
2929
import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCode;
3030
import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
31+
import static org.apache.ignite.internal.util.ExceptionUtils.hasCause;
32+
import static org.apache.ignite.lang.ErrorGroups.Replicator.REPLICA_MISS_ERR;
3133
import static org.hamcrest.MatcherAssert.assertThat;
3234
import static org.hamcrest.Matchers.greaterThan;
3335
import static org.hamcrest.Matchers.hasSize;
@@ -37,6 +39,7 @@
3739
import static org.junit.jupiter.api.Assertions.assertNotNull;
3840
import static org.junit.jupiter.api.Assertions.assertTrue;
3941
import static org.junit.jupiter.api.Assertions.fail;
42+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
4043

4144
import java.time.Instant;
4245
import java.time.ZoneId;
@@ -51,17 +54,23 @@
5154
import java.util.stream.IntStream;
5255
import org.apache.calcite.rel.core.JoinRelType;
5356
import org.apache.ignite.Ignite;
57+
import org.apache.ignite.internal.app.IgniteImpl;
5458
import org.apache.ignite.internal.catalog.commands.CatalogUtils;
5559
import org.apache.ignite.internal.catalog.events.CatalogEvent;
5660
import org.apache.ignite.internal.catalog.events.CreateTableEventParameters;
61+
import org.apache.ignite.internal.client.tx.ClientLazyTransaction;
5762
import org.apache.ignite.internal.event.EventListener;
5863
import org.apache.ignite.internal.sql.BaseSqlIntegrationTest;
5964
import org.apache.ignite.internal.sql.ColumnMetadataImpl;
6065
import org.apache.ignite.internal.sql.ColumnMetadataImpl.ColumnOriginImpl;
6166
import org.apache.ignite.internal.sql.engine.QueryCancelledException;
6267
import org.apache.ignite.internal.sql.engine.exec.fsm.QueryInfo;
6368
import org.apache.ignite.internal.testframework.IgniteTestUtils;
69+
import org.apache.ignite.internal.tx.InternalTransaction;
6470
import org.apache.ignite.internal.tx.TxManager;
71+
import org.apache.ignite.internal.tx.TxState;
72+
import org.apache.ignite.internal.tx.TxStateMeta;
73+
import org.apache.ignite.internal.tx.message.TxMessageGroup;
6574
import org.apache.ignite.internal.util.CompletableFutures;
6675
import org.apache.ignite.lang.CancelHandle;
6776
import org.apache.ignite.lang.CancellationToken;
@@ -83,7 +92,9 @@
8392
import org.apache.ignite.sql.Statement;
8493
import org.apache.ignite.sql.Statement.StatementBuilder;
8594
import org.apache.ignite.tx.Transaction;
95+
import org.apache.ignite.tx.TransactionException;
8696
import org.apache.ignite.tx.TransactionOptions;
97+
import org.awaitility.Awaitility;
8798
import org.hamcrest.Matcher;
8899
import org.jetbrains.annotations.Nullable;
89100
import org.junit.jupiter.api.AfterEach;
@@ -740,6 +751,175 @@ public void runtimeErrorInQueryCausesTransactionToFail(String query) {
740751
"Transaction is already finished due to an error");
741752
}
742753

754+
@Test
755+
public void runtimeErrorReturnsSameTransactionErrorBeforeAndAfterRollbackCompletion() throws Exception {
756+
sql("CREATE TABLE tst(id INTEGER PRIMARY KEY, val INTEGER)");
757+
758+
IgniteSql sql = igniteSql();
759+
760+
Transaction tx = igniteTx().begin();
761+
762+
// Enlist enough operations to make rollback non-trivial.
763+
for (int i = 0; i < 100; i++) {
764+
execute(tx, sql, "INSERT INTO tst VALUES (?, ?)", i, i);
765+
}
766+
767+
UUID txId = txId(tx);
768+
769+
assertThrowsSqlException(
770+
Sql.RUNTIME_ERR,
771+
"Division by zero",
772+
() -> execute(tx, sql, "SELECT val / 0 FROM tst WHERE id = ?", 0)
773+
);
774+
775+
IgniteException[] immediateExceptions = new IgniteException[5];
776+
for (int i = 0; i < immediateExceptions.length; i++) {
777+
immediateExceptions[i] = (IgniteException) assertThrowsWithCause(
778+
() -> executeForRead(sql, tx, "SELECT * FROM tst WHERE id = ?", 1),
779+
IgniteException.class
780+
);
781+
}
782+
783+
if (tx instanceof InternalTransaction) {
784+
assertNotNull(txId, "Expected transaction id for test transaction implementation");
785+
786+
Awaitility.await()
787+
.atMost(5, TimeUnit.SECONDS)
788+
.until(() -> {
789+
TxStateMeta meta = txManager().stateMeta(txId);
790+
791+
return meta != null && TxState.isFinalState(meta.txState());
792+
});
793+
}
794+
795+
IgniteException abortedStateException = (IgniteException) assertThrowsWithCause(
796+
() -> executeForRead(sql, tx, "SELECT * FROM tst WHERE id = ?", 1),
797+
IgniteException.class
798+
);
799+
800+
assertEquals(Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR, abortedStateException.code());
801+
assertTrue(abortedStateException.getMessage().contains("Transaction is already finished due to an error"));
802+
803+
for (IgniteException immediateException : immediateExceptions) {
804+
assertEquals(abortedStateException.code(), immediateException.code());
805+
assertTrue(immediateException.getMessage().contains("Transaction is already finished due to an error"));
806+
}
807+
}
808+
809+
@Test
810+
public void secondRequestDuringRollbackReturnsFinishedWithExceptionAndPreservesOriginalCause() {
811+
sql("CREATE TABLE tst(id INTEGER PRIMARY KEY, val INTEGER)");
812+
sql("INSERT INTO tst VALUES (0, 1)");
813+
814+
IgniteSql sql = igniteSql();
815+
816+
Transaction tx = igniteTx().begin();
817+
818+
List<IgniteImpl> clusterNodes = CLUSTER.runningNodes()
819+
.map(node -> unwrapIgniteImpl(node))
820+
.collect(toList());
821+
822+
CompletableFuture<Void> failingRequestStarted = new CompletableFuture<>();
823+
CompletableFuture<Void> finishRequestBlocked = new CompletableFuture<>();
824+
CompletableFuture<Void> releaseFinishRequest = new CompletableFuture<>();
825+
826+
for (IgniteImpl clusterNode : clusterNodes) {
827+
// Install predicates in cluster
828+
clusterNode.dropMessages((recipientConsistentId, msg) -> {
829+
if (!failingRequestStarted.isDone()) {
830+
return false;
831+
}
832+
833+
if (msg.groupType() != TxMessageGroup.GROUP_TYPE
834+
|| msg.messageType() != TxMessageGroup.TX_FINISH_REQUEST) {
835+
return false;
836+
}
837+
838+
finishRequestBlocked.complete(null);
839+
840+
return !releaseFinishRequest.isDone();
841+
});
842+
}
843+
844+
try {
845+
CompletableFuture<IgniteException> failingRequestFut = IgniteTestUtils.runAsync(() -> {
846+
failingRequestStarted.complete(null);
847+
848+
IgniteException ex = assertInstanceOf(
849+
IgniteException.class,
850+
assertThrowsWithCause(
851+
() -> execute(tx, sql, "SELECT val / 0 FROM tst WHERE id = ?", 0),
852+
IgniteException.class
853+
)
854+
);
855+
856+
assertTrue(hasCause(ex, "Division by zero", Throwable.class));
857+
assertTrue(
858+
ex.code() == Sql.RUNTIME_ERR || ex.code() == Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR,
859+
"Unexpected code for a request that triggers rollback [code=" + ex.code() + ']'
860+
);
861+
862+
return ex;
863+
});
864+
865+
Awaitility.await()
866+
.atMost(5, TimeUnit.SECONDS)
867+
.until(finishRequestBlocked::isDone);
868+
869+
IgniteException parallelRequestException = assertInstanceOf(
870+
IgniteException.class,
871+
assertThrowsWithCause(
872+
() -> executeForRead(sql, tx, "SELECT * FROM tst WHERE id = ?", 0),
873+
IgniteException.class
874+
)
875+
);
876+
877+
assertEquals(Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR, parallelRequestException.code());
878+
assertTrue(parallelRequestException.getMessage().contains("Transaction is already finished due to an error"));
879+
assertTrue(
880+
hasCause(parallelRequestException, "Division by zero", Throwable.class),
881+
"Expected original rollback cause in user-visible exception chain"
882+
);
883+
884+
releaseFinishRequest.complete(null);
885+
886+
IgniteException firstRequestException = await(failingRequestFut);
887+
888+
assertTrue(hasCause(firstRequestException, "Division by zero", Throwable.class));
889+
} finally {
890+
clusterNodes.forEach(IgniteImpl::stopDroppingMessages);
891+
}
892+
}
893+
894+
@Test
895+
public void rollbackWithExceptionCauseIsPropagatedToSubsequentSqlRequest() {
896+
sql("CREATE TABLE tst(id INTEGER PRIMARY KEY, val INTEGER)");
897+
sql("INSERT INTO tst VALUES (?, ?)", 1, 1);
898+
899+
Transaction tx = igniteTx().begin();
900+
901+
assumeTrue(tx instanceof InternalTransaction, "InternalTransaction is required");
902+
903+
InternalTransaction internalTx = (InternalTransaction) tx;
904+
String rollbackCauseMessage = "rollback-cause-primary-replica-changed";
905+
TransactionException rollbackCause = new TransactionException(REPLICA_MISS_ERR, rollbackCauseMessage);
906+
907+
await(internalTx.rollbackWithExceptionAsync(rollbackCause));
908+
909+
IgniteException ex = assertInstanceOf(
910+
IgniteException.class,
911+
assertThrowsWithCause(
912+
() -> executeForRead(igniteSql(), tx, "SELECT * FROM tst WHERE id = ?", 1),
913+
IgniteException.class
914+
)
915+
);
916+
917+
assertEquals(Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR, ex.code());
918+
assertTrue(ex.getMessage().contains("Transaction is already finished due to an error"));
919+
assertTrue(hasCause(ex, TransactionException.class));
920+
assertTrue(hasCause(ex, rollbackCauseMessage, Throwable.class), "Expected rollback cause message in user-visible exception chain");
921+
}
922+
743923
@Test
744924
public void testLockIsNotReleasedAfterTxRollback() {
745925
IgniteSql sql = igniteSql();
@@ -1413,6 +1593,18 @@ protected ResultSet<SqlRow> executeForRead(IgniteSql sql, @Nullable Transaction
14131593

14141594
protected abstract ResultSet<SqlRow> executeForRead(IgniteSql sql, @Nullable Transaction tx, Statement statement, Object... args);
14151595

1596+
private static @Nullable UUID txId(Transaction tx) {
1597+
if (tx instanceof InternalTransaction) {
1598+
return ((InternalTransaction) tx).id();
1599+
}
1600+
1601+
if (tx instanceof ClientLazyTransaction) {
1602+
return ((ClientLazyTransaction) tx).startedTx().txId();
1603+
}
1604+
1605+
return null;
1606+
}
1607+
14161608
protected void checkSqlError(
14171609
int code,
14181610
String msg,

modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/QueryTransactionWrapperSelfTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.ignite.internal.sql.engine;
1919

2020
import static org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException;
21+
import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR;
2122
import static org.hamcrest.MatcherAssert.assertThat;
2223
import static org.hamcrest.Matchers.equalTo;
2324
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -34,6 +35,7 @@
3435
import java.util.Set;
3536
import java.util.UUID;
3637
import org.apache.ignite.internal.hlc.HybridTimestampTracker;
38+
import org.apache.ignite.internal.lang.IgniteInternalException;
3739
import org.apache.ignite.internal.sql.engine.framework.NoOpTransaction;
3840
import org.apache.ignite.internal.sql.engine.sql.IgniteSqlCommitTransaction;
3941
import org.apache.ignite.internal.sql.engine.sql.IgniteSqlStartTransaction;
@@ -45,8 +47,11 @@
4547
import org.apache.ignite.internal.sql.engine.tx.ScriptTransactionContext;
4648
import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
4749
import org.apache.ignite.internal.tx.TxManager;
50+
import org.apache.ignite.internal.tx.TxStateMeta;
51+
import org.apache.ignite.internal.tx.TxStateMetaFinishing;
4852
import org.apache.ignite.internal.tx.impl.TransactionInflights;
4953
import org.apache.ignite.lang.ErrorGroups.Sql;
54+
import org.apache.ignite.tx.TransactionException;
5055
import org.junit.jupiter.api.Test;
5156
import org.junit.jupiter.api.extension.ExtendWith;
5257
import org.mockito.Mock;
@@ -231,6 +236,48 @@ public void testScriptTransactionWrapperTxInflightsInteraction() {
231236
assertEquals(1, inflights.size());
232237
}
233238

239+
@Test
240+
public void testInflightTrackerUsesFinishedWithErrorClassificationForFinishingTx() {
241+
NoOpTransaction tx = NoOpTransaction.readWrite("test-rw", false);
242+
IgniteInternalException failure = new IgniteInternalException(321, "boom");
243+
TxStateMeta finishingMeta = new TxStateMetaFinishing(null, null, false, null, failure, failure.code());
244+
245+
when(transactionInflights.track(tx.id())).thenReturn(false);
246+
when(txManager.stateMeta(tx.id())).thenReturn(finishingMeta);
247+
248+
TransactionException ex = assertThrowsExactly(
249+
TransactionException.class,
250+
() -> new InflightTransactionalOperationTracker(transactionInflights, txManager).registerOperationStart(tx)
251+
);
252+
253+
assertEquals(TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR, ex.code());
254+
assertEquals(failure, ex.getCause());
255+
}
256+
257+
@Test
258+
public void testExplicitSqlTransactionUsesFinishedWithErrorClassificationForFinishingTx() {
259+
NoOpTransaction tx = NoOpTransaction.readWrite("test-rw", false);
260+
IgniteInternalException failure = new IgniteInternalException(321, "boom");
261+
TxStateMeta finishingMeta = new TxStateMetaFinishing(null, null, false, null, failure, failure.code());
262+
263+
when(txManager.stateMeta(tx.id())).thenReturn(finishingMeta);
264+
265+
QueryTransactionContext txCtx = new QueryTransactionContextImpl(
266+
txManager,
267+
observableTimeTracker,
268+
tx,
269+
new InflightTransactionalOperationTracker(transactionInflights, txManager)
270+
);
271+
272+
TransactionException ex = assertThrowsExactly(
273+
TransactionException.class,
274+
() -> txCtx.getOrStartSqlManaged(false, false)
275+
);
276+
277+
assertEquals(TX_ALREADY_FINISHED_WITH_EXCEPTION_ERR, ex.code());
278+
assertEquals(failure, ex.getCause());
279+
}
280+
234281
private void prepareTransactionsMocks() {
235282
when(txManager.beginExplicit(any(), anyBoolean(), any())).thenAnswer(
236283
inv -> {

0 commit comments

Comments
 (0)