Skip to content

Commit c433c4e

Browse files
committed
Lift error finding utility to exceptions helpers
We have code used in the networking layer to search for errors buried in other exceptions. This code will be useful in other locations so with this commit we move it to our exceptions helpers. Relates #28691
1 parent d3af535 commit c433c4e

File tree

5 files changed

+437
-101
lines changed

5 files changed

+437
-101
lines changed

core/src/main/java/org/elasticsearch/ExceptionsHelper.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,15 @@
3333
import java.io.PrintWriter;
3434
import java.io.StringWriter;
3535
import java.util.ArrayList;
36+
import java.util.Arrays;
37+
import java.util.Collections;
3638
import java.util.HashSet;
39+
import java.util.LinkedList;
3740
import java.util.List;
41+
import java.util.Optional;
42+
import java.util.Queue;
3843
import java.util.Set;
44+
import java.util.stream.Collectors;
3945

4046
public final class ExceptionsHelper {
4147

@@ -122,6 +128,46 @@ public static String stackTrace(Throwable e) {
122128
return stackTraceStringWriter.toString();
123129
}
124130

131+
public static String formatStackTrace(final StackTraceElement[] stackTrace) {
132+
return Arrays.stream(stackTrace).skip(1).map(e -> "\tat " + e).collect(Collectors.joining("\n"));
133+
}
134+
135+
static final int MAX_ITERATIONS = 1024;
136+
137+
/**
138+
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
139+
*
140+
* @param cause the root throwable
141+
*
142+
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
143+
*/
144+
public static Optional<Error> maybeError(final Throwable cause, final Logger logger) {
145+
// early terminate if the cause is already an error
146+
if (cause instanceof Error) {
147+
return Optional.of((Error) cause);
148+
}
149+
150+
final Queue<Throwable> queue = new LinkedList<>();
151+
queue.add(cause);
152+
int iterations = 0;
153+
while (!queue.isEmpty()) {
154+
iterations++;
155+
if (iterations > MAX_ITERATIONS) {
156+
logger.warn("giving up looking for fatal errors", cause);
157+
break;
158+
}
159+
final Throwable current = queue.remove();
160+
if (current instanceof Error) {
161+
return Optional.of((Error) current);
162+
}
163+
Collections.addAll(queue, current.getSuppressed());
164+
if (current.getCause() != null) {
165+
queue.add(current.getCause());
166+
}
167+
}
168+
return Optional.empty();
169+
}
170+
125171
/**
126172
* Rethrows the first exception in the list and adds all remaining to the suppressed list.
127173
* If the given list is empty no exception is thrown
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch;
21+
22+
import org.apache.commons.codec.DecoderException;
23+
import org.elasticsearch.test.ESTestCase;
24+
25+
import java.util.Optional;
26+
27+
import static org.elasticsearch.ExceptionsHelper.MAX_ITERATIONS;
28+
import static org.elasticsearch.ExceptionsHelper.maybeError;
29+
import static org.hamcrest.CoreMatchers.equalTo;
30+
31+
public class ExceptionsHelperTests extends ESTestCase {
32+
33+
public void testMaybeError() {
34+
final Error outOfMemoryError = new OutOfMemoryError();
35+
assertError(outOfMemoryError, outOfMemoryError);
36+
37+
final DecoderException decoderException = new DecoderException(outOfMemoryError);
38+
assertError(decoderException, outOfMemoryError);
39+
40+
final Exception e = new Exception();
41+
e.addSuppressed(decoderException);
42+
assertError(e, outOfMemoryError);
43+
44+
final int depth = randomIntBetween(1, 16);
45+
Throwable cause = new Exception();
46+
boolean fatal = false;
47+
Error error = null;
48+
for (int i = 0; i < depth; i++) {
49+
final int length = randomIntBetween(1, 4);
50+
for (int j = 0; j < length; j++) {
51+
if (!fatal && rarely()) {
52+
error = new Error();
53+
cause.addSuppressed(error);
54+
fatal = true;
55+
} else {
56+
cause.addSuppressed(new Exception());
57+
}
58+
}
59+
if (!fatal && rarely()) {
60+
cause = error = new Error(cause);
61+
fatal = true;
62+
} else {
63+
cause = new Exception(cause);
64+
}
65+
}
66+
if (fatal) {
67+
assertError(cause, error);
68+
} else {
69+
assertFalse(maybeError(cause, logger).isPresent());
70+
}
71+
72+
assertFalse(maybeError(new Exception(new DecoderException()), logger).isPresent());
73+
74+
Throwable chain = outOfMemoryError;
75+
for (int i = 0; i < MAX_ITERATIONS; i++) {
76+
chain = new Exception(chain);
77+
}
78+
assertFalse(maybeError(chain, logger).isPresent());
79+
}
80+
81+
private void assertError(final Throwable cause, final Error error) {
82+
final Optional<Error> maybeError = maybeError(cause, logger);
83+
assertTrue(maybeError.isPresent());
84+
assertThat(maybeError.get(), equalTo(error));
85+
}
86+
87+
}

modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.logging.log4j.Logger;
3131
import org.apache.lucene.util.BytesRef;
3232
import org.apache.lucene.util.BytesRefIterator;
33+
import org.elasticsearch.ExceptionsHelper;
3334
import org.elasticsearch.common.Booleans;
3435
import org.elasticsearch.common.bytes.BytesReference;
3536
import org.elasticsearch.common.logging.ESLoggerFactory;
@@ -38,12 +39,9 @@
3839
import java.util.ArrayList;
3940
import java.util.Arrays;
4041
import java.util.Collection;
41-
import java.util.Collections;
42-
import java.util.LinkedList;
4342
import java.util.List;
4443
import java.util.Locale;
4544
import java.util.Optional;
46-
import java.util.Queue;
4745
import java.util.concurrent.atomic.AtomicBoolean;
4846
import java.util.stream.Collectors;
4947

@@ -172,7 +170,8 @@ public static void closeChannels(final Collection<Channel> channels) throws IOEx
172170
* @param cause the throwable to test
173171
*/
174172
public static void maybeDie(final Throwable cause) {
175-
final Optional<Error> maybeError = maybeError(cause);
173+
final Logger logger = ESLoggerFactory.getLogger(Netty4Utils.class);
174+
final Optional<Error> maybeError = ExceptionsHelper.maybeError(cause, logger);
176175
if (maybeError.isPresent()) {
177176
/*
178177
* Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, Netty wraps too many
@@ -182,9 +181,7 @@ public static void maybeDie(final Throwable cause) {
182181
*/
183182
try {
184183
// try to log the current stack trace
185-
final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
186-
final String formatted = Arrays.stream(stackTrace).skip(1).map(e -> "\tat " + e).collect(Collectors.joining("\n"));
187-
final Logger logger = ESLoggerFactory.getLogger(Netty4Utils.class);
184+
final String formatted = ExceptionsHelper.formatStackTrace(Thread.currentThread().getStackTrace());
188185
logger.error("fatal error on the network layer\n{}", formatted);
189186
} finally {
190187
new Thread(
@@ -196,40 +193,4 @@ public static void maybeDie(final Throwable cause) {
196193
}
197194
}
198195

199-
static final int MAX_ITERATIONS = 1024;
200-
201-
/**
202-
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
203-
*
204-
* @param cause the root throwable
205-
*
206-
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
207-
*/
208-
static Optional<Error> maybeError(final Throwable cause) {
209-
// early terminate if the cause is already an error
210-
if (cause instanceof Error) {
211-
return Optional.of((Error) cause);
212-
}
213-
214-
final Queue<Throwable> queue = new LinkedList<>();
215-
queue.add(cause);
216-
int iterations = 0;
217-
while (!queue.isEmpty()) {
218-
iterations++;
219-
if (iterations > MAX_ITERATIONS) {
220-
ESLoggerFactory.getLogger(Netty4Utils.class).warn("giving up looking for fatal errors on the network layer", cause);
221-
break;
222-
}
223-
final Throwable current = queue.remove();
224-
if (current instanceof Error) {
225-
return Optional.of((Error) current);
226-
}
227-
Collections.addAll(queue, current.getSuppressed());
228-
if (current.getCause() != null) {
229-
queue.add(current.getCause());
230-
}
231-
}
232-
return Optional.empty();
233-
}
234-
235196
}

modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4UtilsTests.java

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import io.netty.buffer.ByteBuf;
2323
import io.netty.buffer.CompositeByteBuf;
2424
import io.netty.buffer.Unpooled;
25-
import io.netty.handler.codec.DecoderException;
2625
import org.apache.lucene.util.BytesRef;
2726
import org.elasticsearch.common.bytes.AbstractBytesReferenceTestCase;
2827
import org.elasticsearch.common.bytes.BytesArray;
@@ -33,9 +32,6 @@
3332
import org.elasticsearch.test.ESTestCase;
3433

3534
import java.io.IOException;
36-
import java.util.Optional;
37-
38-
import static org.hamcrest.CoreMatchers.equalTo;
3935

4036
public class Netty4UtilsTests extends ESTestCase {
4137

@@ -79,60 +75,6 @@ public void testToChannelBuffer() throws IOException {
7975
assertArrayEquals(BytesReference.toBytes(ref), BytesReference.toBytes(bytesReference));
8076
}
8177

82-
public void testMaybeError() {
83-
final Error outOfMemoryError = new OutOfMemoryError();
84-
assertError(outOfMemoryError, outOfMemoryError);
85-
86-
final DecoderException decoderException = new DecoderException(outOfMemoryError);
87-
assertError(decoderException, outOfMemoryError);
88-
89-
final Exception e = new Exception();
90-
e.addSuppressed(decoderException);
91-
assertError(e, outOfMemoryError);
92-
93-
final int depth = randomIntBetween(1, 16);
94-
Throwable cause = new Exception();
95-
boolean fatal = false;
96-
Error error = null;
97-
for (int i = 0; i < depth; i++) {
98-
final int length = randomIntBetween(1, 4);
99-
for (int j = 0; j < length; j++) {
100-
if (!fatal && rarely()) {
101-
error = new Error();
102-
cause.addSuppressed(error);
103-
fatal = true;
104-
} else {
105-
cause.addSuppressed(new Exception());
106-
}
107-
}
108-
if (!fatal && rarely()) {
109-
cause = error = new Error(cause);
110-
fatal = true;
111-
} else {
112-
cause = new Exception(cause);
113-
}
114-
}
115-
if (fatal) {
116-
assertError(cause, error);
117-
} else {
118-
assertFalse(Netty4Utils.maybeError(cause).isPresent());
119-
}
120-
121-
assertFalse(Netty4Utils.maybeError(new Exception(new DecoderException())).isPresent());
122-
123-
Throwable chain = outOfMemoryError;
124-
for (int i = 0; i < Netty4Utils.MAX_ITERATIONS; i++) {
125-
chain = new Exception(chain);
126-
}
127-
assertFalse(Netty4Utils.maybeError(chain).isPresent());
128-
}
129-
130-
private void assertError(final Throwable cause, final Error error) {
131-
final Optional<Error> maybeError = Netty4Utils.maybeError(cause);
132-
assertTrue(maybeError.isPresent());
133-
assertThat(maybeError.get(), equalTo(error));
134-
}
135-
13678
private BytesReference getRandomizedBytesReference(int length) throws IOException {
13779
// we know bytes stream output always creates a paged bytes reference, we use it to create randomized content
13880
ReleasableBytesStreamOutput out = new ReleasableBytesStreamOutput(length, bigarrays);

0 commit comments

Comments
 (0)