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

isapego 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 bcfa3e2acd IGNITE-20871 Fix stack trace in client exceptions (#2845)
bcfa3e2acd is described below

commit bcfa3e2acda444c508d0b992919347cc022eb65f
Author: Igor Sapego <[email protected]>
AuthorDate: Tue Nov 21 13:48:42 2023 +0400

    IGNITE-20871 Fix stack trace in client exceptions (#2845)
---
 .../apache/ignite/lang/IgniteCheckedException.java |   9 +-
 .../handler/ClientInboundMessageHandler.java       |   3 +-
 .../apache/ignite/internal/client/ClientUtils.java |  52 ++++++++-
 .../ignite/internal/client/TcpClientChannel.java   |   3 +-
 .../internal/client/compute/ClientCompute.java     |   8 +-
 .../client/ClientKeyValueBinaryViewTest.java       |   6 +
 .../ignite/client/ClientKeyValueViewTest.java      |   7 ++
 .../apache/ignite/client/ClientRecordViewTest.java |   6 +
 .../ignite/internal/client/ClientUtilsTest.java    | 128 +++++++++++++++++++++
 .../ignite/internal/util/ExceptionUtils.java       |  11 --
 .../runner/app/client/ItThinClientComputeTest.java |   9 +-
 11 files changed, 211 insertions(+), 31 deletions(-)

diff --git 
a/modules/api/src/main/java/org/apache/ignite/lang/IgniteCheckedException.java 
b/modules/api/src/main/java/org/apache/ignite/lang/IgniteCheckedException.java
index aaccd9a4cb..0c90a4923b 100755
--- 
a/modules/api/src/main/java/org/apache/ignite/lang/IgniteCheckedException.java
+++ 
b/modules/api/src/main/java/org/apache/ignite/lang/IgniteCheckedException.java
@@ -25,6 +25,7 @@ import static 
org.apache.ignite.lang.ErrorGroups.extractGroupCode;
 import static org.apache.ignite.lang.util.TraceIdUtils.getOrCreateTraceId;
 
 import java.util.UUID;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * General Ignite exception. Used to indicate any error condition within a 
node.
@@ -102,7 +103,7 @@ public class IgniteCheckedException extends Exception 
implements TraceableExcept
      * @param code Full error code.
      * @param cause Optional nested exception (can be {@code null}).
      */
-    public IgniteCheckedException(int code, Throwable cause) {
+    public IgniteCheckedException(int code, @Nullable Throwable cause) {
         this(getOrCreateTraceId(cause), code, cause);
     }
 
@@ -113,7 +114,7 @@ public class IgniteCheckedException extends Exception 
implements TraceableExcept
      * @param code Full error code.
      * @param cause Optional nested exception (can be {@code null}).
      */
-    public IgniteCheckedException(UUID traceId, int code, Throwable cause) {
+    public IgniteCheckedException(UUID traceId, int code, @Nullable Throwable 
cause) {
         super((cause != null) ? cause.getLocalizedMessage() : null, cause);
 
         this.traceId = traceId;
@@ -128,7 +129,7 @@ public class IgniteCheckedException extends Exception 
implements TraceableExcept
      * @param message Detailed message.
      * @param cause Optional nested exception (can be {@code null}).
      */
-    public IgniteCheckedException(int code, String message, Throwable cause) {
+    public IgniteCheckedException(int code, String message, @Nullable 
Throwable cause) {
         this(getOrCreateTraceId(cause), code, message, cause);
     }
 
@@ -140,7 +141,7 @@ public class IgniteCheckedException extends Exception 
implements TraceableExcept
      * @param message Detailed message.
      * @param cause Optional nested exception (can be {@code null}).
      */
-    public IgniteCheckedException(UUID traceId, int code, String message, 
Throwable cause) {
+    public IgniteCheckedException(UUID traceId, int code, String message, 
@Nullable Throwable cause) {
         super(message, cause);
 
         this.traceId = traceId;
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
index f6c03430f5..93efeb3439 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
@@ -94,6 +94,7 @@ import org.apache.ignite.internal.hlc.HybridClock;
 import org.apache.ignite.internal.hlc.HybridTimestamp;
 import org.apache.ignite.internal.jdbc.proto.JdbcQueryCursorHandler;
 import org.apache.ignite.internal.jdbc.proto.JdbcQueryEventHandler;
+import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
@@ -450,7 +451,7 @@ public class ClientInboundMessageHandler extends 
ChannelInboundHandlerAdapter im
         }
 
         // No need to send internal errors to client.
-        Throwable pubErr = ExceptionUtils.unwrapToPublicException(err);
+        Throwable pubErr = 
IgniteExceptionMapperUtil.mapToPublicException(ExceptionUtils.unwrapCause(err));
 
         // Class name and message.
         packer.packString(pubErr.getClass().getName());
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/ClientUtils.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/ClientUtils.java
index 5304b76e5c..27c4871527 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/ClientUtils.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/ClientUtils.java
@@ -17,22 +17,68 @@
 
 package org.apache.ignite.internal.client;
 
-import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
+import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
 
+import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import org.apache.ignite.client.ClientOperationType;
 import org.apache.ignite.client.IgniteClientConfiguration;
 import org.apache.ignite.internal.client.proto.ClientOp;
+import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
 import org.apache.ignite.internal.util.ExceptionUtils;
+import org.apache.ignite.lang.IgniteCheckedException;
+import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.lang.LoggerFactory;
+import org.apache.ignite.lang.TraceableException;
 
 /**
  * Client utilities.
  */
 public class ClientUtils {
+    /**
+     * Wraps an exception in an IgniteException, extracting trace identifier 
and error code when the specified exception or one of its
+     * causes is an IgniteException itself.
+     *
+     * @param e Internal exception.
+     * @return Public exception.
+     */
+    public static Throwable ensurePublicException(Throwable e) {
+        Objects.requireNonNull(e);
+
+        e = ExceptionUtils.unwrapCause(e);
+
+        if (e instanceof IgniteException) {
+            return copyExceptionWithCauseIfPossible((IgniteException) e);
+        }
+
+        if (e instanceof IgniteCheckedException) {
+            return copyExceptionWithCauseIfPossible((IgniteCheckedException) 
e);
+        }
+
+        e = IgniteExceptionMapperUtil.mapToPublicException(e);
+
+        return new IgniteException(INTERNAL_ERR, e.getMessage(), e);
+    }
+
+    /**
+     * Try to copy exception using ExceptionUtils.copyExceptionWithCause and 
return new exception if it was not possible.
+     *
+     * @param e Exception.
+     * @return Properly copied exception or a new error, if exception can not 
be copied.
+     */
+    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) {
+            return copy;
+        }
+
+        return new IgniteException(INTERNAL_ERR, "Public Ignite 
exception-derived class does not have required constructor: "
+                + e.getClass().getName(), e);
+    }
+
     /**
      * Waits for async operation completion.
      *
@@ -46,9 +92,9 @@ public class ClientUtils {
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt(); // Restore interrupt flag.
 
-            throw sneakyThrow(ExceptionUtils.unwrapToPublicException(e));
+            throw ExceptionUtils.sneakyThrow(ensurePublicException(e));
         } catch (ExecutionException e) {
-            throw sneakyThrow(ExceptionUtils.unwrapToPublicException(e));
+            throw ExceptionUtils.sneakyThrow(ensurePublicException(e));
         }
     }
 
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
index 36ca04d3ef..119a49cab0 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
@@ -58,7 +58,6 @@ import org.apache.ignite.internal.client.proto.ResponseFlags;
 import org.apache.ignite.internal.client.proto.ServerMessageType;
 import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.tostring.S;
-import org.apache.ignite.internal.util.ExceptionUtils;
 import org.apache.ignite.lang.ErrorGroups.Table;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.network.NetworkAddress;
@@ -326,7 +325,7 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
 
             metrics.requestsActiveDecrement();
 
-            throw sneakyThrow(ExceptionUtils.unwrapToPublicException(t));
+            throw sneakyThrow(ClientUtils.ensurePublicException(t));
         }
     }
 
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/compute/ClientCompute.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/compute/ClientCompute.java
index 1b23016fcd..18272ca3d3 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/compute/ClientCompute.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/compute/ClientCompute.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.internal.client.compute;
 
-import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 import static org.apache.ignite.lang.ErrorGroups.Client.TABLE_ID_NOT_FOUND_ERR;
 
 import java.util.HashMap;
@@ -32,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ThreadLocalRandom;
 import org.apache.ignite.compute.DeploymentUnit;
 import org.apache.ignite.compute.IgniteCompute;
+import org.apache.ignite.internal.client.ClientUtils;
 import org.apache.ignite.internal.client.ReliableChannel;
 import org.apache.ignite.internal.client.proto.ClientMessagePacker;
 import org.apache.ignite.internal.client.proto.ClientOp;
@@ -103,7 +103,7 @@ public class ClientCompute implements IgniteCompute {
         try {
             return this.<R>executeAsync(nodes, units, jobClassName, 
args).join();
         } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.unwrapToPublicException(e));
+            throw 
ExceptionUtils.sneakyThrow(ClientUtils.ensurePublicException(e));
         }
     }
 
@@ -169,7 +169,7 @@ public class ClientCompute implements IgniteCompute {
         try {
             return this.<R>executeColocatedAsync(tableName, key, units, 
jobClassName, args).join();
         } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.unwrapToPublicException(e));
+            throw 
ExceptionUtils.sneakyThrow(ClientUtils.ensurePublicException(e));
         }
     }
 
@@ -186,7 +186,7 @@ public class ClientCompute implements IgniteCompute {
         try {
             return this.<K, R>executeColocatedAsync(tableName, key, keyMapper, 
units, jobClassName, args).join();
         } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.unwrapToPublicException(e));
+            throw 
ExceptionUtils.sneakyThrow(ClientUtils.ensurePublicException(e));
         }
     }
 
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueBinaryViewTest.java
 
b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueBinaryViewTest.java
index f170db1202..70384fdd32 100644
--- 
a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueBinaryViewTest.java
+++ 
b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueBinaryViewTest.java
@@ -17,6 +17,10 @@
 
 package org.apache.ignite.client;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasToString;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -24,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -174,6 +179,7 @@ public class ClientKeyValueBinaryViewTest extends 
AbstractClientTableTest {
 
         var ex = assertThrows(IgniteException.class, () -> 
kvView.contains(null, Tuple.create()));
         assertTrue(ex.getMessage().contains("Missed key column: ID"), 
ex.getMessage());
+        assertThat(Arrays.asList(ex.getStackTrace()), 
anyOf(hasToString(containsString("ClientKeyValueBinaryView"))));
     }
 
     @Test
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
 
b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
index 4b53e67b6a..b0fa0cd1bb 100644
--- 
a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
+++ 
b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
@@ -19,7 +19,9 @@ package org.apache.ignite.client;
 
 import static java.time.temporal.ChronoField.NANO_OF_SECOND;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
 import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasToString;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -31,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collection;
 import java.util.List;
@@ -100,6 +103,8 @@ public class ClientKeyValueViewTest extends 
AbstractClientTableTest {
 
         IgniteException e = assertThrows(IgniteException.class, () -> 
pojoView.get(null, key));
         assertEquals("Failed to deserialize server response: No mapped object 
field found for column 'ZBOOLEAN'", e.getMessage());
+        assertThat(Arrays.asList(e.getStackTrace()), 
anyOf(hasToString(containsString("ClientKeyValueView"))));
+
     }
 
     @Test
@@ -200,6 +205,7 @@ public class ClientKeyValueViewTest extends 
AbstractClientTableTest {
         IgniteException e = assertThrows(IgniteException.class, () -> 
kvView.get(null, new NamePojo()));
 
         assertThat(e.getMessage(), containsString("No mapped object field 
found for column 'ID'"));
+        assertThat(Arrays.asList(e.getStackTrace()), 
anyOf(hasToString(containsString("ClientKeyValueView"))));
     }
 
     @Test
@@ -485,5 +491,6 @@ public class ClientKeyValueViewTest extends 
AbstractClientTableTest {
         var ex = assertThrows(IgniteException.class, () -> pojoView.put(null, 
1, pojo));
 
         assertTrue(ex.getMessage().contains("null was passed, but column is 
not nullable"), ex.getMessage());
+        assertThat(Arrays.asList(ex.getStackTrace()), 
anyOf(hasToString(containsString("ClientKeyValueView"))));
     }
 }
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
 
b/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
index b5c3e1007f..2a906efd40 100644
--- 
a/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
+++ 
b/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
@@ -19,7 +19,9 @@ package org.apache.ignite.client;
 
 import static java.time.temporal.ChronoField.NANO_OF_SECOND;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
 import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasToString;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -31,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collection;
 import java.util.List;
@@ -104,6 +107,7 @@ public class ClientRecordViewTest extends 
AbstractClientTableTest {
         // This POJO does not have fields for all table columns, which is not 
allowed (to avoid unexpected data loss).
         IgniteException ex = assertThrows(IgniteException.class, () -> 
pojoView.get(null, key));
         assertEquals("Failed to deserialize server response: No mapped object 
field found for column 'ZBOOLEAN'", ex.getMessage());
+        assertThat(Arrays.asList(ex.getStackTrace()), 
anyOf(hasToString(containsString("ClientRecordView"))));
     }
 
     @Test
@@ -195,6 +199,7 @@ public class ClientRecordViewTest extends 
AbstractClientTableTest {
         IgniteException e = assertThrows(IgniteException.class, () -> 
recordView.get(null, new NamePojo()));
 
         assertThat(e.getMessage(), containsString("No mapped object field 
found for column 'ID'"));
+        assertThat(Arrays.asList(e.getStackTrace()), 
anyOf(hasToString(containsString("ClientRecordView"))));
     }
 
     @Test
@@ -511,5 +516,6 @@ public class ClientRecordViewTest extends 
AbstractClientTableTest {
         var ex = assertThrows(IgniteException.class, () -> 
pojoView.upsert(null, pojo));
 
         assertTrue(ex.getMessage().contains("null was passed, but column is 
not nullable"), ex.getMessage());
+        assertThat(Arrays.asList(ex.getStackTrace()), 
anyOf(hasToString(containsString("ClientRecordView"))));
     }
 }
diff --git 
a/modules/client/src/test/java/org/apache/ignite/internal/client/ClientUtilsTest.java
 
b/modules/client/src/test/java/org/apache/ignite/internal/client/ClientUtilsTest.java
new file mode 100644
index 0000000000..eb41308fb2
--- /dev/null
+++ 
b/modules/client/src/test/java/org/apache/ignite/internal/client/ClientUtilsTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.client;
+
+import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasToString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import org.apache.ignite.lang.IgniteCheckedException;
+import org.apache.ignite.lang.IgniteException;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Future utils test.
+ */
+public class ClientUtilsTest {
+    @Test
+    public void testEnsurePublicExceptionIgniteException() {
+        IgniteException ex = assertThrows(IgniteException.class, 
ClientUtilsTest::throwIgniteException);
+
+        Throwable resEx = checkableTestMethod(ex);
+
+        assertTrue(resEx instanceof IgniteException);
+        assertEquals("Test ignite exception", resEx.getMessage());
+        assertEquals(ex.getMessage(), resEx.getMessage());
+        assertThat(Arrays.asList(ex.getStackTrace()), 
anyOf(hasToString(containsString("throwIgniteException"))));
+        assertThat(Arrays.asList(resEx.getStackTrace()), 
anyOf(hasToString(containsString("checkableTestMethod"))));
+        assertSame(ex.getClass(), resEx.getCause().getClass());
+    }
+
+    @Test
+    public void testEnsurePublicExceptionIgniteCheckedException() {
+        IgniteCheckedException ex = assertThrows(IgniteCheckedException.class, 
ClientUtilsTest::throwIgniteCheckedException);
+
+        Throwable resEx = checkableTestMethod(ex);
+
+        assertTrue(resEx instanceof IgniteCheckedException);
+        assertEquals("Test checked exception", resEx.getMessage());
+        assertEquals(ex.getMessage(), resEx.getMessage());
+        assertThat(Arrays.asList(ex.getStackTrace()), 
anyOf(hasToString(containsString("throwIgniteCheckedException"))));
+        assertThat(Arrays.asList(resEx.getStackTrace()), 
anyOf(hasToString(containsString("checkableTestMethod"))));
+        assertSame(ex.getClass(), resEx.getCause().getClass());
+    }
+
+    @Test
+    public void testEnsurePublicExceptionRuntimeException() {
+        RuntimeException ex = assertThrows(RuntimeException.class, 
ClientUtilsTest::throwRuntimeException);
+
+        Throwable resEx = checkableTestMethod(ex);
+
+        assertTrue(resEx instanceof IgniteException);
+        assertEquals("Test runtime exception", resEx.getMessage());
+        assertEquals(ex.getMessage(), resEx.getMessage());
+        assertThat(Arrays.asList(ex.getStackTrace()), 
anyOf(hasToString(containsString("throwRuntimeException"))));
+        assertThat(Arrays.asList(resEx.getStackTrace()), 
anyOf(hasToString(containsString("checkableTestMethod"))));
+        assertSame(IgniteException.class, resEx.getCause().getClass());
+        assertSame(ex.getClass(), resEx.getCause().getCause().getClass());
+    }
+
+    @Test
+    public void testEnsurePublicExceptionInvalidIgniteException() {
+        InvalidIgniteException ex = assertThrows(InvalidIgniteException.class, 
ClientUtilsTest::throwInvalidIgniteException);
+
+        Throwable resEx = checkableTestMethod(ex);
+
+        assertTrue(resEx instanceof IgniteException);
+        assertThat(resEx.getMessage(), containsString("Public Ignite 
exception-derived class does not have required constructor"));
+        assertThat(Arrays.asList(ex.getStackTrace()), 
anyOf(hasToString(containsString("throwInvalidIgniteException"))));
+        assertThat(Arrays.asList(resEx.getStackTrace()), 
anyOf(hasToString(containsString("checkableTestMethod"))));
+        assertSame(InvalidIgniteException.class, resEx.getCause().getClass());
+        assertSame(ex.getClass(), resEx.getCause().getClass());
+    }
+
+    /**
+     * Method that should present in resulting stack trace.
+     */
+    private static Throwable checkableTestMethod(Throwable ex) {
+        return ClientUtils.ensurePublicException(ex);
+    }
+
+    /**
+     * Un-constructable IgniteException.
+     */
+    private static class InvalidIgniteException extends IgniteException {
+        InvalidIgniteException(int code, String message, @Nullable Throwable 
cause) {
+            super(code, message, cause);
+        }
+    }
+
+    private static void throwInvalidIgniteException() {
+        throw new InvalidIgniteException(INTERNAL_ERR, "Test invalid ignite 
exception", null);
+    }
+
+    private static void throwIgniteException() {
+        throw new IgniteException(INTERNAL_ERR, "Test ignite exception", null);
+    }
+
+    private static void throwIgniteCheckedException() throws 
IgniteCheckedException {
+        throw new IgniteCheckedException(INTERNAL_ERR, "Test checked 
exception", null);
+    }
+
+    private static void throwRuntimeException() {
+        throw new RuntimeException("Test runtime exception", null);
+    }
+}
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/ExceptionUtils.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/ExceptionUtils.java
index 6cf71b7cd2..acef8dc60c 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/ExceptionUtils.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/ExceptionUtils.java
@@ -38,7 +38,6 @@ import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.ExecutionException;
-import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
 import org.apache.ignite.internal.lang.IgniteInternalCheckedException;
 import org.apache.ignite.internal.lang.IgniteInternalException;
 import org.apache.ignite.internal.lang.IgniteQuadFunction;
@@ -347,16 +346,6 @@ public final class ExceptionUtils {
         return e;
     }
 
-    /**
-     * Unwraps exception cause from wrappers like CompletionException and 
ExecutionException and converts it to public exception.
-     *
-     * @param err Exception.
-     * @return Public exception.
-     */
-    public static Throwable unwrapToPublicException(Throwable err) {
-        return 
IgniteExceptionMapperUtil.mapToPublicException(unwrapCause(err));
-    }
-
     /**
      * Creates a new exception, which type is defined by the provided {@code 
supplier}, with the specified {@code t} as a cause.
      * In the case when the provided cause {@code t} is an instance of {@link 
TraceableException},
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientComputeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientComputeTest.java
index c1f596d995..3f10fa8899 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientComputeTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientComputeTest.java
@@ -171,8 +171,7 @@ public class ItThinClientComputeTest extends 
ItAbstractThinClientTest {
                     IgniteException.class,
                     () -> client().compute().<String>execute(Set.of(node(0)), 
List.of(), IgniteExceptionJob.class.getName()));
 
-            // TODO IGNITE-20858: Once user errors are handled properly, make 
sure the cause is checked
-            cause = ex;
+            cause = (IgniteException) ex.getCause();
         }
 
         assertThat(cause.getMessage(), containsString("Custom job error"));
@@ -198,8 +197,7 @@ public class ItThinClientComputeTest extends 
ItAbstractThinClientTest {
                     IgniteException.class,
                     () -> client().compute().<String>execute(Set.of(node(0)), 
List.of(), ExceptionJob.class.getName()));
 
-            // TODO IGNITE-20858: Once user errors are handled properly, make 
sure the cause is checked
-            cause = ex;
+            cause = (IgniteException) ex.getCause();
         }
 
         // TODO IGNITE-20858: Once user errors are handled properly, make sure 
the cause is ArithmeticException
@@ -225,8 +223,7 @@ public class ItThinClientComputeTest extends 
ItAbstractThinClientTest {
                     IgniteException.class,
                     () -> client().compute().execute(Set.of(node(1)), 
List.of(), ExceptionJob.class.getName()));
 
-            // TODO IGNITE-20858: Once user errors are handled properly, make 
sure the cause is checked
-            cause = ex;
+            cause = (IgniteException) ex.getCause();
         }
 
         // TODO IGNITE-20858: Once user errors are handled properly, make sure 
the cause is ArithmeticException

Reply via email to