KUDU-2230: java: sync client exception stack traces should make sense

Previously the exceptions thrown by the synchronous client always had
a stack trace somewhere deep inside our async callback chain. So, when
the user eventually caught this exception, it would be very hard to even
know what part of their code failed, since their own code would not even
show up in the call chain.

This patch simply replaces the exception's stack trace with the stack at
the "join" point where the exception is transformed back to a
KuduException. An exception which stores the original stack trace is
attached as a "suppressed" exception.

Change-Id: I874d47b5239bcc8493c15ef763ecfe0fd878d795
Reviewed-on: http://gerrit.cloudera.org:8080/9489
Tested-by: Kudu Jenkins
Reviewed-by: Dan Burkert <danburk...@apache.org>
Reviewed-by: Alexey Serbin <aser...@cloudera.com>
(cherry picked from commit ce0db915787b58a79109e6faecc6f1daef9f2850)
Reviewed-on: http://gerrit.cloudera.org:8080/9610
Reviewed-by: Grant Henke <granthe...@gmail.com>
Tested-by: Grant Henke <granthe...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/kudu/repo
Commit: http://git-wip-us.apache.org/repos/asf/kudu/commit/542cfd9e
Tree: http://git-wip-us.apache.org/repos/asf/kudu/tree/542cfd9e
Diff: http://git-wip-us.apache.org/repos/asf/kudu/diff/542cfd9e

Branch: refs/heads/branch-1.7.x
Commit: 542cfd9e01f3c606bfc5f9650ceb14c7e5cd4f5b
Parents: 62cb06e
Author: Todd Lipcon <t...@apache.org>
Authored: Mon Mar 5 00:00:55 2018 -0800
Committer: Grant Henke <granthe...@gmail.com>
Committed: Tue Mar 13 21:20:13 2018 +0000

----------------------------------------------------------------------
 .../org/apache/kudu/client/KuduException.java   | 31 ++++++++++++++++----
 .../kudu/client/TestConnectToCluster.java       |  5 +++-
 2 files changed, 30 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/kudu/blob/542cfd9e/java/kudu-client/src/main/java/org/apache/kudu/client/KuduException.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/client/KuduException.java 
b/java/kudu-client/src/main/java/org/apache/kudu/client/KuduException.java
index 3aa566a..d6dacf2 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/KuduException.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/KuduException.java
@@ -27,7 +27,6 @@
 package org.apache.kudu.client;
 
 import java.io.IOException;
-import java.util.Arrays;
 
 import com.stumbleupon.async.DeferredGroupException;
 import com.stumbleupon.async.TimeoutException;
@@ -45,7 +44,6 @@ import org.apache.yetus.audience.InterfaceStability;
 @InterfaceStability.Evolving
 @SuppressWarnings("serial")
 public abstract class KuduException extends IOException {
-
   private final Status status;
 
   /**
@@ -77,6 +75,24 @@ public abstract class KuduException extends IOException {
   }
 
   /**
+   * When exceptions are thrown by the asynchronous Kudu client, the stack 
trace is
+   * typically deep within the internals of the Kudu client and/or Netty.
+   * Thus, when the synchronous Kudu client wraps and throws the exception,
+   * we suppress that stack trace and replace it with the stack trace of the 
user's
+   * calling thread. The original stack trace is added to the {@link 
KuduException}
+   * as a suppressed exception ({@see Throwable#addSuppressed(Throwable)}) of
+   * this
+   */
+  @InterfaceAudience.Public
+  @InterfaceStability.Evolving
+  public static class OriginalException extends Throwable {
+    private OriginalException(Throwable e) {
+      super("Original asynchronous stack trace");
+      setStackTrace(e.getStackTrace());
+    }
+  }
+
+  /**
    * Inspects the given exception and transforms it into a KuduException.
    * @param e generic exception we want to transform
    * @return a KuduException that's easier to handle
@@ -85,9 +101,14 @@ public abstract class KuduException extends IOException {
     // The message may be null.
     String message =  e.getMessage() == null ? "" : e.getMessage();
     if (e instanceof KuduException) {
-      // TODO(KUDU-2330) this can lead to methods of the synchronous client
-      // throwing exceptions without the stack trace indicating where
-      // the method was called!
+      // The exception thrown inside the async code has a stack trace
+      // that doesn't correspond to where the user actually called
+      // some synchronous method. This can be very confusing for
+      // users, so we'll reset the stack trace back the call frame
+      // where we are transforming it.
+      e.addSuppressed(new OriginalException(e));
+      StackTraceElement[] stack = new Exception().getStackTrace();
+      e.setStackTrace(stack);
       return (KuduException) e;
     } else if (e instanceof DeferredGroupException) {
       // The cause of a DeferredGroupException is the first exception it sees, 
we're just going to

http://git-wip-us.apache.org/repos/asf/kudu/blob/542cfd9e/java/kudu-client/src/test/java/org/apache/kudu/client/TestConnectToCluster.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/test/java/org/apache/kudu/client/TestConnectToCluster.java
 
b/java/kudu-client/src/test/java/org/apache/kudu/client/TestConnectToCluster.java
index d467c8c..907e987 100644
--- 
a/java/kudu-client/src/test/java/org/apache/kudu/client/TestConnectToCluster.java
+++ 
b/java/kudu-client/src/test/java/org/apache/kudu/client/TestConnectToCluster.java
@@ -24,15 +24,16 @@ import static org.junit.Assert.fail;
 
 import java.util.List;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.common.net.HostAndPort;
 import com.stumbleupon.async.Callback;
 
 import org.junit.Assert;
 import org.junit.Test;
-
 import org.apache.kudu.consensus.Metadata;
 import org.apache.kudu.master.Master.ConnectToMasterResponsePB;
+import org.hamcrest.CoreMatchers;
 
 public class TestConnectToCluster {
 
@@ -97,6 +98,8 @@ public class TestConnectToCluster {
                   ".*Client configured with 1 master\\(s\\) " +
                   "\\(.+?\\) but cluster indicates it expects 3 master\\(s\\) 
" +
                   "\\(.+?,.+?,.+?\\).*"));
+          Assert.assertThat(Joiner.on("\n").join(e.getStackTrace()),
+              CoreMatchers.containsString("testConnectToOneOfManyMasters"));
         } finally {
           if (c != null) {
             c.close();

Reply via email to