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

rpuch 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 2b7dbb6643 IGNITE-22710 Add exception translation to Transaction 
implementations (#4070)
2b7dbb6643 is described below

commit 2b7dbb6643ba18c0dad17b9ddb5a750dd8c1e39d
Author: Roman Puchkovskiy <[email protected]>
AuthorDate: Tue Jul 16 22:42:39 2024 +0400

    IGNITE-22710 Add exception translation to Transaction implementations 
(#4070)
---
 .../internal/lang/IgniteExceptionMapperUtil.java   | 50 +++++++++++--
 .../org/apache/ignite/internal/util/ViewUtils.java |  1 +
 .../ignite/internal/lang/ExceptionUtilsTest.java   | 56 ++++++++++++++
 .../asserts/CompletableFutureAssert.java           | 29 +++++++-
 .../ItSchemaForwardCompatibilityTest.java          |  3 +-
 .../schemasync/ItSchemaSyncSingleNodeTest.java     | 27 ++++---
 .../ignite/internal/table/ItDurableFinishTest.java |  2 +-
 .../internal/table/ItTransactionRecoveryTest.java  |  5 +-
 .../replicator/IncompatibleSchemaException.java    |  1 +
 .../StaleTransactionOperationException.java        |  5 +-
 .../tx/MismatchingTransactionOutcomeException.java |  5 +-
 .../internal/tx/TransactionInternalException.java  | 81 +++++++++++++++++++++
 .../tx/impl/IgniteAbstractTransactionImpl.java     | 31 +++++++-
 .../tx/impl/TransactionsExceptionMapperUtil.java   | 85 ++++++++++++++++++++++
 .../apache/ignite/internal/tx/TxManagerTest.java   |  7 +-
 15 files changed, 356 insertions(+), 32 deletions(-)

diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/lang/IgniteExceptionMapperUtil.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/lang/IgniteExceptionMapperUtil.java
index b77c94023b..f830a65bc7 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/lang/IgniteExceptionMapperUtil.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/lang/IgniteExceptionMapperUtil.java
@@ -27,6 +27,7 @@ import java.util.Map;
 import java.util.ServiceLoader;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
+import java.util.function.Function;
 import org.apache.ignite.lang.ErrorGroups.Common;
 import org.apache.ignite.lang.IgniteCheckedException;
 import org.apache.ignite.lang.IgniteException;
@@ -84,9 +85,29 @@ public class IgniteExceptionMapperUtil {
      * @return Public exception.
      */
     public static Throwable mapToPublicException(Throwable origin) {
+        return mapToPublicException(origin, ex -> new 
IgniteException(INTERNAL_ERR, ex));
+    }
+
+    /**
+     * This method provides a mapping from internal exception to Ignite public 
ones.
+     *
+     * <p>The rules of mapping are the following:</p>
+     * <ul>
+     *     <li>any instance of {@link Error} is returned as is, except {@link 
AssertionError}
+     *     that will always be mapped to {@link IgniteException} with the 
{@link Common#INTERNAL_ERR} error code.</li>
+     *     <li>any instance of {@link IgniteException} or {@link 
IgniteCheckedException} is returned as is.</li>
+     *     <li>if there are no any mappers that can do a mapping from the 
given error to a public exception,
+     *     then unknownProblemMapper is used to map the exception.</li>
+     * </ul>
+     *
+     * @param origin Exception to be mapped.
+     * @param unknownProblemMapper Mapper used to map an unknown exception.
+     * @return Public exception.
+     */
+    public static Throwable mapToPublicException(Throwable origin, 
Function<Throwable, Throwable> unknownProblemMapper) {
         if (origin instanceof Error) {
             if (origin instanceof AssertionError) {
-                return new IgniteException(INTERNAL_ERR, origin);
+                return mapCheckingResultIsPublic(origin, unknownProblemMapper);
             }
             return origin;
         }
@@ -103,9 +124,7 @@ public class IgniteExceptionMapperUtil {
         if (m != null) {
             res = map(m, origin);
 
-            assert res instanceof IgniteException || res instanceof 
IgniteCheckedException :
-                    "Unexpected mapping of internal exception to a public one 
[origin=" + origin + ", mapped=" + res + ']';
-
+            checkResultIsPublic(res, origin);
         } else {
             res = origin;
         }
@@ -115,7 +134,28 @@ public class IgniteExceptionMapperUtil {
         }
 
         // There are no exception mappings for the given exception. This case 
should be considered as internal error.
-        return new IgniteException(INTERNAL_ERR, origin);
+        return mapCheckingResultIsPublic(origin, unknownProblemMapper);
+    }
+
+    private static Throwable mapCheckingResultIsPublic(Throwable origin, 
Function<Throwable, Throwable> unknownProblemMapper) {
+        Throwable result = unknownProblemMapper.apply(origin);
+
+        checkResultIsPublic(result, origin);
+
+        return result;
+    }
+
+    private static void checkResultIsPublic(Throwable mappingResult, Throwable 
origin) {
+        assert mappingResult instanceof IgniteException || mappingResult 
instanceof IgniteCheckedException
+                : "Unexpected mapping of internal exception to a public one 
[origin=" + origin + ", mapped=" + mappingResult
+                + "]: not a public exception";
+        assert isPublicPackage(mappingResult.getClass().getPackage())
+                : "Unexpected mapping of internal exception to a public one 
[origin=" + origin + ", mapped=" + mappingResult
+                + "]: exception is not defined in a public package";
+    }
+
+    private static boolean isPublicPackage(Package somePackage) {
+        return !somePackage.getName().startsWith("org.apache.ignite.internal");
     }
 
     /**
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/ViewUtils.java 
b/modules/core/src/main/java/org/apache/ignite/internal/util/ViewUtils.java
index da3ff46506..6e2ed70096 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/ViewUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/ViewUtils.java
@@ -86,6 +86,7 @@ public final class ViewUtils {
      * @param e Exception.
      * @return Properly copied exception or a new error, if exception can not 
be copied.
      */
+    // TODO: consider removing after IGNITE-22721 gets resolved.
     private static <T extends Throwable & TraceableException> Throwable 
copyExceptionWithCauseIfPossible(T e) {
         Throwable copy = ExceptionUtils.copyExceptionWithCause(e.getClass(), 
e.traceId(), e.code(), e.getMessage(), e);
         if (copy != null) {
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/lang/ExceptionUtilsTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/lang/ExceptionUtilsTest.java
index 88e26ee2df..c9f126a1da 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/lang/ExceptionUtilsTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/lang/ExceptionUtilsTest.java
@@ -22,11 +22,16 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.isA;
 
+import java.util.UUID;
 import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Stream;
 import org.apache.ignite.internal.util.ExceptionUtils;
+import org.apache.ignite.lang.ErrorGroups.Transactions;
 import org.apache.ignite.lang.IgniteCheckedException;
 import org.apache.ignite.lang.IgniteException;
+import org.apache.ignite.lang.TraceableException;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
@@ -90,6 +95,50 @@ public class ExceptionUtilsTest {
         );
     }
 
+    @Test
+    void withCauseDoesNotApplyDefaultCodeWhenCodeIsThere() {
+        TraceableException translated = ExceptionUtils.withCause(
+                TestUncheckedExceptionWithTraceCodeAndCause::new,
+                Transactions.TX_COMMIT_ERR,
+                new IgniteException(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR)
+        );
+
+        assertThat(translated.code(), 
is(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR));
+    }
+
+    @Test
+    void 
withCauseDoesNotApplyDefaultCodeWhenCodeIsInExceptionWrappedInExecutionException()
 {
+        TraceableException translated = ExceptionUtils.withCause(
+                TestUncheckedExceptionWithTraceCodeAndCause::new,
+                Transactions.TX_COMMIT_ERR,
+                new ExecutionException(new 
IgniteException(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR))
+        );
+
+        assertThat(translated.code(), 
is(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR));
+    }
+
+    @Test
+    void 
withCauseDoesNotApplyDefaultCodeWhenCodeIsInExceptionWrappedInCompletionException()
 {
+        TraceableException translated = ExceptionUtils.withCause(
+                TestUncheckedExceptionWithTraceCodeAndCause::new,
+                Transactions.TX_COMMIT_ERR,
+                new CompletionException(new 
IgniteException(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR))
+        );
+
+        assertThat(translated.code(), 
is(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR));
+    }
+
+    @Test
+    void withCauseAppliesDefaultCodeWhenHandlingNonIgniteException() {
+        TraceableException translated = ExceptionUtils.withCause(
+                TestUncheckedExceptionWithTraceCodeAndCause::new,
+                Transactions.TX_COMMIT_ERR,
+                new RuntimeException()
+        );
+
+        assertThat(translated.code(), is(Transactions.TX_COMMIT_ERR));
+    }
+
     /** Test exception class. */
     public static class TestException extends IgniteException {
         public TestException() {
@@ -187,4 +236,11 @@ public class ExceptionUtilsTest {
             super(code, message, cause);
         }
     }
+
+    /** Test exception class. */
+    public static class TestUncheckedExceptionWithTraceCodeAndCause extends 
IgniteException {
+        public TestUncheckedExceptionWithTraceCodeAndCause(UUID traceId, int 
code, Throwable cause) {
+            super(traceId, code, cause);
+        }
+    }
 }
diff --git 
a/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/asserts/CompletableFutureAssert.java
 
b/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/asserts/CompletableFutureAssert.java
index cac1d9fa20..db7176760b 100644
--- 
a/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/asserts/CompletableFutureAssert.java
+++ 
b/modules/core/src/testFixtures/java/org/apache/ignite/internal/testframework/asserts/CompletableFutureAssert.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.testframework.asserts;
 
+import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import java.util.concurrent.CompletableFuture;
@@ -44,13 +45,37 @@ public class CompletableFutureAssert {
     public static <X extends Throwable> X assertWillThrowFast(
             CompletableFuture<?> future,
             Class<X> expectedExceptionClass
+    ) {
+        return assertWillThrow(future, expectedExceptionClass, 1, SECONDS);
+    }
+
+    /**
+     * Asserts that the given future completes with an exception being an 
instance of the given class (in time) and returns
+     * that exception for further examination.
+     *
+     * <p>Unlike
+     * {@link 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher#willThrow(Class,
 int, TimeUnit)},
+     * this method allows to examine the actual exception thrown further in 
the test.
+     *
+     * @param future Future to work on.
+     * @param expectedExceptionClass Expected class of the exception.
+     * @param timeout Duration to wait for future completion.
+     * @param timeUnit Time unit of the duration.
+     * @param <X> Exception type.
+     * @return Matched exception.
+     */
+    public static <X extends Throwable> X assertWillThrow(
+            CompletableFuture<?> future,
+            Class<X> expectedExceptionClass,
+            long timeout,
+            TimeUnit timeUnit
     ) {
         Object normalResult;
 
         try {
-            normalResult = future.get(1, TimeUnit.SECONDS);
+            normalResult = future.get(timeout, timeUnit);
         } catch (TimeoutException e) {
-            return fail("Expected the future to be completed with an exception 
of class in 1 second, but it did not complete in time");
+            return fail("Expected the future to be completed with an exception 
of class in time, but it did not");
         } catch (Throwable e) {
             Throwable unwrapped = unwrapThrowable(e);
 
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/schemasync/ItSchemaForwardCompatibilityTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/schemasync/ItSchemaForwardCompatibilityTest.java
index 47d2e0054e..4b130b40d9 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/schemasync/ItSchemaForwardCompatibilityTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/schemasync/ItSchemaForwardCompatibilityTest.java
@@ -114,7 +114,8 @@ class ItSchemaForwardCompatibilityTest extends 
ClusterPerTestIntegrationTest {
                 ))
         );
 
-        assertThat(ex.code(), is(Transactions.TX_UNEXPECTED_STATE_ERR));
+        // TODO: IGNITE-20415 - assert that the failure is because of a 
changed schema.
+        assertThat(ex.code(), is(Transactions.TX_COMMIT_ERR));
 
         assertThat(tx.state(), is(TxState.ABORTED));
     }
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/schemasync/ItSchemaSyncSingleNodeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/schemasync/ItSchemaSyncSingleNodeTest.java
index cb37e8f382..093693fe06 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/schemasync/ItSchemaSyncSingleNodeTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/schemasync/ItSchemaSyncSingleNodeTest.java
@@ -100,12 +100,14 @@ class ItSchemaSyncSingleNodeTest extends 
ClusterPerTestIntegrationTest {
 
         alterTable(TABLE_NAME);
 
-        IgniteException ex;
+        int errorCode;
 
         int tableId = unwrapTableViewInternal(table).tableId();
 
         if (operation.sql()) {
-            ex = assertThrows(IgniteException.class, () -> 
operation.execute(table, tx, cluster));
+            IgniteException ex = assertThrows(IgniteException.class, () -> 
operation.execute(table, tx, cluster));
+            errorCode = ex.code();
+
             assertThat(
                     ex.getMessage(),
                     containsString(String.format(
@@ -114,7 +116,9 @@ class ItSchemaSyncSingleNodeTest extends 
ClusterPerTestIntegrationTest {
                     ))
             );
         } else {
-            ex = assertThrows(IncompatibleSchemaException.class, () -> 
operation.execute(table, tx, cluster));
+            IncompatibleSchemaException ex = 
assertThrows(IncompatibleSchemaException.class, () -> operation.execute(table, 
tx, cluster));
+            errorCode = ex.code();
+
             assertThat(
                     ex.getMessage(),
                     is(String.format(
@@ -124,7 +128,7 @@ class ItSchemaSyncSingleNodeTest extends 
ClusterPerTestIntegrationTest {
             );
         }
 
-        assertThat(ex.code(), is(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR));
+        assertThat(errorCode, is(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR));
 
         assertThat(tx.state(), is(TxState.ABORTED));
     }
@@ -244,19 +248,23 @@ class ItSchemaSyncSingleNodeTest extends 
ClusterPerTestIntegrationTest {
 
         dropTable(TABLE_NAME);
 
-        IgniteException ex;
+        int errorCode;
 
         int tableId = unwrapTableViewInternal(table).tableId();
 
         if (operation.sql()) {
-            ex = assertThrows(IgniteException.class, () -> 
operation.execute(table, tx, cluster));
+            IgniteException ex = assertThrows(IgniteException.class, () -> 
operation.execute(table, tx, cluster));
+            errorCode = ex.code();
+
             assertThat(
                     ex.getMessage(),
                     // TODO https://issues.apache.org/jira/browse/IGNITE-22309 
use tableName instead
                     containsString(String.format("Table was dropped 
[tableId=%s]", tableId))
             );
         } else {
-            ex = assertThrows(IncompatibleSchemaException.class, () -> 
operation.execute(table, tx, cluster));
+            IncompatibleSchemaException ex = 
assertThrows(IncompatibleSchemaException.class, () -> operation.execute(table, 
tx, cluster));
+            errorCode = ex.code();
+
             assertThat(
                     ex.getMessage(),
                     // TODO https://issues.apache.org/jira/browse/IGNITE-22309 
use tableName instead
@@ -264,7 +272,7 @@ class ItSchemaSyncSingleNodeTest extends 
ClusterPerTestIntegrationTest {
             );
         }
 
-        assertThat(ex.code(), is(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR));
+        assertThat(errorCode, is(Transactions.TX_INCOMPATIBLE_SCHEMA_ERR));
 
         assertThat(tx.state(), is(TxState.ABORTED));
     }
@@ -299,7 +307,8 @@ class ItSchemaSyncSingleNodeTest extends 
ClusterPerTestIntegrationTest {
                 containsString(String.format("Commit failed because a table 
was already dropped [table=%s]", table.name()))
         );
 
-        assertThat(((TransactionException) ex).code(), 
is(Transactions.TX_UNEXPECTED_STATE_ERR));
+        // TODO: IGNITE-20415 - assert that the failure is because of a 
changed schema.
+        assertThat(((TransactionException) ex).code(), 
is(Transactions.TX_COMMIT_ERR));
 
         assertThat(tx.state(), is(TxState.ABORTED));
     }
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItDurableFinishTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItDurableFinishTest.java
index 4055bcba2d..227b28e14d 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItDurableFinishTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItDurableFinishTest.java
@@ -310,7 +310,7 @@ public class ItDurableFinishTest extends 
ClusterPerTestIntegrationTest {
 
         Throwable cause = 
ExceptionUtils.unwrapCause(transactionException.getCause());
 
-        assertInstanceOf(MismatchingTransactionOutcomeException.class, cause);
+        assertInstanceOf(MismatchingTransactionOutcomeException.class, 
cause.getCause());
     }
 
     private void markTxAbortedInTxStateStorage(IgniteImpl primaryNode, 
InternalTransaction tx, Table publicTable) {
diff --git 
a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItTransactionRecoveryTest.java
 
b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItTransactionRecoveryTest.java
index eec1eb4745..76cad253f5 100644
--- 
a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItTransactionRecoveryTest.java
+++ 
b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItTransactionRecoveryTest.java
@@ -23,12 +23,14 @@ import static 
org.apache.ignite.internal.TestWrappers.unwrapTableImpl;
 import static 
org.apache.ignite.internal.catalog.CatalogService.DEFAULT_STORAGE_PROFILE;
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.bypassingThreadAssertions;
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static 
org.apache.ignite.internal.testframework.asserts.CompletableFutureAssert.assertWillThrow;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrow;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
 import static 
org.apache.ignite.internal.tx.impl.ResourceVacuumManager.RESOURCE_VACUUM_INTERVAL_MILLISECONDS_PROPERTY;
 import static 
org.apache.ignite.internal.tx.test.ItTransactionTestUtils.waitAndGetPrimaryReplica;
 import static org.apache.ignite.internal.util.ExceptionUtils.extractCodeFrom;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -789,7 +791,8 @@ public class ItTransactionRecoveryTest extends 
ClusterPerTestIntegrationTest {
 
         cancelLease(commitPartNode, tblReplicationGrp);
 
-        assertThat(commitFut, 
willThrow(MismatchingTransactionOutcomeException.class, 30, SECONDS));
+        TransactionException txEx = assertWillThrow(commitFut, 
TransactionException.class, 30, SECONDS);
+        assertThat(txEx.getCause(), 
instanceOf(MismatchingTransactionOutcomeException.class));
 
         RecordView<Tuple> view = 
txCrdNode.tables().table(TABLE_NAME).recordView();
 
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/IncompatibleSchemaException.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/IncompatibleSchemaException.java
index 5c4f0027f8..99f20b5fda 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/IncompatibleSchemaException.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/IncompatibleSchemaException.java
@@ -25,6 +25,7 @@ import org.apache.ignite.tx.TransactionException;
  * Thrown when, during an attempt to execute a transactional operation, it 
turns out that the operation cannot be executed
  * because an incompatible schema change has happened.
  */
+// TODO: IGNITE-20415 - make this extend TransactionInternalException.
 public class IncompatibleSchemaException extends TransactionException 
implements ExpectedReplicationException {
     private static final String SCHEMA_CHANGED_MESSAGE = "Table schema was 
updated after the transaction was started "
             + "[table=%s, startSchema=%d, operationSchema=%d]";
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/StaleTransactionOperationException.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/StaleTransactionOperationException.java
index 31055db028..1dab412af1 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/StaleTransactionOperationException.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/distributed/replicator/StaleTransactionOperationException.java
@@ -21,10 +21,11 @@ import static 
org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static 
org.apache.ignite.lang.ErrorGroups.Transactions.TX_STALE_OPERATION_ERR;
 
 import java.util.UUID;
-import org.apache.ignite.tx.TransactionException;
+import org.apache.ignite.internal.tx.TransactionInternalException;
 
 /** Error that occurs when a stale operation of a completed transaction is 
detected. */
-public class StaleTransactionOperationException extends TransactionException {
+// TODO: IGNITE-20415 - make this exception public.
+public class StaleTransactionOperationException extends 
TransactionInternalException {
     /**
      * Constructor.
      *
diff --git 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/MismatchingTransactionOutcomeException.java
 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/MismatchingTransactionOutcomeException.java
index 5ae4ea70d3..6fc15f87c7 100644
--- 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/MismatchingTransactionOutcomeException.java
+++ 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/MismatchingTransactionOutcomeException.java
@@ -19,15 +19,14 @@ package org.apache.ignite.internal.tx;
 
 import static 
org.apache.ignite.lang.ErrorGroups.Transactions.TX_UNEXPECTED_STATE_ERR;
 
-import org.apache.ignite.tx.TransactionException;
-
 /**
  * The exception is thrown when the transaction result differs from the 
intended one.
  *
  * <p>For example, {@code tx.commit()} is called for a transaction, but the 
verification logic decided to abort it instead. The transaction
  * will be finished with {@link TxState#ABORTED} and the call to {@code 
tx.commit()} will throw this exception.
  */
-public class MismatchingTransactionOutcomeException extends 
TransactionException {
+// TODO: IGNITE-20415 - split this into public exception (in a public package) 
and internal exception (carrying internal state).
+public class MismatchingTransactionOutcomeException extends 
TransactionInternalException {
 
     private static final long serialVersionUID = -7953057695915339651L;
 
diff --git 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/TransactionInternalException.java
 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/TransactionInternalException.java
new file mode 100644
index 0000000000..8a726212c3
--- /dev/null
+++ 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/TransactionInternalException.java
@@ -0,0 +1,81 @@
+/*
+ * 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.internal.tx;
+
+import java.util.UUID;
+import org.apache.ignite.internal.lang.IgniteInternalException;
+
+/**
+ * An internal exception from the transaction subsystem.
+ */
+public class TransactionInternalException extends IgniteInternalException {
+    /**
+     * Creates a new transaction exception with the given error code and cause.
+     *
+     * @param code Full error code.
+     * @param cause Optional nested exception (can be {@code null}).
+     */
+    public TransactionInternalException(int code, Throwable cause) {
+        super(code, cause);
+    }
+
+    /**
+     * Creates a new transaction exception with the given error code and 
detail message.
+     *
+     * @param code Full error code.
+     * @param message Detail message.
+     */
+    public TransactionInternalException(int code, String message) {
+        super(code, message);
+    }
+
+
+    /**
+     * Creates a new transaction exception with the given trace id, error code 
and cause.
+     *
+     * @param traceId Unique identifier of this exception.
+     * @param code Full error code.
+     * @param cause Optional nested exception (can be {@code null}).
+     */
+    public TransactionInternalException(UUID traceId, int code, Throwable 
cause) {
+        super(traceId, code, cause);
+    }
+
+    /**
+     * Creates a new transaction exception with the given error code, detail 
message and cause.
+     *
+     * @param code Full error code.
+     * @param message Detail message.
+     * @param cause Optional nested exception (can be {@code null}).
+     */
+    public TransactionInternalException(int code, String message, Throwable 
cause) {
+        super(code, message, cause);
+    }
+
+    /**
+     * Creates a new transaction exception with the given trace id, error 
code, detail message and cause.
+     *
+     * @param traceId Unique identifier of this exception.
+     * @param code Full error code.
+     * @param message Detail message.
+     * @param cause Optional nested exception (can be {@code null}).
+     */
+    public TransactionInternalException(UUID traceId, int code, String 
message, Throwable cause) {
+        super(traceId, code, message, cause);
+    }
+}
diff --git 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/IgniteAbstractTransactionImpl.java
 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/IgniteAbstractTransactionImpl.java
index 699a4ff803..c90d06e7d6 100644
--- 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/IgniteAbstractTransactionImpl.java
+++ 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/IgniteAbstractTransactionImpl.java
@@ -17,12 +17,16 @@
 
 package org.apache.ignite.internal.tx.impl;
 
+import static 
org.apache.ignite.internal.util.ExceptionUtils.copyExceptionWithCause;
+import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 import static org.apache.ignite.internal.util.ExceptionUtils.withCause;
+import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
 import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_COMMIT_ERR;
 import static org.apache.ignite.lang.ErrorGroups.Transactions.TX_ROLLBACK_ERR;
 
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.internal.tx.TxManager;
 import org.apache.ignite.internal.tx.TxState;
@@ -87,7 +91,11 @@ public abstract class IgniteAbstractTransactionImpl 
implements InternalTransacti
     public void commit() throws TransactionException {
         try {
             commitAsync().get();
-        } catch (Exception e) {
+        } catch (ExecutionException e) {
+            throw sneakyThrow(tryToCopyExceptionWithCause(e));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+
             throw withCause(TransactionException::new, TX_COMMIT_ERR, e);
         }
     }
@@ -95,7 +103,7 @@ public abstract class IgniteAbstractTransactionImpl 
implements InternalTransacti
     /** {@inheritDoc} */
     @Override
     public CompletableFuture<Void> commitAsync() {
-        return finish(true);
+        return 
TransactionsExceptionMapperUtil.convertToPublicFuture(finish(true), 
TX_COMMIT_ERR);
     }
 
     /** {@inheritDoc} */
@@ -103,7 +111,11 @@ public abstract class IgniteAbstractTransactionImpl 
implements InternalTransacti
     public void rollback() throws TransactionException {
         try {
             rollbackAsync().get();
-        } catch (Exception e) {
+        } catch (ExecutionException e) {
+            throw sneakyThrow(tryToCopyExceptionWithCause(e));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+
             throw withCause(TransactionException::new, TX_ROLLBACK_ERR, e);
         }
     }
@@ -111,7 +123,7 @@ public abstract class IgniteAbstractTransactionImpl 
implements InternalTransacti
     /** {@inheritDoc} */
     @Override
     public CompletableFuture<Void> rollbackAsync() {
-        return finish(false);
+        return 
TransactionsExceptionMapperUtil.convertToPublicFuture(finish(false), 
TX_ROLLBACK_ERR);
     }
 
     /**
@@ -122,4 +134,15 @@ public abstract class IgniteAbstractTransactionImpl 
implements InternalTransacti
      * @return The future.
      */
     protected abstract CompletableFuture<Void> finish(boolean commit);
+
+    // TODO: remove after IGNITE-22721 gets resolved.
+    private static Throwable tryToCopyExceptionWithCause(ExecutionException 
exception) {
+        Throwable copy = copyExceptionWithCause(exception);
+
+        if (copy == null) {
+            return new TransactionException(INTERNAL_ERR, "Cannot make a 
proper copy of " + exception.getCause().getClass(), exception);
+        }
+
+        return copy;
+    }
 }
diff --git 
a/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionsExceptionMapperUtil.java
 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionsExceptionMapperUtil.java
new file mode 100644
index 0000000000..25c8d1d0db
--- /dev/null
+++ 
b/modules/transactions/src/main/java/org/apache/ignite/internal/tx/impl/TransactionsExceptionMapperUtil.java
@@ -0,0 +1,85 @@
+/*
+ * 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.internal.tx.impl;
+
+import static 
org.apache.ignite.internal.lang.IgniteExceptionMapperUtil.mapToPublicException;
+import static 
org.apache.ignite.internal.util.CompletableFutures.isCompletedSuccessfully;
+import static org.apache.ignite.internal.util.ExceptionUtils.unwrapCause;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import org.apache.ignite.lang.TraceableException;
+import org.apache.ignite.tx.Transaction;
+import org.apache.ignite.tx.TransactionException;
+
+/**
+ * Utils for mapping exceptions that is specific to the {@link Transaction} 
context.
+ */
+class TransactionsExceptionMapperUtil {
+    /**
+     * Returns a new CompletableFuture that, when the given {@code origin} 
future completes exceptionally, maps the origin's exception to a
+     * public Ignite exception if it is needed. The mapping is made in the 
context of {@link Transaction} methods.
+     *
+     * @param origin The future to use to create a new stage.
+     * @param <T> Type os result.
+     * @return New CompletableFuture.
+     */
+    static <T> CompletableFuture<T> convertToPublicFuture(CompletableFuture<T> 
origin, int defaultCode) {
+        if (isCompletedSuccessfully(origin)) {
+            // No need to translate exceptions.
+            return origin;
+        }
+
+        return origin
+                .handle((res, err) -> {
+                    if (err != null) {
+                        throw new 
CompletionException(mapToPublicTransactionException(unwrapCause(err), 
defaultCode));
+                    }
+
+                    return res;
+                });
+    }
+
+    private static Throwable mapToPublicTransactionException(Throwable origin, 
int defaultCode) {
+        if (origin instanceof PrimaryReplicaExpiredException) {
+            PrimaryReplicaExpiredException err = 
(PrimaryReplicaExpiredException) origin;
+
+            return new TransactionException(err.traceId(), err.code(), 
err.getMessage(), err);
+        }
+        if (origin instanceof AssertionError) {
+            return new TransactionException(defaultCode, origin);
+        }
+        if (origin instanceof Error) {
+            return origin;
+        }
+
+        Throwable mapped = mapToPublicException(origin, ex -> new 
TransactionException(defaultCode, ex));
+
+        if (mapped instanceof TransactionException) {
+            return mapped;
+        }
+
+        if (mapped instanceof TraceableException) {
+            TraceableException traceable = (TraceableException) mapped;
+
+            return new TransactionException(traceable.traceId(), 
traceable.code(), mapped.getMessage(), mapped);
+        }
+
+        return new TransactionException(defaultCode, mapped);
+    }
+}
diff --git 
a/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxManagerTest.java
 
b/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxManagerTest.java
index 099d2fbeca..11d25032e7 100644
--- 
a/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxManagerTest.java
+++ 
b/modules/transactions/src/test/java/org/apache/ignite/internal/tx/TxManagerTest.java
@@ -31,7 +31,6 @@ import static 
org.apache.ignite.lang.ErrorGroups.Transactions.TX_PRIMARY_REPLICA
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -77,6 +76,7 @@ import org.apache.ignite.internal.replicator.ReplicaService;
 import org.apache.ignite.internal.replicator.TablePartitionId;
 import 
org.apache.ignite.internal.replicator.exception.PrimaryReplicaMissException;
 import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.apache.ignite.internal.tx.configuration.TransactionConfiguration;
 import org.apache.ignite.internal.tx.impl.HeapLockManager;
 import org.apache.ignite.internal.tx.impl.PrimaryReplicaExpiredException;
@@ -87,7 +87,6 @@ import org.apache.ignite.internal.tx.impl.TxManagerImpl;
 import org.apache.ignite.internal.tx.message.TxFinishReplicaRequest;
 import org.apache.ignite.internal.tx.test.TestLocalRwTxCounter;
 import org.apache.ignite.internal.tx.test.TestTransactionIds;
-import org.apache.ignite.internal.util.ExceptionUtils;
 import org.apache.ignite.lang.ErrorGroups.Transactions;
 import org.apache.ignite.network.ClusterNode;
 import org.apache.ignite.network.NetworkAddress;
@@ -343,7 +342,7 @@ public class TxManagerTest extends IgniteAbstractTest {
 
         TransactionException transactionException = 
assertThrows(TransactionException.class, tx::commit);
 
-        assertInstanceOf(MismatchingTransactionOutcomeException.class, 
ExceptionUtils.unwrapCause(transactionException.getCause()));
+        assertTrue(IgniteTestUtils.hasCause(transactionException, 
MismatchingTransactionOutcomeException.class, null));
 
         tx.commitAsync().get(3, TimeUnit.SECONDS);
         tx.rollbackAsync().get(3, TimeUnit.SECONDS);
@@ -371,7 +370,7 @@ public class TxManagerTest extends IgniteAbstractTest {
 
         TransactionException transactionException = 
assertThrows(TransactionException.class, tx::rollback);
 
-        assertInstanceOf(MismatchingTransactionOutcomeException.class, 
ExceptionUtils.unwrapCause(transactionException.getCause()));
+        assertTrue(IgniteTestUtils.hasCause(transactionException, 
MismatchingTransactionOutcomeException.class, null));
 
         tx.commitAsync().get(3, TimeUnit.SECONDS);
         tx.rollbackAsync().get(3, TimeUnit.SECONDS);


Reply via email to