Skip to content

Commit 1911f1f

Browse files
committed
JVMCBC-1658 Better filtering of suppressed exception stack traces
Motivation ---------- Remove uninteresting Netty stack frames from suppressed exceptions, just as we do for the causal chain. Modifications ------------- Move the stack trace filtering code into CbThrowables as a generic `filterStackTrace` method. Recursively filter suppressed exceptions. Protect against circular causes and suppression. Change-Id: I597ace7c3546100bb0e6b81de463cee21a1dc34b Reviewed-on: https://review.couchbase.org/c/couchbase-jvm-clients/+/229292 Tested-by: Build Bot <[email protected]> Reviewed-by: Graham Pople <[email protected]>
1 parent ec5690a commit 1911f1f

File tree

3 files changed

+84
-14
lines changed

3 files changed

+84
-14
lines changed

core-io/src/main/java/com/couchbase/client/core/endpoint/BaseEndpoint.java

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@
7878
import java.net.InetSocketAddress;
7979
import java.net.SocketAddress;
8080
import java.time.Duration;
81-
import java.util.Arrays;
82-
import java.util.LinkedList;
83-
import java.util.List;
8481
import java.util.Optional;
8582
import java.util.concurrent.CompletableFuture;
8683
import java.util.concurrent.TimeUnit;
@@ -92,6 +89,7 @@
9289
import java.util.function.Supplier;
9390

9491
import static com.couchbase.client.core.logging.RedactableArgument.redactMeta;
92+
import static com.couchbase.client.core.util.CbThrowables.filterStackTrace;
9593

9694
/**
9795
* This {@link BaseEndpoint} implements all common logic for endpoints that wrap the IO layer.
@@ -491,18 +489,10 @@ public synchronized Throwable fillInStackTrace() {
491489
* removing all the generic netty stack we do not need.
492490
*
493491
* @param input the input exception.
494-
* @return the trimmed exception.
492+
* @return the same exception instance, now with filtered stack trace.
495493
*/
496-
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
497-
private Throwable trimNettyFromStackTrace(final Throwable input) {
498-
if (input == null) {
499-
return null;
500-
}
501-
502-
final List<StackTraceElement> elements = new LinkedList<>(Arrays.asList(input.getStackTrace()));
503-
elements.removeIf(next -> next.getClassName().startsWith("com.couchbase.client.core.deps.io.netty"));
504-
input.setStackTrace(elements.toArray(new StackTraceElement[]{}));
505-
trimNettyFromStackTrace(input.getCause());
494+
private static Throwable trimNettyFromStackTrace(final Throwable input) {
495+
filterStackTrace(input, frame -> !frame.getClassName().startsWith("com.couchbase.client.core.deps.io.netty"));
506496
return input;
507497
}
508498

core-io/src/main/java/com/couchbase/client/core/util/CbThrowables.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818

1919
import com.couchbase.client.core.annotation.Stability;
2020
import com.couchbase.client.core.error.CouchbaseException;
21+
import org.jspecify.annotations.Nullable;
2122

2223
import java.io.PrintWriter;
2324
import java.io.StringWriter;
25+
import java.util.Arrays;
26+
import java.util.IdentityHashMap;
2427
import java.util.Optional;
28+
import java.util.Set;
29+
import java.util.function.Predicate;
2530

31+
import static java.util.Collections.newSetFromMap;
2632
import static java.util.Objects.requireNonNull;
2733

2834
@Stability.Internal
@@ -92,4 +98,41 @@ public static String getStackTraceAsString(Throwable t) {
9298
t.printStackTrace(new PrintWriter(w));
9399
return w.toString();
94100
}
101+
102+
/**
103+
* Modifies the stack trace of the given throwable by removing elements
104+
* that do not match the filter predicate.
105+
* <p>
106+
* Applies the modification recursively to the throwable's causal chain
107+
* and suppressed throwables.
108+
*/
109+
public static void filterStackTrace(
110+
Throwable t,
111+
Predicate<StackTraceElement> filter
112+
) {
113+
requireNonNull(t);
114+
filterStackTraceRecursive(t, filter, newSetFromMap(new IdentityHashMap<>()));
115+
}
116+
117+
private static void filterStackTraceRecursive(
118+
@Nullable Throwable t,
119+
Predicate<StackTraceElement> filter,
120+
Set<Throwable> seen
121+
) {
122+
if (t == null || !seen.add(t)) {
123+
return;
124+
}
125+
126+
t.setStackTrace(
127+
Arrays.stream(t.getStackTrace())
128+
.filter(filter)
129+
.toArray(StackTraceElement[]::new)
130+
);
131+
132+
filterStackTraceRecursive(t.getCause(), filter, seen);
133+
134+
for (Throwable suppressed : t.getSuppressed()) {
135+
filterStackTraceRecursive(suppressed, filter, seen);
136+
}
137+
}
95138
}

core-io/src/test/java/com/couchbase/client/core/util/CbThrowablesTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020

2121
import java.util.Optional;
2222

23+
import static com.couchbase.client.core.util.CbThrowables.filterStackTrace;
24+
import static com.couchbase.client.core.util.CbThrowables.getStackTraceAsString;
2325
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.junit.jupiter.api.Assertions.assertFalse;
2427
import static org.junit.jupiter.api.Assertions.assertThrows;
2528
import static org.junit.jupiter.api.Assertions.assertTrue;
2629

@@ -77,4 +80,38 @@ void throwIfUncheckedWorks() {
7780
assertThrows(IndexOutOfBoundsException.class, () ->
7881
CbThrowables.throwIfUnchecked(new IndexOutOfBoundsException()));
7982
}
83+
84+
@Test
85+
void canFilterStackTraceWithCircularCause() {
86+
Throwable a = new RuntimeException();
87+
Throwable b = new RuntimeException(a);
88+
a.initCause(b);
89+
filterStackTrace(a, frame -> true);
90+
}
91+
92+
@Test
93+
void canFilterStackTraceWithCircularSuppression() {
94+
Throwable a = new RuntimeException();
95+
Throwable b = new RuntimeException();
96+
a.addSuppressed(b);
97+
b.addSuppressed(a);
98+
filterStackTrace(a, frame -> true);
99+
}
100+
101+
@Test
102+
void canFilterStackTrace() {
103+
String className = getClass().getName();
104+
105+
Throwable t = new RuntimeException(new RuntimeException(new RuntimeException()));
106+
t.addSuppressed(new RuntimeException(new RuntimeException()));
107+
108+
assertTrue(getStackTraceAsString(t).contains(className)); // sanity check
109+
110+
filterStackTrace(t, frame -> !frame.getClassName().equals(className));
111+
112+
String s = getStackTraceAsString(t);
113+
assertFalse(
114+
s.contains(className),
115+
"Stack trace should not contain " + className + ", but got: " + s);
116+
}
80117
}

0 commit comments

Comments
 (0)